honeybee-core 1.64.12__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.
Files changed (48) hide show
  1. honeybee/__init__.py +23 -0
  2. honeybee/__main__.py +4 -0
  3. honeybee/_base.py +331 -0
  4. honeybee/_basewithshade.py +310 -0
  5. honeybee/_lockable.py +99 -0
  6. honeybee/altnumber.py +47 -0
  7. honeybee/aperture.py +997 -0
  8. honeybee/boundarycondition.py +358 -0
  9. honeybee/checkdup.py +173 -0
  10. honeybee/cli/__init__.py +118 -0
  11. honeybee/cli/compare.py +132 -0
  12. honeybee/cli/create.py +265 -0
  13. honeybee/cli/edit.py +559 -0
  14. honeybee/cli/lib.py +103 -0
  15. honeybee/cli/setconfig.py +43 -0
  16. honeybee/cli/validate.py +224 -0
  17. honeybee/colorobj.py +363 -0
  18. honeybee/config.json +5 -0
  19. honeybee/config.py +347 -0
  20. honeybee/dictutil.py +54 -0
  21. honeybee/door.py +746 -0
  22. honeybee/extensionutil.py +208 -0
  23. honeybee/face.py +2360 -0
  24. honeybee/facetype.py +153 -0
  25. honeybee/logutil.py +79 -0
  26. honeybee/model.py +4272 -0
  27. honeybee/orientation.py +132 -0
  28. honeybee/properties.py +845 -0
  29. honeybee/room.py +3485 -0
  30. honeybee/search.py +107 -0
  31. honeybee/shade.py +514 -0
  32. honeybee/shademesh.py +362 -0
  33. honeybee/typing.py +498 -0
  34. honeybee/units.py +88 -0
  35. honeybee/writer/__init__.py +7 -0
  36. honeybee/writer/aperture.py +6 -0
  37. honeybee/writer/door.py +6 -0
  38. honeybee/writer/face.py +6 -0
  39. honeybee/writer/model.py +6 -0
  40. honeybee/writer/room.py +6 -0
  41. honeybee/writer/shade.py +6 -0
  42. honeybee/writer/shademesh.py +6 -0
  43. honeybee_core-1.64.12.dist-info/METADATA +94 -0
  44. honeybee_core-1.64.12.dist-info/RECORD +48 -0
  45. honeybee_core-1.64.12.dist-info/WHEEL +5 -0
  46. honeybee_core-1.64.12.dist-info/entry_points.txt +2 -0
  47. honeybee_core-1.64.12.dist-info/licenses/LICENSE +661 -0
  48. honeybee_core-1.64.12.dist-info/top_level.txt +1 -0
honeybee/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+ """Honeybee core library."""
2
+ import importlib
3
+ import pkgutil
4
+ import sys
5
+
6
+ from honeybee.logutil import get_logger
7
+
8
+
9
+ logger = get_logger(__name__)
10
+
11
+ # find and import honeybee extensions
12
+ # this is a critical step to add additional functionalities to honeybee core library.
13
+ extensions = {}
14
+ for finder, name, ispkg in pkgutil.iter_modules():
15
+ if not name.startswith('honeybee_') or name.count('_') > 1:
16
+ continue
17
+ try:
18
+ extensions[name] = importlib.import_module(name)
19
+ except Exception:
20
+ if (sys.version_info >= (3, 0)):
21
+ logger.exception('Failed to import {0}!'.format(name))
22
+ else:
23
+ logger.info('Successfully imported Honeybee plugin: {}'.format(name))
honeybee/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from honeybee.cli import main
2
+
3
+ if __name__ == '__main__':
4
+ main()
honeybee/_base.py ADDED
@@ -0,0 +1,331 @@
1
+ # coding: utf-8
2
+ """Base class for all geometry objects."""
3
+ from ladybug_geometry.geometry3d.pointvector import Point3D
4
+
5
+ from .typing import valid_string
6
+
7
+
8
+ class _Base(object):
9
+ """A base class for all geometry objects.
10
+
11
+ Args:
12
+ identifier: Text string for a unique object ID. Must be < 100 characters and
13
+ not contain any spaces or special characters.
14
+
15
+ Properties:
16
+ * identifier
17
+ * display_name
18
+ * full_id
19
+ * user_data
20
+ """
21
+ __slots__ = ('_identifier', '_display_name', '_properties', '_user_data')
22
+
23
+ def __init__(self, identifier):
24
+ """Initialize base object."""
25
+ self.identifier = identifier
26
+ self._display_name = None
27
+ self._properties = None
28
+ self._user_data = None
29
+
30
+ @property
31
+ def identifier(self):
32
+ """Get or set a text string for the unique object identifier.
33
+
34
+ This identifier remains constant as the object is mutated, copied, and
35
+ serialized to different formats (eg. dict, idf, rad). As such, this
36
+ property is used to reference the object across a Model.
37
+ """
38
+ return self._identifier
39
+
40
+ @identifier.setter
41
+ def identifier(self, value):
42
+ self._identifier = valid_string(value, 'honeybee object identifier')
43
+
44
+ @property
45
+ def display_name(self):
46
+ """Get or set a string for the object name without any character restrictions.
47
+
48
+ If not set, this will be equal to the identifier.
49
+ """
50
+ if self._display_name is None:
51
+ return self._identifier
52
+ return self._display_name
53
+
54
+ @display_name.setter
55
+ def display_name(self, value):
56
+ if value is not None:
57
+ try:
58
+ value = str(value)
59
+ except UnicodeEncodeError: # Python 2 machine lacking the character set
60
+ pass # keep it as unicode
61
+ self._display_name = value
62
+
63
+ @property
64
+ def full_id(self):
65
+ """Get a string with both the object display_name and identifier.
66
+
67
+ This is formatted as display_name[identifier].
68
+
69
+ This is useful in error messages to give users an easy means of finding
70
+ invalid objects within models. If there is no display_name assigned,
71
+ only the identifier will be returned.
72
+ """
73
+ if self._display_name is None:
74
+ return self._identifier
75
+ else:
76
+ return '{}[{}]'.format(self._display_name, self._identifier)
77
+
78
+ @property
79
+ def properties(self):
80
+ """Get object properties, including Radiance, Energy and other properties."""
81
+ return self._properties
82
+
83
+ @property
84
+ def user_data(self):
85
+ """Get or set an optional dictionary for additional meta data for this object.
86
+
87
+ This will be None until it has been set. All keys and values of this
88
+ dictionary should be of a standard Python type to ensure correct
89
+ serialization of the object to/from JSON (eg. str, float, int, list, dict)
90
+ """
91
+ return self._user_data
92
+
93
+ @user_data.setter
94
+ def user_data(self, value):
95
+ if value is not None:
96
+ assert isinstance(value, dict), 'Expected dictionary for honeybee ' \
97
+ 'object user_data. Got {}.'.format(type(value))
98
+ self._user_data = value
99
+
100
+ def duplicate(self):
101
+ """Get a copy of this object."""
102
+ return self.__copy__()
103
+
104
+ def display_dict(self):
105
+ """Get a list of DisplayFace3D dictionaries for visualizing the object."""
106
+ return []
107
+
108
+ def _changed_dict(self, other_object, tolerance):
109
+ """Get a dictionary reporting changes between this object and another.
110
+
111
+ Args:
112
+ other_object: Another object of the same type to be compared to this one.
113
+ tolerance: The tolerance to be used in checking whether the geometry
114
+ has been changed.
115
+
116
+ Returns:
117
+ A dictionary with a report of differences. This will be None if there
118
+ are no differences detected between this object and the other object.
119
+ """
120
+ # check whether each type of property has changed
121
+ geo_changed = not other_object.is_geo_equivalent(self, tolerance)
122
+ meta_changed = other_object.properties.is_equivalent(self.properties)
123
+ if not geo_changed and all(meta_changed.values()):
124
+ return None
125
+ # establish the base dictionary
126
+ base_dict = {
127
+ 'type': 'ChangedObject',
128
+ 'element_type': self.__class__.__name__,
129
+ 'element_id': self.identifier,
130
+ 'element_name': self.display_name,
131
+ 'geometry_changed': geo_changed
132
+ }
133
+ # add booleans for whether metadata changed
134
+ for atr, equiv in meta_changed.items():
135
+ base_dict['{}_changed'.format(atr)] = not equiv
136
+ # add a representation of the geometry if it has changed
137
+ base_dict['geometry'] = other_object.display_dict()
138
+ if geo_changed:
139
+ base_dict['existing_geometry'] = self.display_dict()
140
+ return base_dict
141
+
142
+ def _base_report_dict(self, dict_type='AddedObject'):
143
+ """Get a dictionary reporting the object as an addition/deletion to/from a Model.
144
+ """
145
+ return {
146
+ 'type': dict_type,
147
+ 'element_type': self.__class__.__name__,
148
+ 'element_id': self.identifier,
149
+ 'element_name': self.display_name,
150
+ 'geometry': self.display_dict()
151
+ }
152
+
153
+ def _validation_message(
154
+ self, message, raise_exception=True, detailed=False,
155
+ code='000000', extension='Core', error_type='Unknown Error'):
156
+ """Handle a validation error message given various options.
157
+
158
+ Args:
159
+ message: Text for the error message.
160
+ raise_exception: Boolean to note whether an ValueError should be
161
+ raised with the message. (Default: True).
162
+ detailed: Boolean for whether the returned object is a detailed list of
163
+ dicts with error info or a string with a message. (Default: False).
164
+ code: Text for the error code. (Default: 000000).
165
+ extension: Text for the name of the Honeybee extension for which duplicate
166
+ identifiers are being evaluated. (Default: Core).
167
+ error_type: Text for the type of error. This should be directly linked
168
+ to the error code and should simply be a human-readable version of
169
+ the error code. (Default: Unknown Error).
170
+
171
+ Returns:
172
+ A string with the message or a list with a dictionary if detailed is True.
173
+ """
174
+ # first check whether an exception should be raised or the message returned
175
+ if raise_exception:
176
+ raise ValueError(message)
177
+ if not detailed:
178
+ return message
179
+ # if not, then assemble a dictionary with detailed error information
180
+ error_dict = {
181
+ 'type': 'ValidationError',
182
+ 'code': code,
183
+ 'error_type': error_type,
184
+ 'extension_type': extension,
185
+ 'element_type': self.__class__.__name__,
186
+ 'element_id': [self.identifier],
187
+ 'element_name': [self.display_name],
188
+ 'message': message
189
+ }
190
+ # add parents to the error dictionary if they exist
191
+ if getattr(self, '_parent', None) is not None:
192
+ parents = []
193
+ rel_obj = self
194
+ while getattr(rel_obj, '_parent', None) is not None:
195
+ rel_obj = getattr(rel_obj, '_parent')
196
+ par_dict = {
197
+ 'parent_type': rel_obj.__class__.__name__,
198
+ 'id': rel_obj.identifier,
199
+ 'name': rel_obj.display_name
200
+ }
201
+ parents.append(par_dict)
202
+ error_dict['parents'] = [parents]
203
+ return [error_dict]
204
+
205
+ def _top_parent(self):
206
+ """Get the highest parent object that this object is a part of."""
207
+ if getattr(self, '_parent', None) is not None:
208
+ rel_obj = self
209
+ while getattr(rel_obj, '_parent', None) is not None:
210
+ rel_obj = getattr(rel_obj, '_parent')
211
+ return rel_obj
212
+
213
+ @staticmethod
214
+ def _validation_message_child(
215
+ message, child_obj, detailed=False, code='000000', extension='Core',
216
+ error_type='Unknown Error'):
217
+ """Process a validation error message of a child object.
218
+
219
+ Args:
220
+ message: Text for the error message.
221
+ child_obj: The child object instance for which the error message is for.
222
+ detailed: Boolean for whether the returned object is a detailed list of
223
+ dicts with error info or a string with a message. (Default: False).
224
+ code: Text for the error code. (Default: 000000).
225
+ extension: Text for the name of the Honeybee extension for which duplicate
226
+ identifiers are being evaluated. (Default: Core).
227
+ error_type: Text for the type of error. This should be directly linked
228
+ to the error code and should simply be a human-readable version of
229
+ the error code. (Default: Unknown Error).
230
+
231
+ Returns:
232
+ A string with the message or a dictionary if detailed is True.
233
+ """
234
+ # first check whether an exception should be raised or the message returned
235
+ if not detailed:
236
+ return message
237
+ # if not, then assemble a dictionary with detailed error information
238
+ error_dict = {
239
+ 'type': 'ValidationError',
240
+ 'code': code,
241
+ 'error_type': error_type,
242
+ 'extension_type': extension,
243
+ 'element_type': child_obj.__class__.__name__,
244
+ 'element_id': [child_obj.identifier],
245
+ 'element_name': [child_obj.display_name],
246
+ 'message': message
247
+ }
248
+ # add parents to the error dictionary
249
+ parents = []
250
+ rel_obj = child_obj
251
+ while getattr(rel_obj, '_parent', None) is not None:
252
+ rel_obj = getattr(rel_obj, '_parent')
253
+ par_dict = {
254
+ 'parent_type': rel_obj.__class__.__name__,
255
+ 'id': rel_obj.identifier,
256
+ 'name': rel_obj.display_name
257
+ }
258
+ parents.append(par_dict)
259
+ error_dict['parents'] = [parents]
260
+ return error_dict
261
+
262
+ @staticmethod
263
+ def _from_dict_error_message(obj_dict, exception_obj):
264
+ """Give an error message when the object serialization from_dict fails.
265
+
266
+ This error message will include the identifier if it exists in the dict.
267
+
268
+ Args:
269
+ obj_dict: The objection dictionary that failed serialization.
270
+ exception_obj: The exception object to be included in the message.
271
+ """
272
+ obj_name = obj_dict['type'] if 'type' in obj_dict else 'Honeybee object'
273
+ full_id = ''
274
+ if 'identifier' in obj_dict and obj_dict['identifier'] is not None:
275
+ full_id = '{}[{}]'.format(obj_dict['display_name'], obj_dict['identifier']) \
276
+ if 'display_name' in obj_dict and obj_dict['display_name'] is not None \
277
+ else obj_dict['identifier']
278
+ msg = '{} "{}" is not valid and is not following honeybee-schema:\n{}'.format(
279
+ obj_name, full_id, exception_obj)
280
+ raise ValueError(msg)
281
+
282
+ @staticmethod
283
+ def _display_face(face3d, color):
284
+ """Create a DisplayFace3D dictionary from a Face3D and color."""
285
+ return {
286
+ 'type': 'DisplayFace3D',
287
+ 'geometry': face3d.to_dict(),
288
+ 'color': color.to_dict(),
289
+ 'display_mode': 'SurfaceWithEdges'
290
+ }
291
+
292
+ @staticmethod
293
+ def _calculate_min(geometry_objects):
294
+ """Calculate min Point3D around an array of geometry with min attributes."""
295
+ first_obj = geometry_objects[0]
296
+ min_pt = [first_obj.min.x, first_obj.min.y, first_obj.min.z]
297
+ for obj in geometry_objects[1:]:
298
+ if obj.min.x < min_pt[0]:
299
+ min_pt[0] = obj.min.x
300
+ if obj.min.y < min_pt[1]:
301
+ min_pt[1] = obj.min.y
302
+ if obj.min.z < min_pt[2]:
303
+ min_pt[2] = obj.min.z
304
+ return Point3D(*min_pt)
305
+
306
+ @staticmethod
307
+ def _calculate_max(geometry_objects):
308
+ """Calculate max Point3D around an array of geometry with max attributes."""
309
+ first_obj = geometry_objects[0]
310
+ max_pt = [first_obj.max.x, first_obj.max.y, first_obj.max.z]
311
+ for obj in geometry_objects[1:]:
312
+ if obj.max.x > max_pt[0]:
313
+ max_pt[0] = obj.max.x
314
+ if obj.max.y > max_pt[1]:
315
+ max_pt[1] = obj.max.y
316
+ if obj.max.z > max_pt[2]:
317
+ max_pt[2] = obj.max.z
318
+ return Point3D(*max_pt)
319
+
320
+ def __copy__(self):
321
+ new_obj = self.__class__(self.identifier)
322
+ new_obj._display_name = self._display_name
323
+ new_obj._user_data = None if self.user_data is None else self.user_data.copy()
324
+ return new_obj
325
+
326
+ def ToString(self):
327
+ """Overwrite .NET ToString."""
328
+ return self.__repr__()
329
+
330
+ def __repr__(self):
331
+ return 'Honeybee Base Object: %s' % self.display_name
@@ -0,0 +1,310 @@
1
+ # coding: utf-8
2
+ """Base class for all geometry objects that can have shades as children."""
3
+ from ._base import _Base
4
+ from .shade import Shade
5
+ from .typing import invalid_dict_error
6
+
7
+
8
+ class _BaseWithShade(_Base):
9
+ """A base class for all objects that can have Shades nested on them.
10
+
11
+ Args:
12
+ identifier: Text string for a unique object ID. Must be < 100 characters and
13
+ not contain any spaces or special characters.
14
+
15
+ Properties:
16
+ * identifier
17
+ * display_name
18
+ * geometry
19
+ * outdoor_shades
20
+ * indoor_shades
21
+ * shades
22
+ """
23
+ __slots__ = ('_outdoor_shades', '_indoor_shades')
24
+
25
+ def __init__(self, identifier):
26
+ """Initialize base with shade object."""
27
+ _Base.__init__(self, identifier) # process the identifier
28
+ self._outdoor_shades = []
29
+ self._indoor_shades = []
30
+
31
+ @property
32
+ def outdoor_shades(self):
33
+ """Get an array of all outdoor shades assigned to this object."""
34
+ return tuple(self._outdoor_shades)
35
+
36
+ @property
37
+ def indoor_shades(self):
38
+ """Get an array of all indoor shades assigned to this object."""
39
+ return tuple(self._indoor_shades)
40
+
41
+ @property
42
+ def shades(self):
43
+ """Get an array of all shades (indoor + outdoor) assigned to this object."""
44
+ return self._outdoor_shades + self._indoor_shades
45
+
46
+ def remove_shades(self):
47
+ """Remove all indoor and outdoor shades assigned to this object."""
48
+ self.remove_indoor_shades()
49
+ self.remove_outdoor_shades()
50
+
51
+ def remove_outdoor_shades(self):
52
+ """Remove all outdoor shades assigned to this object."""
53
+ for shade in self._outdoor_shades:
54
+ shade._parent = None
55
+ self._outdoor_shades = []
56
+
57
+ def remove_indoor_shades(self):
58
+ """Remove all indoor shades assigned to this object."""
59
+ for shade in self._indoor_shades:
60
+ shade._parent = None
61
+ shade._is_indoor = False
62
+ self._indoor_shades = []
63
+
64
+ def add_outdoor_shade(self, shade):
65
+ """Add a Shade object to the outdoors of this object.
66
+
67
+ Outdoor Shade objects can be used to represent balconies, outdoor furniture,
68
+ overhangs, light shelves, fins, the exterior part of mullions, etc.
69
+ For representing larger shade objects like trees or other buildings,
70
+ it may be more appropriate to add them to the Model as orphaned_shades
71
+ without a specific parent object.
72
+
73
+ Args:
74
+ shade: A Shade object to add to the outdoors of this object.
75
+ """
76
+ assert isinstance(shade, Shade), \
77
+ 'Expected Shade for outdoor_shade. Got {}.'.format(type(shade))
78
+ assert shade.parent is None, 'Shade cannot have more than one parent object.'
79
+ shade._parent = self
80
+ shade._is_detached = False
81
+ self._outdoor_shades.append(shade)
82
+
83
+ def add_indoor_shade(self, shade):
84
+ """Add a Shade object to be added to the indoors of this object.
85
+
86
+ Indoor Shade objects can be used to represent furniture, the interior
87
+ portion of light shelves, the interior part of mullions, etc.
88
+ For representing finely detailed objects like blinds or roller shades,
89
+ it may be more appropriate to model them as materials assigned to
90
+ Aperture properties (like Radiance materials or Energy constructions).
91
+
92
+ Args:
93
+ shade: A Shade object to add to the indoors of this object.
94
+ """
95
+ assert isinstance(shade, Shade), \
96
+ 'Expected Shade for indoor_shade. Got {}.'.format(type(shade))
97
+ assert shade.parent is None, 'Shade cannot have more than one parent object.'
98
+ shade._parent = self
99
+ shade._is_detached = False
100
+ shade._is_indoor = True
101
+ self._indoor_shades.append(shade)
102
+
103
+ def add_outdoor_shades(self, shades):
104
+ """Add a list of Shade objects to the outdoors of this object.
105
+
106
+ Args:
107
+ shades: A list of Shade objects to add to the outdoors of this object.
108
+ """
109
+ for shade in shades:
110
+ self.add_outdoor_shade(shade)
111
+
112
+ def add_indoor_shades(self, shades):
113
+ """Add a list of Shade objects to the indoors of this object.
114
+
115
+ Args:
116
+ shades: A list of Shade objects to add to the indoors of this object.
117
+ """
118
+ for shade in shades:
119
+ self.add_indoor_shade(shade)
120
+
121
+ def move_shades(self, moving_vec):
122
+ """Move all indoor and outdoor shades assigned to this object along a vector.
123
+
124
+ Args:
125
+ moving_vec: A ladybug_geometry Vector3D with the direction and distance
126
+ to move the shades.
127
+ """
128
+ for oshd in self._outdoor_shades:
129
+ oshd.move(moving_vec)
130
+ for ishd in self._indoor_shades:
131
+ ishd.move(moving_vec)
132
+
133
+ def rotate_shades(self, axis, angle, origin):
134
+ """Rotate all indoor and outdoor shades assigned to this object.
135
+
136
+ Args:
137
+ axis: A ladybug_geometry Vector3D axis representing the axis of rotation.
138
+ angle: An angle for rotation in degrees.
139
+ origin: A ladybug_geometry Point3D for the origin around which the
140
+ object will be rotated.
141
+ """
142
+ for oshd in self._outdoor_shades:
143
+ oshd.rotate(axis, angle, origin)
144
+ for ishd in self._indoor_shades:
145
+ ishd.rotate(axis, angle, origin)
146
+
147
+ def rotate_xy_shades(self, angle, origin):
148
+ """Rotate all indoor and outdoor shades counterclockwise in the world XY plane.
149
+
150
+ Args:
151
+ angle: An angle in degrees.
152
+ origin: A ladybug_geometry Point3D for the origin around which the
153
+ object will be rotated.
154
+ """
155
+ for oshd in self._outdoor_shades:
156
+ oshd.rotate_xy(angle, origin)
157
+ for ishd in self._indoor_shades:
158
+ ishd.rotate_xy(angle, origin)
159
+
160
+ def reflect_shades(self, plane):
161
+ """Reflect all indoor and outdoor shades assigned to this object across a plane.
162
+
163
+ Args:
164
+ plane: A ladybug_geometry Plane across which the object will
165
+ be reflected.
166
+ """
167
+ for oshd in self._outdoor_shades:
168
+ oshd.reflect(plane)
169
+ for ishd in self._indoor_shades:
170
+ ishd.reflect(plane)
171
+
172
+ def scale_shades(self, factor, origin=None):
173
+ """Scale all indoor and outdoor shades assigned to this object by a factor.
174
+
175
+ Args:
176
+ factor: A number representing how much the object should be scaled.
177
+ origin: A ladybug_geometry Point3D representing the origin from which
178
+ to scale. If None, it will be scaled from the World origin (0, 0, 0).
179
+ """
180
+ for oshd in self._outdoor_shades:
181
+ oshd.scale(factor, origin)
182
+ for ishd in self._indoor_shades:
183
+ ishd.scale(factor, origin)
184
+
185
+ def _add_prefix_shades(self, prefix):
186
+ """Change the name of all child shades by inserting a prefix.
187
+
188
+ Args:
189
+ prefix: Text that will be inserted at the start of this shades' name
190
+ and display_name.
191
+ """
192
+ for shade in self._outdoor_shades:
193
+ shade.add_prefix(prefix)
194
+ for shade in self._indoor_shades:
195
+ shade.add_prefix(prefix)
196
+
197
+ def _check_planar_shades(self, tolerance, detailed=False):
198
+ """Check that all of the child shades are planar."""
199
+ msgs = []
200
+ for oshd in self._outdoor_shades:
201
+ msgs.append(oshd.check_planar(tolerance, False, detailed))
202
+ for ishd in self._indoor_shades:
203
+ msgs.append(ishd.check_planar(tolerance, False, detailed))
204
+ flat_msgs = [m for m in msgs if m]
205
+ return flat_msgs if detailed else '\n'.join(flat_msgs)
206
+
207
+ def _check_self_intersecting_shades(self, tolerance, detailed=False):
208
+ """Check that no edges of the indoor or outdoor shades self-intersect."""
209
+ msgs = []
210
+ for oshd in self._outdoor_shades:
211
+ msgs.append(oshd.check_self_intersecting(tolerance, False, detailed))
212
+ for ishd in self._indoor_shades:
213
+ msgs.append(ishd.check_self_intersecting(tolerance, False, detailed))
214
+ flat_msgs = [m for m in msgs if m]
215
+ return flat_msgs if detailed else '\n'.join(flat_msgs)
216
+
217
+ def _add_shades_to_dict(
218
+ self, base, abridged=False, included_prop=None, include_plane=True):
219
+ """Method used to add child shades to the parent base dictionary.
220
+
221
+ Args:
222
+ base: The base object dictionary to which the child shades will be added.
223
+ abridged: Boolean to note whether the extension properties of the
224
+ object should be included in detail (False) or just referenced by
225
+ identifier (True). (Default: False).
226
+ included_prop: List of properties to filter keys that must be included in
227
+ output dictionary. For example ['energy'] will include 'energy' key if
228
+ available in properties to_dict. By default all the keys will be
229
+ included. To exclude all the keys from extensions use an empty list.
230
+ include_plane: Boolean to note wether the plane of the Face3D should be
231
+ included in the output. This can preserve the orientation of the
232
+ X/Y axes of the plane but is not required and can be removed to
233
+ keep the dictionary smaller. (Default: True).
234
+ """
235
+ if self._outdoor_shades != []:
236
+ base['outdoor_shades'] = [shd.to_dict(abridged, included_prop, include_plane)
237
+ for shd in self._outdoor_shades]
238
+ if self._indoor_shades != []:
239
+ base['indoor_shades'] = [shd.to_dict(abridged, included_prop, include_plane)
240
+ for shd in self._indoor_shades]
241
+
242
+ def _recover_shades_from_dict(self, data):
243
+ """Method used to recover shades from a dictionary.
244
+
245
+ Args:
246
+ data: The dictionary representation of this object to which shades will
247
+ be added from the dictionary.
248
+ """
249
+ if 'outdoor_shades' in data and data['outdoor_shades'] is not None:
250
+ for sh in data['outdoor_shades']:
251
+ try:
252
+ oshd = Shade.from_dict(sh)
253
+ oshd._parent = self
254
+ self._outdoor_shades.append(oshd)
255
+ except Exception as e:
256
+ invalid_dict_error(sh, e)
257
+ if 'indoor_shades' in data and data['indoor_shades'] is not None:
258
+ for sh in data['indoor_shades']:
259
+ try:
260
+ ishd = Shade.from_dict(sh)
261
+ ishd._parent = self
262
+ ishd._is_indoor = True
263
+ self._indoor_shades.append(ishd)
264
+ except Exception as e:
265
+ invalid_dict_error(sh, e)
266
+
267
+ def _duplicate_child_shades(self, new_object):
268
+ """Add duplicated child shades to a duplicated new_object."""
269
+ new_object._outdoor_shades = [oshd.duplicate() for oshd in self._outdoor_shades]
270
+ new_object._indoor_shades = [ishd.duplicate() for ishd in self._indoor_shades]
271
+ for oshd in new_object._outdoor_shades:
272
+ oshd._parent = new_object
273
+ for ishd in new_object._indoor_shades:
274
+ ishd._parent = new_object
275
+ ishd._is_indoor = True
276
+
277
+ def _min_with_shades(self, geometry):
278
+ """Calculate min Point3D around this object's geometry and its shades."""
279
+ all_geo = self._outdoor_shades + self._indoor_shades
280
+ all_geo.append(geometry)
281
+ return self._calculate_min(all_geo)
282
+
283
+ def _max_with_shades(self, geometry):
284
+ """Calculate max Point3D around this object's geometry and its shades."""
285
+ all_geo = self._outdoor_shades + self._indoor_shades
286
+ all_geo.append(geometry)
287
+ return self._calculate_max(all_geo)
288
+
289
+ def _are_shades_equivalent(self, other, tolerance=0.01):
290
+ """Get a boolean for whether this object's shades are equivalent to another.
291
+
292
+ Args:
293
+ other: Another object for which shade equivalency will be tested.
294
+ tolerance: The minimum difference between the coordinate values of two
295
+ vertices at which they can be considered geometrically equivalent.
296
+
297
+ Returns:
298
+ True if shades are equivalent. False if shades are not equivalent.
299
+ """
300
+ if len(self._outdoor_shades) != len(other._outdoor_shades):
301
+ return False
302
+ for f1, f2 in zip(self._outdoor_shades, other._outdoor_shades):
303
+ if not f1.is_geo_equivalent(f2, tolerance):
304
+ return False
305
+ if len(self._indoor_shades) != len(other._indoor_shades):
306
+ return False
307
+ for f1, f2 in zip(self._indoor_shades, other._indoor_shades):
308
+ if not f1.is_geo_equivalent(f2, tolerance):
309
+ return False
310
+ return True