honeybee-core 1.62.12__py3-none-any.whl → 1.64.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
honeybee/aperture.py CHANGED
@@ -94,13 +94,6 @@ class Aperture(_BaseWithShade):
94
94
  assert data['type'] == 'Aperture', 'Expected Aperture dictionary. ' \
95
95
  'Got {}.'.format(data['type'])
96
96
 
97
- # remove any invalid holes from the geometry
98
- geo_dict = data['geometry']
99
- if 'holes' in geo_dict and geo_dict['holes'] is not None:
100
- for i, hole_list in enumerate(geo_dict['holes']):
101
- if len(hole_list) < 3:
102
- geo_dict['holes'].pop(i)
103
-
104
97
  # serialize the aperture
105
98
  is_operable = data['is_operable'] if 'is_operable' in data else False
106
99
  if data['boundary_condition']['type'] == 'Outdoors':
honeybee/door.py CHANGED
@@ -96,13 +96,6 @@ class Door(_BaseWithShade):
96
96
  assert data['type'] == 'Door', 'Expected Door dictionary. ' \
97
97
  'Got {}.'.format(data['type'])
98
98
 
99
- # remove any invalid holes from the geometry
100
- geo_dict = data['geometry']
101
- if 'holes' in geo_dict and geo_dict['holes'] is not None:
102
- for i, hole_list in enumerate(geo_dict['holes']):
103
- if len(hole_list) < 3:
104
- geo_dict['holes'].pop(i)
105
-
106
99
  # serialize the door
107
100
  is_glass = data['is_glass'] if 'is_glass' in data else False
108
101
  if data['boundary_condition']['type'] == 'Outdoors':
honeybee/face.py CHANGED
@@ -133,13 +133,6 @@ class Face(_BaseWithShade):
133
133
  assert data['type'] == 'Face', 'Expected Face dictionary. ' \
134
134
  'Got {}.'.format(data['type'])
135
135
 
136
- # remove any invalid holes from the geometry
137
- geo_dict = data['geometry']
138
- if 'holes' in geo_dict and geo_dict['holes'] is not None:
139
- for i, hole_list in enumerate(geo_dict['holes']):
140
- if len(hole_list) < 3:
141
- geo_dict['holes'].pop(i)
142
-
143
136
  # first serialize it with an outdoor boundary condition
144
137
  face_type = face_types.by_name(data['face_type'])
145
138
  face = cls(data['identifier'], Face3D.from_dict(data['geometry']),
honeybee/model.py CHANGED
@@ -107,6 +107,18 @@ class Model(_Base):
107
107
  * exterior_skylight_aperture_area
108
108
  * min
109
109
  * max
110
+ * roof_to_exterior_edges
111
+ * slab_to_exterior_edges
112
+ * exposed_floor_to_exterior_wall_edges
113
+ * exterior_wall_to_wall_edges
114
+ * roof_ridge_edges
115
+ * exposed_floor_to_floor_edges
116
+ * underground_edges
117
+ * interior_edges
118
+ * exterior_aperture_edges
119
+ * exterior_door_edges
120
+ * exterior_aperture_edges
121
+ * exterior_door_edges
110
122
  * top_level_dict
111
123
  * user_data
112
124
  """
@@ -161,11 +173,16 @@ class Model(_Base):
161
173
  self._properties = ModelProperties(self)
162
174
 
163
175
  @classmethod
164
- def from_dict(cls, data):
176
+ def from_dict(cls, data, cleanup_irrational=False):
165
177
  """Initialize a Model from a dictionary.
166
178
 
167
179
  Args:
168
180
  data: A dictionary representation of a Model object.
181
+ cleanup_irrational: Boolean to note whether common types of irrational
182
+ objects should be cleaned or removed from the dictionary before
183
+ serializing the model to Python. Typical cases that are removed
184
+ this way include Face3Ds with fewer than 3 vertices, Rooms that
185
+ have no Face geometry, etc. (Default: False).
169
186
  """
170
187
  # check the type of dictionary
171
188
  assert data['type'] == 'Model', 'Expected Model dictionary. ' \
@@ -179,6 +196,10 @@ class Model(_Base):
179
196
  angle_tol = 1.0 if 'angle_tolerance' not in data or \
180
197
  data['angle_tolerance'] is None else data['angle_tolerance']
181
198
 
199
+ # clean the irrational objects out if requested
200
+ if cleanup_irrational:
201
+ cls.clean_irrational_geometry(data)
202
+
182
203
  # import all of the geometry
183
204
  rooms = None # import rooms
184
205
  if 'rooms' in data and data['rooms'] is not None:
@@ -244,11 +265,16 @@ class Model(_Base):
244
265
  return model
245
266
 
246
267
  @classmethod
247
- def from_file(cls, hb_file):
268
+ def from_file(cls, hb_file, cleanup_irrational=False):
248
269
  """Initialize a Model from a HBJSON or HBpkl file, auto-sensing the type.
249
270
 
250
271
  Args:
251
272
  hb_file: Path to either a HBJSON or HBpkl file.
273
+ cleanup_irrational: Boolean to note whether common types of irrational
274
+ objects should be cleaned or removed from the dictionary before
275
+ serializing the model to Python. Typical cases that are removed
276
+ this way include Face3Ds with fewer than 3 vertices, Rooms that
277
+ have no Face geometry, etc. (Default: False).
252
278
  """
253
279
  # sense the file type from the first character to avoid maxing memory with JSON
254
280
  # this is needed since queenbee overwrites all file extensions
@@ -258,15 +284,20 @@ class Model(_Base):
258
284
  is_json = True if first_char == '{' or second_char == '{' else False
259
285
  # load the file using either HBJSON pathway or HBpkl
260
286
  if is_json:
261
- return cls.from_hbjson(hb_file)
262
- return cls.from_hbpkl(hb_file)
287
+ return cls.from_hbjson(hb_file, cleanup_irrational)
288
+ return cls.from_hbpkl(hb_file, cleanup_irrational)
263
289
 
264
290
  @classmethod
265
- def from_hbjson(cls, hbjson_file):
291
+ def from_hbjson(cls, hbjson_file, cleanup_irrational=False):
266
292
  """Initialize a Model from a HBJSON file.
267
293
 
268
294
  Args:
269
295
  hbjson_file: Path to HBJSON file.
296
+ cleanup_irrational: Boolean to note whether common types of irrational
297
+ objects should be cleaned or removed from the dictionary before
298
+ serializing the model to Python. Typical cases that are removed
299
+ this way include Face3Ds with fewer than 3 vertices, Rooms that
300
+ have no Face geometry, etc. (Default: False).
270
301
  """
271
302
  assert os.path.isfile(hbjson_file), 'Failed to find %s' % hbjson_file
272
303
  with io.open(hbjson_file, encoding='utf-8') as inf:
@@ -276,19 +307,24 @@ class Model(_Base):
276
307
  if second_char == '{':
277
308
  inf.read(1)
278
309
  data = json.load(inf)
279
- return cls.from_dict(data)
310
+ return cls.from_dict(data, cleanup_irrational)
280
311
 
281
312
  @classmethod
282
- def from_hbpkl(cls, hbpkl_file):
313
+ def from_hbpkl(cls, hbpkl_file, cleanup_irrational=False):
283
314
  """Initialize a Model from a HBpkl file.
284
315
 
285
316
  Args:
286
317
  hbpkl_file: Path to HBpkl file.
318
+ cleanup_irrational: Boolean to note whether common types of irrational
319
+ objects should be cleaned or removed from the dictionary before
320
+ serializing the model to Python. Typical cases that are removed
321
+ this way include Face3Ds with fewer than 3 vertices, Rooms that
322
+ have no Face geometry, etc. (Default: False).
287
323
  """
288
324
  assert os.path.isfile(hbpkl_file), 'Failed to find %s' % hbpkl_file
289
325
  with open(hbpkl_file, 'rb') as inf:
290
326
  data = pickle.load(inf)
291
- return cls.from_dict(data)
327
+ return cls.from_dict(data, cleanup_irrational)
292
328
 
293
329
  @classmethod
294
330
  def from_stl(cls, file_path, geometry_to_faces=False, units='Meters',
@@ -990,6 +1026,92 @@ class Model(_Base):
990
1026
  """Get a Point3D for the max bounding box vertex in the XY plane."""
991
1027
  return self._calculate_max(self._all_objects())
992
1028
 
1029
+ @property
1030
+ def roof_to_exterior_edges(self):
1031
+ """Get LineSegment3Ds where roofs meet exterior walls (or floors).
1032
+
1033
+ Note that both the roof Face and the wall/floor Face must be next to one
1034
+ another in the model's outer envelope and have outdoor boundary conditions for
1035
+ the edge to show up in this list.
1036
+ """
1037
+ return self.classified_envelope_edges()[0]
1038
+
1039
+ @property
1040
+ def slab_to_exterior_edges(self):
1041
+ """Get LineSegment3Ds where ground floor slabs meet exterior walls or roofs.
1042
+
1043
+ Note that the floor Face must have a ground boundary condition and the wall or
1044
+ roof Face must have an outdoor boundary condition for the edge between the
1045
+ two Faces to show up in this list.
1046
+ """
1047
+ return self.classified_envelope_edges()[1]
1048
+
1049
+ @property
1050
+ def exposed_floor_to_exterior_wall_edges(self):
1051
+ """Get LineSegment3Ds where exposed floors meet exterior walls.
1052
+
1053
+ Note that both the wall Face and the floor Face must be next to one
1054
+ another in the model's outer envelope and have outdoor boundary conditions for
1055
+ the edge to show up in this list.
1056
+ """
1057
+ return self.classified_envelope_edges()[2]
1058
+
1059
+ @property
1060
+ def exterior_wall_to_wall_edges(self):
1061
+ """Get LineSegment3Ds where exterior walls meet one another.
1062
+
1063
+ Note that both wall Faces must be next to one another in the model's
1064
+ outer envelope and have outdoor boundary conditions for the edge to
1065
+ show up in this list.
1066
+ """
1067
+ return self.classified_envelope_edges()[3]
1068
+
1069
+ @property
1070
+ def roof_ridge_edges(self):
1071
+ """Get a list of LineSegment3D where exterior roofs meet one another.
1072
+
1073
+ Note that both roof Faces must be next to one another in the model's
1074
+ outer envelope and have outdoor boundary conditions for the edge to
1075
+ show up in this list.
1076
+ """
1077
+ return self.classified_envelope_edges()[4]
1078
+
1079
+ @property
1080
+ def exposed_floor_to_floor_edges(self):
1081
+ """Get LineSegment3Ds where exposed floors meet one another.
1082
+
1083
+ Note that both floor Faces must be next to one another in the model's
1084
+ outer envelope and have outdoor boundary conditions for the edge to
1085
+ show up in this list.
1086
+ """
1087
+ return self.classified_envelope_edges()[5]
1088
+
1089
+ @property
1090
+ def underground_edges(self):
1091
+ """Get a list of LineSegment3D where underground Faces meet one another.
1092
+
1093
+ Note that both Faces must be next to one another in the model's outer envelope
1094
+ and have ground boundary conditions for the edge to show up in this list.
1095
+ """
1096
+ return self.classified_envelope_edges()[6]
1097
+
1098
+ @property
1099
+ def exterior_aperture_edges(self):
1100
+ """Get a list of LineSegment3D for the borders around room exterior apertures.
1101
+ """
1102
+ edges = []
1103
+ for room in self.rooms:
1104
+ edges.extend(room.exterior_aperture_edges)
1105
+ return edges
1106
+
1107
+ @property
1108
+ def exterior_door_edges(self):
1109
+ """Get a list of LineSegment3D for the borders around room exterior doors."""
1110
+ edges = []
1111
+ for room in self.rooms:
1112
+ edges.extend(room.exterior_door_edges)
1113
+ return edges
1114
+
993
1115
  @property
994
1116
  def top_level_dict(self):
995
1117
  """Get dictionary of top-level model objects with identifiers as the keys.
@@ -1354,6 +1476,63 @@ class Model(_Base):
1354
1476
  )
1355
1477
  return shades
1356
1478
 
1479
+ def classified_envelope_edges(self, tolerance=None, exclude_coplanar=False):
1480
+ """Get classified edges of this Model's envelope based on Faces they adjoin.
1481
+
1482
+ The edges returned by this method will only exist along the exterior
1483
+ envelope of the Model's Rooms as defined by the contiguous volume across
1484
+ all Room interior adjacencies.
1485
+
1486
+ Args:
1487
+ tolerance: The maximum difference between point values for them to be
1488
+ considered equivalent. If None, the Model's tolerance will be used.
1489
+ exclude_coplanar: Boolean to note whether edges falling between two
1490
+ coplanar Faces in the building envelope should be included
1491
+ in the result (False) or excluded from it (True). (Default: False).
1492
+
1493
+ Returns:
1494
+ A tuple with eight items where each item is a list containing
1495
+ LineSegment3D adjoining different types of Faces.
1496
+
1497
+ - roof_to_exterior - Roofs meet exterior walls or floors.
1498
+
1499
+ - slab_to_exterior - Ground floor slabs meet exterior walls or roofs.
1500
+
1501
+ - exposed_floor_to_exterior_wall - Exposed floors meet exterior walls.
1502
+
1503
+ - exterior_wall_to_wall - Exterior walls meet.
1504
+
1505
+ - roof_ridge - Exterior roofs meet.
1506
+
1507
+ - exposed_floor_to_floor - Exposed floors meet.
1508
+
1509
+ - underground - Underground faces meet.
1510
+ """
1511
+ # set up lists to be populated
1512
+ roof_to_exterior, slab_to_exterior, exposed_floor_to_exterior_wall = [], [], []
1513
+ exterior_wall_to_wall, roof_ridge, exposed_floor_to_floor = [], [], []
1514
+ underground, interior = [], []
1515
+ tol = tolerance if tolerance else self.tolerance
1516
+ ang_tol = self.angle_tolerance if exclude_coplanar else None
1517
+
1518
+ # join all of the rooms in the model across their adjacencies
1519
+ merged_rooms = Room.join_adjacent_rooms(self.rooms, tol)
1520
+ for room in merged_rooms:
1521
+ rf_to_ext, slb_to_ext, ex_flr_to_ext, ext_wl_to_wl, rf_ridge, \
1522
+ ex_flr_to_flr, under_gnd, inter = room.classified_edges(tol, ang_tol)
1523
+ roof_to_exterior.extend(rf_to_ext)
1524
+ slab_to_exterior.extend(slb_to_ext)
1525
+ exposed_floor_to_exterior_wall.extend(ex_flr_to_ext)
1526
+ exterior_wall_to_wall.extend(ext_wl_to_wl)
1527
+ roof_ridge.extend(rf_ridge)
1528
+ exposed_floor_to_floor.extend(ex_flr_to_flr)
1529
+ underground.extend(under_gnd)
1530
+ interior.extend(inter)
1531
+
1532
+ # return the classified edges
1533
+ return roof_to_exterior, slab_to_exterior, exposed_floor_to_exterior_wall, \
1534
+ exterior_wall_to_wall, roof_ridge, exposed_floor_to_floor, underground
1535
+
1357
1536
  def add_prefix(self, prefix):
1358
1537
  """Change the identifier of this object and child objects by inserting a prefix.
1359
1538
 
@@ -3685,6 +3864,97 @@ class Model(_Base):
3685
3864
  out_dict['valid'] = False
3686
3865
  return json.dumps(out_dict, indent=4)
3687
3866
 
3867
+ @staticmethod
3868
+ def clean_irrational_geometry(model_dict):
3869
+ """Remove irrational geometry objects from a honeybee Model dictionary.
3870
+
3871
+ This can be useful to run prior to serializing the Model object from a
3872
+ dictionary if it was produced from a source other than the Python
3873
+ core libraries, in which case the dictionary is necessarily rational
3874
+ and serializable. This is because not all honeybee-schema bindings
3875
+ enforce fundamental definitions of geometry types upon initialization
3876
+ of the geometry objects, leading to exceptions when an attempt is made
3877
+ to serialize them to Python. Furthermore, it is possible that the honeybee
3878
+ Model dictionary did not originate from any schema bindings at all, in
3879
+ which case it is highly recommended that this method be run.
3880
+
3881
+ Typical irrational geometry cases that are removed by this method include.
3882
+
3883
+ * Apertures/Doors with less than 3 vertices or holes less than 3 vertices.
3884
+ * Faces with less than 3 vertices or holes with less than 3 vertices.
3885
+ * Rooms that have no Face geometry.
3886
+ * Shade Face3Ds with less than 3 vertices or holes with less than 3 vertices.
3887
+ * ShadeMesh Mesh3Ds with no faces or faces with less than 3 vertices.
3888
+ """
3889
+ # clean all of the Room geometry in the model dictionary
3890
+ if 'rooms' in model_dict and model_dict['rooms'] is not None:
3891
+ for ri in range(len(model_dict['rooms']) - 1, -1, -1):
3892
+ r_dict = model_dict['rooms'][ri]
3893
+ # clean all of the Face geometry
3894
+ Model._clean_irrational_geo_with_shade(r_dict['faces'])
3895
+ for f_dict in r_dict['faces']:
3896
+ # clean all of the Aperture/Door geometry
3897
+ if 'apertures' in f_dict and f_dict['apertures'] is not None:
3898
+ Model._clean_irrational_geo_with_shade(f_dict['apertures'])
3899
+ if 'doors' in f_dict and f_dict['doors'] is not None:
3900
+ Model._clean_irrational_geo_with_shade(f_dict['doors'])
3901
+ # clean the assigned shade geometry
3902
+ if 'outdoor_shades' in r_dict and r_dict['outdoor_shades'] is not None:
3903
+ Model._clean_irrational_face3ds(r_dict['outdoor_shades'])
3904
+ if 'indoor_shades' in r_dict and r_dict['indoor_shades'] is not None:
3905
+ Model._clean_irrational_face3ds(r_dict['indoor_shades'])
3906
+ if len(r_dict['faces']) == 0: # the entire Room is irrational
3907
+ model_dict['rooms'].pop(ri)
3908
+ # clean all of the orphaned geometry in the model dictionary
3909
+ if 'orphaned_faces' in model_dict and model_dict['orphaned_faces'] is not None:
3910
+ Model._clean_irrational_geo_with_shade(model_dict['orphaned_faces'])
3911
+ if 'orphaned_apertures' in model_dict and \
3912
+ model_dict['orphaned_apertures'] is not None:
3913
+ Model._clean_irrational_geo_with_shade(model_dict['orphaned_apertures'])
3914
+ if 'orphaned_doors' in model_dict and model_dict['orphaned_doors'] is not None:
3915
+ Model._clean_irrational_geo_with_shade(model_dict['orphaned_doors'])
3916
+ if 'orphaned_shades' in model_dict and model_dict['orphaned_shades'] is not None:
3917
+ Model._clean_irrational_face3ds(model_dict['orphaned_shades'])
3918
+ # clean all of the shade mesh geometry in the model dictionary
3919
+ if 'shade_meshes' in model_dict and model_dict['shade_meshes'] is not None:
3920
+ for smi in range(len(model_dict['shade_meshes']) - 1, -1, -1):
3921
+ sm_dict = model_dict['shade_meshes'][smi]['geometry']
3922
+ for mfi in range(len(sm_dict['faces']) - 1, -1, -1):
3923
+ mf = sm_dict['faces'][mfi]
3924
+ if len(mf) not in (3, 4):
3925
+ sm_dict['faces'].pop(mfi)
3926
+ else:
3927
+ for ind in mf:
3928
+ try:
3929
+ sm_dict['vertices'][ind]
3930
+ except IndexError:
3931
+ sm_dict['faces'].pop(mfi)
3932
+ break
3933
+ if len(sm_dict['faces']) == 0:
3934
+ model_dict['shade_meshes'].pop(smi)
3935
+
3936
+ @staticmethod
3937
+ def _clean_irrational_geo_with_shade(geo_obj_dicts):
3938
+ """Clean irrational Face3Ds out of a list of honeybee geometry objects."""
3939
+ Model._clean_irrational_face3ds(geo_obj_dicts)
3940
+ for f_dict in geo_obj_dicts:
3941
+ if 'outdoor_shades' in f_dict and f_dict['outdoor_shades'] is not None:
3942
+ Model._clean_irrational_face3ds(f_dict['outdoor_shades'])
3943
+ if 'indoor_shades' in f_dict and f_dict['indoor_shades'] is not None:
3944
+ Model._clean_irrational_face3ds(f_dict['indoor_shades'])
3945
+
3946
+ @staticmethod
3947
+ def _clean_irrational_face3ds(geo_obj_dicts):
3948
+ """Clean irrational Face3Ds out of a list of honeybee geometry objects."""
3949
+ for fi in range(len(geo_obj_dicts) - 1, -1, -1):
3950
+ f_dict = geo_obj_dicts[fi]
3951
+ face_3d = f_dict['geometry']
3952
+ if len(face_3d['boundary']) < 3:
3953
+ geo_obj_dicts.pop(fi)
3954
+ continue
3955
+ elif 'holes' in face_3d:
3956
+ face_3d['holes'] = [h for h in face_3d['holes'] if len(h) >= 3]
3957
+
3688
3958
  @staticmethod
3689
3959
  def conversion_factor_to_meters(units):
3690
3960
  """Get the conversion factor to meters based on input units.
honeybee/room.py CHANGED
@@ -8,6 +8,7 @@ import uuid
8
8
  from ladybug_geometry.geometry2d import Point2D, Vector2D, Polygon2D
9
9
  from ladybug_geometry.geometry3d import Point3D, Vector3D, Ray3D, Plane, Face3D, \
10
10
  Mesh3D, Polyface3D
11
+ from ladybug_geometry.bounding import overlapping_bounding_boxes
11
12
  from ladybug_geometry_polyskel.polysplit import perimeter_core_subpolygons
12
13
 
13
14
  import honeybee.writer.room as writer
@@ -71,6 +72,8 @@ class Room(_BaseWithShade):
71
72
  * center
72
73
  * min
73
74
  * max
75
+ * exterior_aperture_edges
76
+ * exterior_door_edges
74
77
  * volume
75
78
  * floor_area
76
79
  * exposed_area
@@ -430,6 +433,24 @@ class Room(_BaseWithShade):
430
433
  all_geo.extend(self._faces)
431
434
  return self._calculate_max(all_geo)
432
435
 
436
+ @property
437
+ def exterior_aperture_edges(self):
438
+ """Get a list of LineSegment3D for the borders around exterior apertures."""
439
+ edges = []
440
+ for ap in self.apertures:
441
+ if isinstance(ap.boundary_condition, Outdoors):
442
+ edges.extend(ap.geometry.segments)
443
+ return edges
444
+
445
+ @property
446
+ def exterior_door_edges(self):
447
+ """Get a list of LineSegment3D for the borders around exterior doors."""
448
+ edges = []
449
+ for dr in self.doors:
450
+ if isinstance(dr.boundary_condition, Outdoors):
451
+ edges.extend(dr.geometry.segments)
452
+ return edges
453
+
433
454
  @property
434
455
  def volume(self):
435
456
  """Get the volume of the room.
@@ -584,101 +605,92 @@ class Room(_BaseWithShade):
584
605
  areas += face.area
585
606
  return orientations / areas if areas != 0 else None
586
607
 
587
- def add_prefix(self, prefix):
588
- """Change the identifier of this object and child objects by inserting a prefix.
589
-
590
- This is particularly useful in workflows where you duplicate and edit
591
- a starting object and then want to combine it with the original object
592
- into one Model (like making a model of repeated rooms) since all objects
593
- within a Model must have unique identifiers.
608
+ def classified_edges(self, tolerance=0.01, angle_tolerance=None):
609
+ """Get classified edges of this Room's Polyface3D based on Faces they adjoin.
594
610
 
595
611
  Args:
596
- prefix: Text that will be inserted at the start of this object's
597
- (and child objects') identifier and display_name. It is recommended
598
- that this prefix be short to avoid maxing out the 100 allowable
599
- characters for honeybee identifiers.
600
- """
601
- self._identifier = clean_string('{}_{}'.format(prefix, self.identifier))
602
- self.display_name = '{}_{}'.format(prefix, self.display_name)
603
- self.properties.add_prefix(prefix)
604
- for face in self._faces:
605
- face.add_prefix(prefix)
606
- self._add_prefix_shades(prefix)
612
+ tolerance: The maximum difference between point values for them to be
613
+ considered equivalent. (Default: 0.01, suitable for objects in meters).
614
+ angle_tolerance: An optional value in degrees, which can be used to
615
+ exclude edges falling between coplanar Faces. If None, edges falling
616
+ between coplanar Faces will be included. (Default: None).
607
617
 
608
- def rename_by_attribute(
609
- self, format_str='{story} - {display_name}'
610
- ):
611
- """Set the display name of this Room using a format string with Room attributes.
618
+ Returns:
619
+ A tuple with eight items where each item is a list containing
620
+ LineSegment3D adjoining different types of Faces.
612
621
 
613
- Args:
614
- format_str: Text string for the pattern with which the Room will be
615
- renamed. Any property on this class may be used (eg. story)
616
- and each property should be put in curly brackets. Nested
617
- properties can be specified by using "." to denote nesting levels
618
- (eg. properties.energy.program_type.display_name). Functions that
619
- return string outputs can also be passed here as long as these
620
- functions defaults specified for all arguments.
621
- """
622
- matches = re.findall(r'{([^}]*)}', format_str)
623
- attributes = [get_attr_nested(self, m) for m in matches]
624
- for attr_name, attr_val in zip(matches, attributes):
625
- format_str = format_str.replace('{{{}}}'.format(attr_name), attr_val)
626
- self.display_name = format_str
627
- return format_str
622
+ - roof_to_exterior - Roofs meet exterior walls or floors.
628
623
 
629
- def rename_faces_by_attribute(
630
- self,
631
- format_str='{parent.display_name} - {gbxml_type} - {cardinal_direction}'
632
- ):
633
- """Set the display name for all of this Room's faces using a format string.
624
+ - slab_to_exterior - Ground floor slabs meet exterior walls or roofs.
634
625
 
635
- Args:
636
- format_str: Text string for the pattern with which the faces will be
637
- renamed. Any property of the Face class may be used (eg. gbxml_str)
638
- and each property should be put in curly brackets. Nested
639
- properties can be specified by using "." to denote nesting levels
640
- (eg. properties.energy.construction.display_name). Functions that
641
- return string outputs can also be passed here as long as these
642
- functions defaults specified for all arguments.
643
- """
644
- for face in self.faces:
645
- face.rename_by_attribute(format_str)
626
+ - exposed_floor_to_exterior_wall - Exposed floors meet exterior walls.
646
627
 
647
- def rename_apertures_by_attribute(
648
- self,
649
- format_str='{parent.parent.display_name} - {gbxml_type} - {cardinal_direction}'
650
- ):
651
- """Set the display name for all of this Room's apertures using a format string.
628
+ - exterior_wall_to_wall - Exterior walls meet.
652
629
 
653
- Args:
654
- format_str: Text string for the pattern with which the apertures will be
655
- renamed. Any property on the Aperture class may be used (eg. gbxml_str)
656
- and each property should be put in curly brackets. Nested
657
- properties can be specified by using "." to denote nesting levels
658
- (eg. properties.energy.construction.display_name). Functions that
659
- return string outputs can also be passed here as long as these
660
- functions defaults specified for all arguments.
661
- """
662
- for ap in self.apertures:
663
- ap.rename_by_attribute(format_str)
630
+ - roof_ridge - Exterior roofs meet.
664
631
 
665
- def rename_doors_by_attribute(
666
- self,
667
- format_str='{parent.parent.display_name} - {energyplus_type} - {cardinal_direction}'
668
- ):
669
- """Set the display name for all of this Room's doors using a format string.
632
+ - exposed_floor_to_floor - Exposed floors meet.
670
633
 
671
- Args:
672
- format_str: Text string for the pattern with which the doors will be
673
- renamed. Any property on the Door class may be used (eg. gbxml_str)
674
- and each property should be put in curly brackets. Nested
675
- properties can be specified by using "." to denote nesting levels
676
- (eg. properties.energy.construction.display_name). Functions that
677
- return string outputs can also be passed here as long as these
678
- functions defaults specified for all arguments.
634
+ - underground - Underground faces meet.
635
+
636
+ - interior - Interior faces meet.
679
637
  """
680
- for dr in self.doors:
681
- dr.rename_by_attribute(format_str)
638
+ # set up lists to be populated
639
+ roof_to_exterior, slab_to_exterior, exposed_floor_to_exterior_wall = [], [], []
640
+ exterior_wall_to_wall, roof_ridge, exposed_floor_to_floor = [], [], []
641
+ underground, interior = [], []
642
+
643
+ # get all of the edges and map them to room faces
644
+ edges = self.geometry.internal_edges
645
+ edge_faces = [[] for _ in edges]
646
+ for i, edge in enumerate(edges):
647
+ for face in self.faces:
648
+ if overlapping_bounding_boxes(face.geometry, edge, tolerance):
649
+ for f_edge in face.geometry.segments:
650
+ if edge.distance_to_point(f_edge.p1) < tolerance and \
651
+ edge.distance_to_point(f_edge.p2) < tolerance:
652
+ edge_faces[i].append(face)
653
+ break
654
+
655
+ # classify the edges by analyzing the faces they adjoin
656
+ for edge, faces in zip(edges, edge_faces):
657
+ # first check for cases where the edge should be excluded
658
+ if len(edge_faces) <= 1: # not an edge between two faces
659
+ continue
660
+ if angle_tolerance is not None:
661
+ ang_tol = math.radians(angle_tolerance)
662
+ base_normal = faces[0].normal
663
+ if all(f.normal.angle(base_normal) < ang_tol for f in faces[1:]):
664
+ continue
665
+
666
+ # then check for which category the edge should go into
667
+ ext_faces = [f for f in faces if isinstance(f.boundary_condition, Outdoors)
668
+ and not isinstance(f.type, AirBoundary)]
669
+ if len(ext_faces) >= 2: # some type of exterior edge
670
+ if all(isinstance(f.type, Wall) for f in ext_faces):
671
+ exterior_wall_to_wall.append(edge)
672
+ elif all(isinstance(f.type, RoofCeiling) for f in ext_faces):
673
+ roof_ridge.append(edge)
674
+ elif all(isinstance(f.type, Floor) for f in ext_faces):
675
+ exposed_floor_to_floor.append(edge)
676
+ elif any(isinstance(f.type, RoofCeiling) for f in ext_faces):
677
+ roof_to_exterior.append(edge)
678
+ elif any(isinstance(f.type, Floor) for f in ext_faces):
679
+ exposed_floor_to_exterior_wall.append(edge)
680
+ else:
681
+ gnd_faces = [f for f in faces if isinstance(f.boundary_condition, Ground)
682
+ and not isinstance(f.type, AirBoundary)]
683
+ if len(ext_faces) >= 1 and len(gnd_faces) >= 1:
684
+ slab_to_exterior.append(edge)
685
+ elif len(gnd_faces) >= 2: # some type of underground edge
686
+ underground.append(edge)
687
+ else: # some type of interior edge
688
+ interior.append(edge)
689
+
690
+ # return the classified edges
691
+ return roof_to_exterior, slab_to_exterior, exposed_floor_to_exterior_wall, \
692
+ exterior_wall_to_wall, roof_ridge, exposed_floor_to_floor, \
693
+ underground, interior
682
694
 
683
695
  def horizontal_boundary(self, match_walls=False, tolerance=0.01):
684
696
  """Get a Face3D representing the horizontal boundary around the Room.
@@ -774,6 +786,102 @@ class Room(_BaseWithShade):
774
786
  return self._match_walls_to_horizontal_faces(horiz_bound, tolerance)
775
787
  return horiz_bound
776
788
 
789
+ def add_prefix(self, prefix):
790
+ """Change the identifier of this object and child objects by inserting a prefix.
791
+
792
+ This is particularly useful in workflows where you duplicate and edit
793
+ a starting object and then want to combine it with the original object
794
+ into one Model (like making a model of repeated rooms) since all objects
795
+ within a Model must have unique identifiers.
796
+
797
+ Args:
798
+ prefix: Text that will be inserted at the start of this object's
799
+ (and child objects') identifier and display_name. It is recommended
800
+ that this prefix be short to avoid maxing out the 100 allowable
801
+ characters for honeybee identifiers.
802
+ """
803
+ self._identifier = clean_string('{}_{}'.format(prefix, self.identifier))
804
+ self.display_name = '{}_{}'.format(prefix, self.display_name)
805
+ self.properties.add_prefix(prefix)
806
+ for face in self._faces:
807
+ face.add_prefix(prefix)
808
+ self._add_prefix_shades(prefix)
809
+
810
+ def rename_by_attribute(
811
+ self, format_str='{story} - {display_name}'
812
+ ):
813
+ """Set the display name of this Room using a format string with Room attributes.
814
+
815
+ Args:
816
+ format_str: Text string for the pattern with which the Room will be
817
+ renamed. Any property on this class may be used (eg. story)
818
+ and each property should be put in curly brackets. Nested
819
+ properties can be specified by using "." to denote nesting levels
820
+ (eg. properties.energy.program_type.display_name). Functions that
821
+ return string outputs can also be passed here as long as these
822
+ functions defaults specified for all arguments.
823
+ """
824
+ matches = re.findall(r'{([^}]*)}', format_str)
825
+ attributes = [get_attr_nested(self, m) for m in matches]
826
+ for attr_name, attr_val in zip(matches, attributes):
827
+ format_str = format_str.replace('{{{}}}'.format(attr_name), attr_val)
828
+ self.display_name = format_str
829
+ return format_str
830
+
831
+ def rename_faces_by_attribute(
832
+ self,
833
+ format_str='{parent.display_name} - {gbxml_type} - {cardinal_direction}'
834
+ ):
835
+ """Set the display name for all of this Room's faces using a format string.
836
+
837
+ Args:
838
+ format_str: Text string for the pattern with which the faces will be
839
+ renamed. Any property of the Face class may be used (eg. gbxml_str)
840
+ and each property should be put in curly brackets. Nested
841
+ properties can be specified by using "." to denote nesting levels
842
+ (eg. properties.energy.construction.display_name). Functions that
843
+ return string outputs can also be passed here as long as these
844
+ functions defaults specified for all arguments.
845
+ """
846
+ for face in self.faces:
847
+ face.rename_by_attribute(format_str)
848
+
849
+ def rename_apertures_by_attribute(
850
+ self,
851
+ format_str='{parent.parent.display_name} - {gbxml_type} - {cardinal_direction}'
852
+ ):
853
+ """Set the display name for all of this Room's apertures using a format string.
854
+
855
+ Args:
856
+ format_str: Text string for the pattern with which the apertures will be
857
+ renamed. Any property on the Aperture class may be used (eg. gbxml_str)
858
+ and each property should be put in curly brackets. Nested
859
+ properties can be specified by using "." to denote nesting levels
860
+ (eg. properties.energy.construction.display_name). Functions that
861
+ return string outputs can also be passed here as long as these
862
+ functions defaults specified for all arguments.
863
+ """
864
+ for ap in self.apertures:
865
+ ap.rename_by_attribute(format_str)
866
+
867
+ def rename_doors_by_attribute(
868
+ self,
869
+ format_str='{parent.parent.display_name} - {energyplus_type} - {cardinal_direction}'
870
+ ):
871
+ """Set the display name for all of this Room's doors using a format string.
872
+
873
+ Args:
874
+ format_str: Text string for the pattern with which the doors will be
875
+ renamed. Any property on the Door class may be used (eg. gbxml_str)
876
+ and each property should be put in curly brackets. Nested
877
+ properties can be specified by using "." to denote nesting levels
878
+ (eg. properties.energy.construction.display_name). Functions that
879
+ return string outputs can also be passed here as long as these
880
+ functions defaults specified for all arguments.
881
+ """
882
+ for dr in self.doors:
883
+ dr.rename_by_attribute(format_str)
884
+
777
885
  def remove_indoor_furniture(self):
778
886
  """Remove all indoor furniture assigned to this Room.
779
887
 
honeybee/shade.py CHANGED
@@ -86,16 +86,9 @@ class Shade(_Base):
86
86
  assert data['type'] == 'Shade', 'Expected Shade dictionary. ' \
87
87
  'Got {}.'.format(data['type'])
88
88
 
89
- # remove any invalid holes from the geometry
90
- geo_dict = data['geometry']
91
- if 'holes' in geo_dict and geo_dict['holes'] is not None:
92
- for i, hole_list in enumerate(geo_dict['holes']):
93
- if len(hole_list) < 3:
94
- geo_dict['holes'].pop(i)
95
-
96
89
  # serialize the dictionary to an object
97
- is_detached = data['is_detached'] if 'is_detached' in data else False
98
- shade = cls(data['identifier'], Face3D.from_dict(geo_dict), is_detached)
90
+ is_det = data['is_detached'] if 'is_detached' in data else False
91
+ shade = cls(data['identifier'], Face3D.from_dict(data['geometry']), is_det)
99
92
  if 'display_name' in data and data['display_name'] is not None:
100
93
  shade.display_name = data['display_name']
101
94
  if 'user_data' in data and data['user_data'] is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: honeybee-core
3
- Version: 1.62.12
3
+ Version: 1.64.0
4
4
  Summary: A library to create 3D building geometry for various types of environmental simulation.
5
5
  Home-page: https://github.com/ladybug-tools/honeybee-core
6
6
  Author: Ladybug Tools
@@ -4,24 +4,24 @@ honeybee/_base.py,sha256=11TweR_0YJFv5ma1ttN2bs6FW_RlC3hNTjhhjvqXDB0,13181
4
4
  honeybee/_basewithshade.py,sha256=SXCWZ-4do6_Z-bZpVFidUINbylVyjwpkY2TIJE28Vuw,12943
5
5
  honeybee/_lockable.py,sha256=jmTVsXEWmYyEHpL3WsQZ_ItGOVVlJht1z3NFn07DIp0,3707
6
6
  honeybee/altnumber.py,sha256=nVXIvji9YDpy_I37oG41-rkPDx5QLplW9B2RYRCNeoA,1002
7
- honeybee/aperture.py,sha256=MheeMEaIDtilVu37sCeov-Rw5N_cnd2dvH-zAvxns0g,45838
7
+ honeybee/aperture.py,sha256=Kegu9rPZtRhezwsJwqVz74jxepoTmGnzzLJ3D1xUfmc,45512
8
8
  honeybee/boundarycondition.py,sha256=ys3kgMqgeJDBMLpXwSgwNBwvKMRz-LbCtLiL9tiOXS0,13777
9
9
  honeybee/checkdup.py,sha256=8q5p4tD3S4dl69jcURQhppeajJ_pYXRBlA48zTOoAGI,8221
10
10
  honeybee/colorobj.py,sha256=jhJmTBcLayFKSniW5ASo2-XMKFsW-RBUJilc9v-33ik,13349
11
11
  honeybee/config.json,sha256=GAFduJOXTlNcPM4M0fMkSXom5Cb7XZAMYQis2WsNaq0,167
12
12
  honeybee/config.py,sha256=auH_ooOyqMKAeBZxUc2NGUehUcYg8SHya98b5hfNlvM,13836
13
13
  honeybee/dictutil.py,sha256=cOqkhgJSQ3MviIkqOPv8MRb-N-BeDv07cVcNg7w7nLY,1992
14
- honeybee/door.py,sha256=XWMb9CL5aZoIisVyzE29uF8k4usfNvl0A0FzrKxFVmc,31732
14
+ honeybee/door.py,sha256=HiMBdT07_KjaGdasT2MvUdDl6XjOM7lhr-DxEy_0oZE,31406
15
15
  honeybee/extensionutil.py,sha256=DDQYhM7tFD3avRSCOjwTzLqX9ldUxl5GzY79V3_sATE,9644
16
- honeybee/face.py,sha256=j8ud3d-8Z8RvDomeiyYTqalu5czgiCMb08Xue_tFRGo,112287
16
+ honeybee/face.py,sha256=BOL2tlFLErxY27euixVWFzL1z-xtABtzvMmLIpzMqcE,111961
17
17
  honeybee/facetype.py,sha256=vCtWZKHp21RH-Yzs8zsHJHuFhJvczNh0yFl8wDe_RWY,4489
18
18
  honeybee/logutil.py,sha256=2gn-6RcWqFLvwdFzBHPqUwFqTj_R3iwHKALrl-2eL7M,2564
19
- honeybee/model.py,sha256=WzquyG8denJ5pX-ZiAyXKp1cRXFuIFih-fT7BqiRWL4,181757
19
+ honeybee/model.py,sha256=RgD5JsxESt1JjmFanJZ4Vu5gQAHTXLMmtP5R0M9vL2Q,195385
20
20
  honeybee/orientation.py,sha256=GogGblASW9OU-fobfDaQ7w5yRbEAFdJJuHwg2fadhKI,5046
21
21
  honeybee/properties.py,sha256=ok976fbUdXzLDR2XiPNf8Q5ejfOM-PArqm5CVUbFiCc,34316
22
- honeybee/room.py,sha256=ec8DIebXfAc9A4f5oVcKCrF-ZR0loFvNgB1AZ7sXkw8,163195
22
+ honeybee/room.py,sha256=ZrjABLErBTdTHcDRvPdt4W8-0REyPjQLtnu0rnrML0U,168284
23
23
  honeybee/search.py,sha256=KOIeQjYdj0yhRWPmF5kiFiH8Ei0WbzuiU-capnwYVeM,5060
24
- honeybee/shade.py,sha256=HwLkOqvPpluLMXzicWXmKl4fMVJHmkqqehntT69e00Q,21026
24
+ honeybee/shade.py,sha256=2zuW1lR5wfe-UyAosuCX-3tII6CKONq7wt8Ac_jXU6c,20698
25
25
  honeybee/shademesh.py,sha256=oldugnwhu-ibX9f0hfxpO-DvgM8U7S-dYwUjBSVzo4g,13273
26
26
  honeybee/typing.py,sha256=E8-HrCB9cSoqhFR6zcFXhrAlQRAFw_sxHGdobB8Lre8,20051
27
27
  honeybee/units.py,sha256=_qG_G5b9hdqjpyVOpGdIYCB6k8VKYjcxSJn1St-7Xjc,3204
@@ -40,9 +40,9 @@ honeybee/writer/model.py,sha256=N7F_jksf-5TrdVecuxTaFWxnPVFLmQs7k8g27TsdB7Q,177
40
40
  honeybee/writer/room.py,sha256=kFghgStTU1SEJSLigXB0VjOWhZtgs4uXuAqdwd4yRQo,174
41
41
  honeybee/writer/shade.py,sha256=EpgX-vMc-s21TnMvNWvWTKyT8iAnxu1nFVXzjY1oyF8,177
42
42
  honeybee/writer/shademesh.py,sha256=Y41bLogJ7dwpvMe5cAWVRDRVqJEwo9e5hFJQjlt6UX8,189
43
- honeybee_core-1.62.12.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
44
- honeybee_core-1.62.12.dist-info/METADATA,sha256=iEGta_mCYJAxA0h2_6GCyt8pRkfzV99UE3vR10Nrvyc,3730
45
- honeybee_core-1.62.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
46
- honeybee_core-1.62.12.dist-info/entry_points.txt,sha256=r3YqOm40goBroH3ccUhpwQjvTwu10JWLd0HIRHI1J8E,47
47
- honeybee_core-1.62.12.dist-info/top_level.txt,sha256=8ve7puCRLUA9XDEGc1Mcs-UX9sFjpPV8MeTaIMwQ_Tg,9
48
- honeybee_core-1.62.12.dist-info/RECORD,,
43
+ honeybee_core-1.64.0.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
44
+ honeybee_core-1.64.0.dist-info/METADATA,sha256=RlL15eO292imJELxLIkC81hR94wrn3_qeGxIxqEGGiM,3729
45
+ honeybee_core-1.64.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
46
+ honeybee_core-1.64.0.dist-info/entry_points.txt,sha256=r3YqOm40goBroH3ccUhpwQjvTwu10JWLd0HIRHI1J8E,47
47
+ honeybee_core-1.64.0.dist-info/top_level.txt,sha256=8ve7puCRLUA9XDEGc1Mcs-UX9sFjpPV8MeTaIMwQ_Tg,9
48
+ honeybee_core-1.64.0.dist-info/RECORD,,