From 1f06ab4fb66484c2190e31d308b93739d901c378 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Fri, 20 Dec 2024 14:37:03 -0800 Subject: [PATCH] feat(zone): Add a property for Room.zone This is just an initial commit so that we can get the ball rolling to implement zones in other repos. --- honeybee/model.py | 19 ++++++++++++++++++ honeybee/room.py | 48 +++++++++++++++++++++++++++++++++++++++------ tests/model_test.py | 9 ++++++++- 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/honeybee/model.py b/honeybee/model.py index 6e8d7f73..92079b4f 100644 --- a/honeybee/model.py +++ b/honeybee/model.py @@ -961,6 +961,25 @@ def top_level_dict(self): base[sm.identifier] = sm return base + @property + def has_zones(self): + """Get a boolean for whether any Rooms in the model have zones assigned.""" + return any(room._zone is not None for room in self._rooms) + + @property + def zone_dict(self): + """Get dictionary of Rooms with zone identifiers as the keys. + + This is useful for grouping rooms by their Zone for export. + """ + zones = {} + for room in self.rooms: + try: + zones[room.zone].append(room) + except KeyError: # first room to be found in the zone + zones[room.zone] = [room] + return zones + def add_model(self, other_model): """Add another Model object to this model.""" assert isinstance(other_model, Model), \ diff --git a/honeybee/room.py b/honeybee/room.py index 1e2f23e7..e69bc313 100644 --- a/honeybee/room.py +++ b/honeybee/room.py @@ -56,6 +56,7 @@ class Room(_BaseWithShade): * display_name * faces * multiplier + * zone * story * exclude_floor_area * indoor_furniture @@ -83,8 +84,9 @@ class Room(_BaseWithShade): * user_data """ __slots__ = ( - '_geometry', '_faces', - '_multiplier', '_story', '_exclude_floor_area', '_parent') + '_geometry', '_faces', '_multiplier', '_zone', '_story', + '_exclude_floor_area', + '_parent') def __init__(self, identifier, faces, tolerance=0, angle_tolerance=0): """Initialize Room.""" @@ -124,6 +126,7 @@ def __init__(self, identifier, faces, tolerance=0, angle_tolerance=0): self._geometry = room_polyface self._multiplier = 1 # default value that can be overridden later + self._zone = None # default value that can be overridden later self._story = None # default value that can be overridden later self._exclude_floor_area = False # default value that can be overridden later self._parent = None # completely hidden as it is only used by Dragonfly @@ -164,6 +167,8 @@ def from_dict(cls, data, tolerance=0, angle_tolerance=0): room.user_data = data['user_data'] if 'multiplier' in data and data['multiplier'] is not None: room.multiplier = data['multiplier'] + if 'zone' in data and data['zone'] is not None: + room.zone = data['zone'] if 'story' in data and data['story'] is not None: room.story = data['story'] if 'exclude_floor_area' in data and data['exclude_floor_area'] is not None: @@ -276,6 +281,31 @@ def multiplier(self): def multiplier(self, value): self._multiplier = int_in_range(value, 1, input_name='room multiplier') + @property + def zone(self): + """Get or set text for the zone identifier to which this Room belongs. + + Rooms sharing the same zone identifier are considered part of the same + zone in a Model. If the zone identifier has not been specified, it + will be the same as the Room identifier. + + Note that the zone identifier has no character restrictions much + like display_name. + """ + if self._zone is None: + return self._identifier + return self._zone + + @zone.setter + def zone(self, value): + if value is not None: + try: + self._zone = str(value) + except UnicodeEncodeError: # Python 2 machine lacking the character set + self._zone = value # keep it as unicode + else: + self._zone = value + @property def story(self): """Get or set text for the story identifier to which this Room belongs. @@ -1330,8 +1360,10 @@ def is_geo_equivalent(self, room, tolerance=0.01): Returns: True if geometrically equivalent. False if not geometrically equivalent. """ - met_1 = (self.display_name, self.multiplier, self.story, self.exclude_floor_area) - met_2 = (room.display_name, room.multiplier, room.story, room.exclude_floor_area) + met_1 = (self.display_name, self.multiplier, self.zone, self.story, + self.exclude_floor_area) + met_2 = (room.display_name, room.multiplier, room.zone, room.story, + room.exclude_floor_area) if met_1 != met_2: return False if len(self._faces) != len(room._faces): @@ -2666,7 +2698,8 @@ def to_extrusion(self, tolerance=0.01, angle_tolerance=1.0): ext_room._display_name = self._display_name ext_room._user_data = None if self.user_data is None else self.user_data.copy() ext_room._multiplier = self.multiplier - ext_room._story = self.story + ext_room._zone = self._zone + ext_room._story = self._story ext_room._exclude_floor_area = self.exclude_floor_area ext_room._properties._duplicate_extension_attr(self._properties) return ext_room @@ -2719,6 +2752,8 @@ def to_dict(self, abridged=False, included_prop=None, include_plane=True): self._add_shades_to_dict(base, abridged, included_prop, include_plane) if self.multiplier != 1: base['multiplier'] = self.multiplier + if self._zone is not None: + base['zone'] = self.zone if self.story is not None: base['story'] = self.story if self.exclude_floor_area: @@ -2905,7 +2940,8 @@ def __copy__(self): new_r._display_name = self._display_name new_r._user_data = None if self.user_data is None else self.user_data.copy() new_r._multiplier = self.multiplier - new_r._story = self.story + new_r._zone = self._zone + new_r._story = self._story new_r._exclude_floor_area = self.exclude_floor_area self._duplicate_child_shades(new_r) new_r._geometry = self._geometry diff --git a/tests/model_test.py b/tests/model_test.py index 4b0694af..49f91c96 100644 --- a/tests/model_test.py +++ b/tests/model_test.py @@ -92,6 +92,7 @@ def test_model_properties_setability(): assert model.angle_tolerance == 0.01 model.tolerance = None assert model.tolerance == 0.01 + assert not model.has_zones def test_model_init_orphaned_objects(): @@ -146,15 +147,17 @@ def test_model_init_orphaned_objects(): assert len(model.orphaned_shades) == 2 assert len(model.orphaned_apertures) == 1 assert len(model.orphaned_doors) == 1 + assert not model.has_zones def test_adjacent_zone_model(): """Test the solve adjacency method with an interior aperture.""" room_south = Room.from_box('SouthZone', 5, 5, 3, origin=Point3D(0, 0, 0)) room_north = Room.from_box('NorthZone', 5, 5, 3, origin=Point3D(0, 5, 0)) + room_south.zone = 'FullHouse' + room_north.zone = 'FullHouse' room_south[1].apertures_by_ratio(0.4, 0.01) room_north[3].apertures_by_ratio(0.4, 0.01) - room_south[3].apertures_by_ratio(0.4, 0.01) room_south[3].apertures[0].overhang(0.5, indoor=False) room_south[3].apertures[0].overhang(0.5, indoor=True) @@ -176,6 +179,8 @@ def test_adjacent_zone_model(): assert len(model.shades) == 2 assert len(model.apertures) == 4 assert len(model.doors) == 1 + assert model.has_zones + assert len(model.zone_dict) == 1 model_dict = model.to_dict() new_model = Model.from_dict(model_dict) @@ -186,6 +191,8 @@ def test_adjacent_zone_model(): new_model.rooms[1][3].apertures[0].identifier assert new_model.rooms[1][3].apertures[0].boundary_condition.boundary_condition_object == \ new_model.rooms[0][1].apertures[0].identifier + assert model.has_zones + assert len(model.zone_dict) == 1 def test_model_init_from_objects():