honeybee-radiance 1.66.190__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.

Potentially problematic release.


This version of honeybee-radiance might be problematic. Click here for more details.

Files changed (152) hide show
  1. honeybee_radiance/__init__.py +11 -0
  2. honeybee_radiance/__main__.py +4 -0
  3. honeybee_radiance/_extend_honeybee.py +93 -0
  4. honeybee_radiance/cli/__init__.py +88 -0
  5. honeybee_radiance/cli/dc.py +400 -0
  6. honeybee_radiance/cli/edit.py +529 -0
  7. honeybee_radiance/cli/glare.py +118 -0
  8. honeybee_radiance/cli/grid.py +859 -0
  9. honeybee_radiance/cli/lib.py +458 -0
  10. honeybee_radiance/cli/modifier.py +133 -0
  11. honeybee_radiance/cli/mtx.py +226 -0
  12. honeybee_radiance/cli/multiphase.py +1034 -0
  13. honeybee_radiance/cli/octree.py +640 -0
  14. honeybee_radiance/cli/postprocess.py +1186 -0
  15. honeybee_radiance/cli/raytrace.py +219 -0
  16. honeybee_radiance/cli/rpict.py +125 -0
  17. honeybee_radiance/cli/schedule.py +56 -0
  18. honeybee_radiance/cli/setconfig.py +63 -0
  19. honeybee_radiance/cli/sky.py +545 -0
  20. honeybee_radiance/cli/study.py +66 -0
  21. honeybee_radiance/cli/sunpath.py +331 -0
  22. honeybee_radiance/cli/threephase.py +255 -0
  23. honeybee_radiance/cli/translate.py +400 -0
  24. honeybee_radiance/cli/util.py +121 -0
  25. honeybee_radiance/cli/view.py +261 -0
  26. honeybee_radiance/cli/viewfactor.py +347 -0
  27. honeybee_radiance/config.json +6 -0
  28. honeybee_radiance/config.py +427 -0
  29. honeybee_radiance/dictutil.py +50 -0
  30. honeybee_radiance/dynamic/__init__.py +5 -0
  31. honeybee_radiance/dynamic/group.py +479 -0
  32. honeybee_radiance/dynamic/multiphase.py +557 -0
  33. honeybee_radiance/dynamic/state.py +718 -0
  34. honeybee_radiance/dynamic/stategeo.py +352 -0
  35. honeybee_radiance/geometry/__init__.py +13 -0
  36. honeybee_radiance/geometry/bubble.py +42 -0
  37. honeybee_radiance/geometry/cone.py +215 -0
  38. honeybee_radiance/geometry/cup.py +54 -0
  39. honeybee_radiance/geometry/cylinder.py +197 -0
  40. honeybee_radiance/geometry/geometrybase.py +37 -0
  41. honeybee_radiance/geometry/instance.py +40 -0
  42. honeybee_radiance/geometry/mesh.py +38 -0
  43. honeybee_radiance/geometry/polygon.py +174 -0
  44. honeybee_radiance/geometry/ring.py +214 -0
  45. honeybee_radiance/geometry/source.py +182 -0
  46. honeybee_radiance/geometry/sphere.py +178 -0
  47. honeybee_radiance/geometry/tube.py +46 -0
  48. honeybee_radiance/lib/__init__.py +1 -0
  49. honeybee_radiance/lib/_loadmodifiers.py +72 -0
  50. honeybee_radiance/lib/_loadmodifiersets.py +69 -0
  51. honeybee_radiance/lib/modifiers.py +58 -0
  52. honeybee_radiance/lib/modifiersets.py +63 -0
  53. honeybee_radiance/lightpath.py +204 -0
  54. honeybee_radiance/lightsource/__init__.py +1 -0
  55. honeybee_radiance/lightsource/_gendaylit.py +479 -0
  56. honeybee_radiance/lightsource/dictutil.py +49 -0
  57. honeybee_radiance/lightsource/ground.py +160 -0
  58. honeybee_radiance/lightsource/sky/__init__.py +7 -0
  59. honeybee_radiance/lightsource/sky/_skybase.py +177 -0
  60. honeybee_radiance/lightsource/sky/certainirradiance.py +232 -0
  61. honeybee_radiance/lightsource/sky/cie.py +378 -0
  62. honeybee_radiance/lightsource/sky/climatebased.py +501 -0
  63. honeybee_radiance/lightsource/sky/hemisphere.py +160 -0
  64. honeybee_radiance/lightsource/sky/skydome.py +113 -0
  65. honeybee_radiance/lightsource/sky/skymatrix.py +163 -0
  66. honeybee_radiance/lightsource/sky/strutil.py +34 -0
  67. honeybee_radiance/lightsource/sky/sunmatrix.py +212 -0
  68. honeybee_radiance/lightsource/sunpath.py +247 -0
  69. honeybee_radiance/modifier/__init__.py +3 -0
  70. honeybee_radiance/modifier/material/__init__.py +30 -0
  71. honeybee_radiance/modifier/material/absdf.py +477 -0
  72. honeybee_radiance/modifier/material/antimatter.py +54 -0
  73. honeybee_radiance/modifier/material/ashik2.py +51 -0
  74. honeybee_radiance/modifier/material/brtdfunc.py +81 -0
  75. honeybee_radiance/modifier/material/bsdf.py +292 -0
  76. honeybee_radiance/modifier/material/dielectric.py +53 -0
  77. honeybee_radiance/modifier/material/glass.py +431 -0
  78. honeybee_radiance/modifier/material/glow.py +246 -0
  79. honeybee_radiance/modifier/material/illum.py +51 -0
  80. honeybee_radiance/modifier/material/interface.py +49 -0
  81. honeybee_radiance/modifier/material/light.py +206 -0
  82. honeybee_radiance/modifier/material/materialbase.py +36 -0
  83. honeybee_radiance/modifier/material/metal.py +167 -0
  84. honeybee_radiance/modifier/material/metal2.py +41 -0
  85. honeybee_radiance/modifier/material/metdata.py +41 -0
  86. honeybee_radiance/modifier/material/metfunc.py +41 -0
  87. honeybee_radiance/modifier/material/mirror.py +340 -0
  88. honeybee_radiance/modifier/material/mist.py +86 -0
  89. honeybee_radiance/modifier/material/plasdata.py +58 -0
  90. honeybee_radiance/modifier/material/plasfunc.py +59 -0
  91. honeybee_radiance/modifier/material/plastic.py +354 -0
  92. honeybee_radiance/modifier/material/plastic2.py +58 -0
  93. honeybee_radiance/modifier/material/prism1.py +57 -0
  94. honeybee_radiance/modifier/material/prism2.py +48 -0
  95. honeybee_radiance/modifier/material/spotlight.py +50 -0
  96. honeybee_radiance/modifier/material/trans.py +518 -0
  97. honeybee_radiance/modifier/material/trans2.py +49 -0
  98. honeybee_radiance/modifier/material/transdata.py +50 -0
  99. honeybee_radiance/modifier/material/transfunc.py +53 -0
  100. honeybee_radiance/modifier/mixture/__init__.py +6 -0
  101. honeybee_radiance/modifier/mixture/mixdata.py +49 -0
  102. honeybee_radiance/modifier/mixture/mixfunc.py +54 -0
  103. honeybee_radiance/modifier/mixture/mixpict.py +52 -0
  104. honeybee_radiance/modifier/mixture/mixtext.py +66 -0
  105. honeybee_radiance/modifier/mixture/mixturebase.py +28 -0
  106. honeybee_radiance/modifier/modifierbase.py +40 -0
  107. honeybee_radiance/modifier/pattern/__init__.py +9 -0
  108. honeybee_radiance/modifier/pattern/brightdata.py +49 -0
  109. honeybee_radiance/modifier/pattern/brightfunc.py +47 -0
  110. honeybee_radiance/modifier/pattern/brighttext.py +81 -0
  111. honeybee_radiance/modifier/pattern/colordata.py +56 -0
  112. honeybee_radiance/modifier/pattern/colorfunc.py +47 -0
  113. honeybee_radiance/modifier/pattern/colorpict.py +54 -0
  114. honeybee_radiance/modifier/pattern/colortext.py +73 -0
  115. honeybee_radiance/modifier/pattern/patternbase.py +34 -0
  116. honeybee_radiance/modifier/texture/__init__.py +4 -0
  117. honeybee_radiance/modifier/texture/texdata.py +29 -0
  118. honeybee_radiance/modifier/texture/texfunc.py +26 -0
  119. honeybee_radiance/modifier/texture/texturebase.py +27 -0
  120. honeybee_radiance/modifierset.py +1091 -0
  121. honeybee_radiance/mutil.py +60 -0
  122. honeybee_radiance/postprocess/__init__.py +1 -0
  123. honeybee_radiance/postprocess/annual.py +108 -0
  124. honeybee_radiance/postprocess/annualdaylight.py +425 -0
  125. honeybee_radiance/postprocess/annualglare.py +201 -0
  126. honeybee_radiance/postprocess/annualirradiance.py +187 -0
  127. honeybee_radiance/postprocess/electriclight.py +119 -0
  128. honeybee_radiance/postprocess/en17037.py +261 -0
  129. honeybee_radiance/postprocess/leed.py +304 -0
  130. honeybee_radiance/postprocess/solartracking.py +90 -0
  131. honeybee_radiance/primitive.py +554 -0
  132. honeybee_radiance/properties/__init__.py +1 -0
  133. honeybee_radiance/properties/_base.py +390 -0
  134. honeybee_radiance/properties/aperture.py +197 -0
  135. honeybee_radiance/properties/door.py +198 -0
  136. honeybee_radiance/properties/face.py +123 -0
  137. honeybee_radiance/properties/model.py +1291 -0
  138. honeybee_radiance/properties/room.py +490 -0
  139. honeybee_radiance/properties/shade.py +186 -0
  140. honeybee_radiance/properties/shademesh.py +116 -0
  141. honeybee_radiance/putil.py +44 -0
  142. honeybee_radiance/reader.py +214 -0
  143. honeybee_radiance/sensor.py +166 -0
  144. honeybee_radiance/sensorgrid.py +1008 -0
  145. honeybee_radiance/view.py +1101 -0
  146. honeybee_radiance/writer.py +951 -0
  147. honeybee_radiance-1.66.190.dist-info/METADATA +89 -0
  148. honeybee_radiance-1.66.190.dist-info/RECORD +152 -0
  149. honeybee_radiance-1.66.190.dist-info/WHEEL +5 -0
  150. honeybee_radiance-1.66.190.dist-info/entry_points.txt +2 -0
  151. honeybee_radiance-1.66.190.dist-info/licenses/LICENSE +661 -0
  152. honeybee_radiance-1.66.190.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1008 @@
1
+ """A grid of sensors."""
2
+ from __future__ import division
3
+
4
+ from .sensor import Sensor
5
+ from .lightpath import light_path_from_room
6
+
7
+ from honeybee.facetype import AirBoundary
8
+ import honeybee.typing as typing
9
+ import ladybug.futil as futil
10
+ from ladybug_geometry.geometry3d.pointvector import Point3D, Vector3D
11
+ from ladybug_geometry.geometry3d.face import Face3D
12
+ from ladybug_geometry.geometry3d.mesh import Mesh3D
13
+
14
+ import os
15
+ import json
16
+ import math
17
+ try:
18
+ from itertools import izip as zip
19
+ except ImportError: # python 3
20
+ pass
21
+
22
+
23
+ class SensorGrid(object):
24
+ """A grid of sensors.
25
+
26
+ Args:
27
+ identifier: Text string for a unique SensorGrid ID. Must not contain spaces
28
+ or special characters. This will be used to identify the object in the
29
+ exported Radiance files.
30
+ sensors: A collection of Sensors.
31
+
32
+ Properties:
33
+ * identifier
34
+ * display_name
35
+ * sensors
36
+ * positions
37
+ * directions
38
+ * room_identifier
39
+ * light_path
40
+ * mesh
41
+ * base_geometry
42
+ * group_identifier
43
+ * full_identifier
44
+ """
45
+
46
+ __slots__ = ('_identifier', '_display_name', '_sensors', '_room_identifier',
47
+ '_light_path', '_mesh', '_base_geometry', '_group_identifier')
48
+
49
+ def __init__(self, identifier, sensors):
50
+ """Initialize a SensorGrid."""
51
+ self.identifier = identifier
52
+ self._display_name = None
53
+ self.sensors = sensors
54
+ self._room_identifier = None
55
+ self._group_identifier = None
56
+ self._light_path = None
57
+ self._mesh = None
58
+ self._base_geometry = None
59
+
60
+ @classmethod
61
+ def from_dict(cls, ag_dict):
62
+ """Create a sensor grid from a dictionary in the following format.
63
+
64
+ .. code-block:: python
65
+
66
+ {
67
+ "type": "SensorGrid",
68
+ "identifier": str, # SensorGrid identifier
69
+ "display_name": str, # SensorGrid display name
70
+ "sensors": [], # list of Sensor dictionaries
71
+ 'room_identifier': str, # optional room identifier
72
+ 'group_identifier': str, # optional group identifier
73
+ 'light_path': [] # optional list of lists for light path
74
+ }
75
+ """
76
+ assert ag_dict['type'] == 'SensorGrid', \
77
+ 'Expected SensorGrid dictionary. Got {}.'.format(ag_dict['type'])
78
+ sensors = (Sensor.from_dict(sensor) for sensor in ag_dict['sensors'])
79
+ new_obj = cls(identifier=ag_dict["identifier"], sensors=sensors)
80
+ if 'display_name' in ag_dict and ag_dict['display_name'] is not None:
81
+ new_obj.display_name = ag_dict['display_name']
82
+ if 'room_identifier' in ag_dict and ag_dict['room_identifier'] is not None:
83
+ new_obj.room_identifier = ag_dict['room_identifier']
84
+ if 'group_identifier' in ag_dict and ag_dict['group_identifier'] is not None:
85
+ new_obj.group_identifier = ag_dict['group_identifier']
86
+ if 'light_path' in ag_dict and ag_dict['light_path'] is not None:
87
+ new_obj.light_path = ag_dict['light_path']
88
+ if 'mesh' in ag_dict and ag_dict['mesh'] is not None:
89
+ new_obj.mesh = Mesh3D.from_dict(ag_dict['mesh'])
90
+ if 'base_geometry' in ag_dict and ag_dict['base_geometry'] is not None:
91
+ new_obj.base_geometry = \
92
+ tuple(Face3D.from_dict(face) for face in ag_dict['base_geometry'])
93
+ return new_obj
94
+
95
+ @classmethod
96
+ def from_planar_positions(cls, identifier, positions, plane_normal):
97
+ """Create a sensor grid from a collection of positions with the same direction.
98
+
99
+ Args:
100
+ identifier: Text string for a unique SensorGrid ID. Must not contain spaces
101
+ or special characters. This will be used to identify the object across
102
+ a model and in the exported Radiance files.
103
+ positions: A list of (x, y ,z) tuples for position of sensors.
104
+ plane_normal: (x, y, z) tuples for direction of sensors.
105
+ """
106
+ sg = (Sensor(pt, plane_normal) for pt in positions)
107
+ return cls(identifier, sg)
108
+
109
+ @classmethod
110
+ def from_position_and_direction(cls, identifier, positions, directions):
111
+ """Create a sensor grid from a collection of positions and directions.
112
+
113
+ The length of positions and directions should be the same. In case the lists have
114
+ different lengths the shorter list will be used as the reference.
115
+
116
+ Args:
117
+ identifier: Text string for a unique SensorGrid ID. Must not contain spaces
118
+ or special characters. This will be used to identify the object across
119
+ a model and in the exported Radiance files.
120
+ positions: A list of (x, y ,z) tuples for position of sensors.
121
+ directions: A list of (x, y, z) tuples for direction of sensors.
122
+ """
123
+ sg = tuple(Sensor(pt, v) for pt, v in zip(positions, directions))
124
+ return cls(identifier, sg)
125
+
126
+ @classmethod
127
+ def from_mesh3d(cls, identifier, mesh):
128
+ """Create a sensor grid from a ladybug_geometry Mesh3D.
129
+
130
+ The centroids of the mesh faces will be used to create the sensor positions
131
+ and the normals of the faces will set the directions. The mesh will be
132
+ assigned to the resulting SensorGrid's mesh property.
133
+
134
+ Args:
135
+ identifier: Text string for a unique SensorGrid ID. Must not contain spaces
136
+ or special characters. This will be used to identify the object across
137
+ a model and in the exported Radiance files.
138
+ mesh: A ladybug_geometry Mesh3D.
139
+ """
140
+ assert isinstance(mesh, Mesh3D), 'Expected ladybug_geometry Mesh3D for ' \
141
+ 'SensorGrid.from_mesh3d. Got {}.'.format(type(mesh))
142
+ positions = [(pt.x, pt.y, pt.z) for pt in mesh.face_centroids]
143
+ directions = [(vec.x, vec.y, vec.z) for vec in mesh.face_normals]
144
+ s_grid = cls.from_position_and_direction(identifier, positions, directions)
145
+ s_grid.mesh = mesh
146
+ return s_grid
147
+
148
+ @classmethod
149
+ def from_face3d(cls, identifier, faces, x_dim, y_dim=None, offset=0, flip=False):
150
+ """Create a sensor grid from an array of ladybug_geometry Face3D.
151
+
152
+ The Face3D will be converted into a gridded mesh using the input x_dim
153
+ and y_dim. The centroids of the mesh faces will be used to create the
154
+ sensor positions and the normals of the faces will set the directions.
155
+ The mesh will be assigned to the resulting SensorGrid's mesh property
156
+ and the Face3Ds assigned to the base_geometry.
157
+
158
+ Args:
159
+ identifier: Text string for a unique SensorGrid ID. Must not contain spaces
160
+ or special characters. This will be used to identify the object across
161
+ a model and in the exported Radiance files.
162
+ faces: An array of ladybug_geometry Face3Ds from which a SensorGrid will
163
+ be generated.
164
+ x_dim: The x dimension of the grid cells as a number.
165
+ y_dim: The y dimension of the grid cells as a number. Default is None,
166
+ which will assume the same cell dimension for y as is set for x.
167
+ offset: A number for how far to offset the grid from the base face.
168
+ flip: Set to True to have the mesh normals reversed from the direction of
169
+ this face and to have the offset input move the mesh in the opposite
170
+ direction from this face normal. Defaults to False, which means the
171
+ normal direction of the face will be used as the direction of the
172
+ sensor grids.
173
+ """
174
+ meshes = []
175
+ for face in faces:
176
+ try:
177
+ meshes.append(face.mesh_grid(x_dim, y_dim, offset, flip))
178
+ except AssertionError: # tiny geometry not compatible with quad faces
179
+ continue
180
+ assert len(meshes) > 0, 'None of the Face3Ds input to SensorGrid.from_face3d ' \
181
+ 'can produce a quad grid at the specified grid dimensions.'
182
+ if len(meshes) == 1:
183
+ s_grid = cls.from_mesh3d(identifier, meshes[0])
184
+ elif len(meshes) > 1:
185
+ s_grid = cls.from_mesh3d(identifier, Mesh3D.join_meshes(meshes))
186
+ s_grid.base_geometry = faces
187
+ return s_grid
188
+
189
+ @classmethod
190
+ def from_positions_radial(
191
+ cls, identifier, positions, dir_count=8, start_vector=Vector3D(0, -1, 0),
192
+ mesh_radius=0):
193
+ """Create a sensor grid from radial directions around sensor positions.
194
+
195
+ This type of sensor grid is particularly helpful for studies of multiple view
196
+ directions, such as imageless glare studies.
197
+
198
+ Args:
199
+ identifier: Text string for a unique SensorGrid ID. Must not contain spaces
200
+ or special characters. This will be used to identify the object across
201
+ a model and in the exported Radiance files.
202
+ positions: A list of (x, y ,z) tuples for position of sensors.
203
+ dir_count: A positive integer for the number of radial directions
204
+ to be generated around each position. (Default: 8).
205
+ start_vector: A Vector3D to set the start direction of the generated
206
+ directions. This can be used to orient the resulting sensors to
207
+ specific parts of the scene. It can also change the elevation of the
208
+ resulting directions since this start vector will always be rotated in
209
+ the XY plane to generate the resulting directions. (Default: (0, -1, 0)).
210
+ mesh_radius: An optional number that can be used to generate a Mesh3D
211
+ that is aligned with the resulting sensors and will automatically
212
+ be assigned to the grid's mesh property. Such meshes will resemble
213
+ a circle around each sensor with the specified radius and will
214
+ contain triangular faces that can be colored with simulation results.
215
+ If zero, no mesh will be generated for the sensor grid. (Default: 0).
216
+ """
217
+ # set up the vectors to generate the rays
218
+ inc_ang = (math.pi * 2) / dir_count
219
+ vw_vecs = [start_vector.rotate_xy(i * inc_ang) for i in range(dir_count)]
220
+ vw_vecs = [(round(v.x, 5), round(v.y, 5), round(v.z, 3)) for v in vw_vecs]
221
+ # set up the sensor grid object
222
+ sensors = tuple(Sensor(pt, v) for pt in positions for v in vw_vecs)
223
+ sg = cls(identifier, sensors)
224
+ # generate the mesh if it was requested
225
+ if mesh_radius > 0:
226
+ sg.mesh = cls.radial_positions_mesh(
227
+ positions, dir_count, start_vector, mesh_radius)
228
+ return sg
229
+
230
+ @classmethod
231
+ def from_mesh3d_radial(
232
+ cls, identifier, mesh, dir_count=8, start_vector=Vector3D(0, -1, 0),
233
+ mesh_radius=0):
234
+ """Create a sensor grid from radial directions around centroids of a Mesh3D.
235
+
236
+ This type of sensor grid is particularly helpful for studies of multiple view
237
+ directions, such as imageless glare studies.
238
+
239
+ Args:
240
+ identifier: Text string for a unique SensorGrid ID. Must not contain spaces
241
+ or special characters. This will be used to identify the object across
242
+ a model and in the exported Radiance files.
243
+ mesh: A ladybug_geometry Mesh3D from which the sensor grid will be generated.
244
+ dir_count: A positive integer for the number of radial directions
245
+ to be generated around each position. (Default: 8).
246
+ start_vector: A Vector3D to set the start direction of the generated
247
+ directions. This can be used to orient the resulting sensors to
248
+ specific parts of the scene. It can also change the elevation of
249
+ the resulting directions since this start vector will always be
250
+ rotated in the XY plane to generate the resulting directions.
251
+ mesh_radius: An optional number that can be used to generate a Mesh3D
252
+ that is aligned with the resulting sensors and will automatically
253
+ be assigned to the grid's mesh property. Such meshes will resemble
254
+ a circle around each sensor with the specified radius and will
255
+ contain triangular faces that can be colored with simulation results.
256
+ If zero, no mesh will be generated for the sensor grid. (Default: 0).
257
+ """
258
+ assert isinstance(mesh, Mesh3D), 'Expected ladybug_geometry Mesh3D for ' \
259
+ 'SensorGrid.from_mesh3d. Got {}.'.format(type(mesh))
260
+ positions = [(pt.x, pt.y, pt.z) for pt in mesh.face_centroids]
261
+ return cls.from_positions_radial(
262
+ identifier, positions, dir_count, start_vector, mesh_radius)
263
+
264
+ @classmethod
265
+ def from_file(cls, file_path, start_line=None, end_line=None, identifier=None):
266
+ """Create a sensor grid from a sensor (.pts) file.
267
+
268
+ The sensors must be structured as
269
+
270
+ x1, y1, z1, dx1, dy1, dz1
271
+ x2, y2, z2, dx2, dy2, dz2
272
+ ...
273
+
274
+ The lines that start with # will be considred as commented lines and won't be
275
+ loaded. However, these commented lines are still considered in total line
276
+ count for the start_line and end_line inputs.
277
+
278
+ Args:
279
+ file_path: Full path to sensors file
280
+ start_line: Start line including the comments (default: 0).
281
+ end_line: End line as an integer including the comments
282
+ (default: last line in file).
283
+ identifier: Text string for a unique SensorGrid ID. Must not contain spaces
284
+ or special characters. This will be used to identify the object across
285
+ a model and in the exported Radiance files. If None, the file name
286
+ will be used. (Default: None)
287
+ """
288
+ if not os.path.isfile(file_path):
289
+ raise IOError("Can't find {}.".format(file_path))
290
+ identifier = identifier or os.path.split(os.path.splitext(file_path)[0])[-1]
291
+
292
+ start_line = int(start_line) if start_line is not None else 0
293
+ try:
294
+ end_line = int(end_line)
295
+ except TypeError:
296
+ end_line = float('+inf')
297
+
298
+ line_count = end_line - start_line + 1
299
+
300
+ sensors = []
301
+ with open(file_path, 'r') as inf:
302
+ for _ in range(start_line):
303
+ next(inf)
304
+
305
+ for count, l in enumerate(inf):
306
+ if not count < line_count:
307
+ break
308
+ if not l or l[0] == '#':
309
+ # commented line
310
+ continue
311
+ sensors.append(Sensor.from_raw_values(*l.split()))
312
+
313
+ return cls(identifier, sensors)
314
+
315
+ @classmethod
316
+ def from_merged_grids(cls, grids):
317
+ """Create a SensorGrid by merging together several SensorGrid objects.
318
+
319
+ The first SensorGrid in the input grids will be used to set global properties
320
+ of the result like identifiers and display_names.
321
+
322
+ Args:
323
+ grids: A list of SensorGrid objects to be merged together into a singe
324
+ resulting SensorGrid.
325
+ """
326
+ # check the inputs
327
+ assert len(grids) != 0, 'At least one SensorGrid is needed to use ' \
328
+ 'SensorGrid.from_merged_grids.'
329
+ for grid in grids:
330
+ assert isinstance(grid, SensorGrid), 'Expected SensorGrid for ' \
331
+ 'from_merged_grids. Got {}.'.format(type(grid))
332
+ # merge the sensors, meshes, and base geometry together
333
+ sensors, meshes, base_geo = [], [], []
334
+ for grid in grids:
335
+ sensors.extend(grid.sensors)
336
+ if grid.mesh is not None:
337
+ meshes.extend(grid.mesh)
338
+ if grid.base_geometry is not None:
339
+ base_geo.extend(grid.base_geometry)
340
+ mesh = Mesh3D.join_meshes(meshes) if len(meshes) == len(grids) else None
341
+ base_geo = tuple(base_geo) if len(base_geo) != 0 else None
342
+ # create the new grid and set all properties based on the first one
343
+ new_grid = cls(grids[0].identifier, sensors)
344
+ new_grid.mesh = mesh
345
+ new_grid.base_geometry = base_geo
346
+ new_grid._display_name = grids[0]._display_name
347
+ new_grid._room_identifier = grids[0]._room_identifier
348
+ new_grid._group_identifier = grids[0]._group_identifier
349
+ new_grid._light_path = grids[0]._light_path
350
+ return new_grid
351
+
352
+ @property
353
+ def identifier(self):
354
+ """Get or set text for a unique SensorGrid identifier."""
355
+ return self._identifier
356
+
357
+ @identifier.setter
358
+ def identifier(self, n):
359
+ self._identifier = typing.valid_rad_string(n, 'sensor grid identifier')
360
+
361
+ @property
362
+ def display_name(self):
363
+ """Get or set a string for the object name without any character restrictions.
364
+
365
+ If not set, this will be equal to the identifier.
366
+ """
367
+ if self._display_name is None:
368
+ return self._identifier
369
+ return self._display_name
370
+
371
+ @display_name.setter
372
+ def display_name(self, value):
373
+ try:
374
+ self._display_name = str(value)
375
+ except UnicodeEncodeError: # Python 2 machine lacking the character set
376
+ self._display_name = value # keep it as unicode
377
+
378
+ @property
379
+ def sensors(self):
380
+ """Get or set a tuple of Sensor objects for the grid sensors."""
381
+ return self._sensors
382
+
383
+ @sensors.setter
384
+ def sensors(self, value):
385
+ self._sensors = tuple(value)
386
+ for sen in self._sensors:
387
+ if not isinstance(sen, Sensor):
388
+ raise ValueError(
389
+ 'SensorGrid sensors must be of the Sensor type not %s' % type(sen))
390
+
391
+ @property
392
+ def positions(self):
393
+ """Get a generator of sensor positions as x, y, z."""
394
+ return (ap.pos for ap in self.sensors)
395
+
396
+ @property
397
+ def directions(self):
398
+ """Get a generator of sensor directions as x, y , z."""
399
+ return (ap.dir for ap in self.sensors)
400
+
401
+ @property
402
+ def count(self):
403
+ """Get the number of sensors."""
404
+ return len(self._sensors)
405
+
406
+ @property
407
+ def room_identifier(self):
408
+ """Get or set text for the Room identifier to which this SensorGrid belongs.
409
+
410
+ This will be used in the info_dict method to narrow down the
411
+ number of aperture groups that have to be run with this sensor grid.
412
+ If None, the grid will be run with all aperture groups in the model.
413
+ """
414
+ return self._room_identifier
415
+
416
+ @room_identifier.setter
417
+ def room_identifier(self, n):
418
+ self._room_identifier = typing.valid_string(n)
419
+
420
+ @property
421
+ def group_identifier(self):
422
+ """Get or set text for the group identifier to which this SensorGrid belongs.
423
+
424
+ This will be used in the write to radiance folder method to write all the grids
425
+ with the same group identifier under the same subfolder.
426
+
427
+ You may use / in name to identify nested grid groups. For example
428
+ floor_1/living_room create a sensor grid under living_room/floor_1 subfolder.
429
+
430
+ If None, the grid will be written to the root of grids folder.
431
+ """
432
+ return self._group_identifier
433
+
434
+ @group_identifier.setter
435
+ def group_identifier(self, identifier_key):
436
+ if identifier_key is not None:
437
+ identifier_key = \
438
+ '/'.join(
439
+ typing.valid_rad_string(key, 'sensor grid group identifier')
440
+ for key in identifier_key.split('/')
441
+ )
442
+ self._group_identifier = identifier_key
443
+
444
+ @property
445
+ def full_identifier(self):
446
+ """Get full identifier for a sensor grid.
447
+
448
+ For a sensor grid with group identifier it will be group_identifier/identifier
449
+ """
450
+ return self.identifier if not self.group_identifier \
451
+ else '%s/%s' % (self.group_identifier, self.identifier)
452
+
453
+ @property
454
+ def light_path(self):
455
+ """Get or set list of lists for the light path from the grid to the sky.
456
+
457
+ Each sub-list contains identifiers of aperture groups through which light
458
+ passes. (eg. [['SouthWindow1'], ['__static_apertures__', 'NorthWindow2']]).
459
+ Setting this property will override any auto-calculation of the light
460
+ path from the model upon export to the simulation.
461
+ """
462
+ return self._light_path
463
+
464
+ @light_path.setter
465
+ def light_path(self, l_path):
466
+ if l_path is not None:
467
+ assert isinstance(l_path, (tuple, list)), 'Expected list or tuple for ' \
468
+ 'light_path. Got {}.'.format(type(l_path))
469
+ for ap_list in l_path:
470
+ assert isinstance(ap_list, (tuple, list)), 'Expected list or tuple ' \
471
+ 'for light_path sub-list. Got {}.'.format(type(ap_list))
472
+ for ap in ap_list:
473
+ assert isinstance(ap, str), 'Expected text for light_path ' \
474
+ 'aperture group identifier. Got {}.'.format(type(ap))
475
+ self._light_path = l_path
476
+
477
+ @property
478
+ def mesh(self):
479
+ """Get or set an optional ladybug_geometry Mesh3D that aligns with the sensors.
480
+
481
+ Note that the number of sensors in the grid must match the number of
482
+ faces or the number vertices within the Mesh3D.
483
+ """
484
+ return self._mesh
485
+
486
+ @mesh.setter
487
+ def mesh(self, value):
488
+ if value is not None:
489
+ assert isinstance(value, Mesh3D), \
490
+ 'Expected Mesh3D for SensorGrid mesh. Got {}.'.format(type(value))
491
+ assert self.count == len(value.faces) or self.count == len(value.vertices), \
492
+ 'Number of sensors ({}) does not match the number of mesh faces ({}) ' \
493
+ 'nor the number of vertices ({}).'.format(
494
+ self.count, len(value.faces), len(value.vertices))
495
+ self._mesh = value
496
+
497
+ @property
498
+ def base_geometry(self):
499
+ """Get or set an optional array of ladybug_geometry Face3D used to make the grid.
500
+
501
+ There are no restrictions on how this property relates to the sensors and it
502
+ is provided only to assist with the display of the grid when the number
503
+ of sensors or the mesh is too large to be practically visualized.
504
+ """
505
+ return self._base_geometry
506
+
507
+ @base_geometry.setter
508
+ def base_geometry(self, value):
509
+ if value is not None:
510
+ if not isinstance(value, tuple):
511
+ value = tuple(value)
512
+ for face in value:
513
+ assert isinstance(face, Face3D), 'Expected Face3D for SensorGrid ' \
514
+ 'base_geometry. Got {}.'.format(type(value))
515
+ self._base_geometry = value
516
+
517
+ def info_dict(self, model=None):
518
+ """Get a dictionary with information about the SensorGrid.
519
+
520
+ This can be written as a JSON into a model radiance folder to narrow
521
+ down the number of aperture groups that have to be run with this sensor grid.
522
+
523
+ Args:
524
+ model: A honeybee Model object which will be used to identify
525
+ the aperture groups that will be run with this sensor grid.
526
+ """
527
+ base = {
528
+ 'count': self.count,
529
+ 'name': self.display_name,
530
+ 'identifier': self.identifier,
531
+ 'group': self.group_identifier or '',
532
+ 'full_id': self.full_identifier
533
+ }
534
+ if self._light_path:
535
+ base['light_path'] = self._light_path
536
+ elif model and self._room_identifier: # auto-calculate the light path
537
+ try:
538
+ base['light_path'] = light_path_from_room(model, self._room_identifier)
539
+ except ValueError: # room is not in the model; just ignore light path
540
+ pass
541
+
542
+ if self._group_identifier:
543
+ base['group_identifier'] = self._group_identifier
544
+
545
+ return base
546
+
547
+ def enclosure_info_dict(self, model, air_boundary_distance=0):
548
+ """Get a dictionary with information about sensor relation to rooms.
549
+
550
+ This can be written as a JSON in order to map sensors with appropriate
551
+ energy simulation results in thermal mapping workflows.
552
+
553
+ Args:
554
+ model: A honeybee Model object which will be used to identify
555
+ the rooms/enclosure that each sensor in the grid is contained within.
556
+ air_boundary_distance: An optional number to set the distance from
557
+ air boundaries over which values should be interpolated.
558
+ Using 0 will assume a hard edge between Rooms of the same
559
+ radiant enclosures. (Default: 0).
560
+ """
561
+ # setup rooms and lists to check enclosure info
562
+ enclosures, sensor_indices, air_bound_proximity = {}, [], {}
563
+ has_indoor, has_outdoor = False, False
564
+ rooms = model._rooms
565
+ if self.room_identifier: # put the assigned room first for faster calculation
566
+ rooms = model.rooms_by_identifier([self.room_identifier]) + rooms
567
+
568
+ # have a dictionary to track proximity to AirBoundary faces
569
+ model_ab = {}
570
+ for room in rooms:
571
+ model_ab[room.identifier] = \
572
+ [f for f in room.faces if isinstance(f.type, AirBoundary)]
573
+
574
+ def _air_boundary_info(distance, face, room_index):
575
+ """Method to perform interpolation across AirBoundary Faces."""
576
+ adj_room = face.boundary_condition.boundary_condition_objects[-1]
577
+ try:
578
+ adj_i = enclosures[adj_room]
579
+ except KeyError: # the first time that this room is needed
580
+ adj_i = len(enclosures)
581
+ enclosures[adj_room] = len(enclosures)
582
+ fac_1 = 0.5 + (distance / (air_boundary_distance * 2))
583
+ return {room_index: fac_1, adj_i: 1 - fac_1}
584
+
585
+ # loop through the sensors and verify the room that they belong to
586
+ for i, sensor in enumerate(self.sensors):
587
+ sensor_pt = Point3D(*sensor.pos)
588
+ for room in rooms:
589
+ if room.geometry.is_point_inside(sensor_pt):
590
+ # add the room index of the sensor
591
+ try:
592
+ sensor_indices.append(enclosures[room.identifier])
593
+ except KeyError: # the first time that this room is needed
594
+ enclosures[room.identifier] = len(enclosures)
595
+ sensor_indices.append(enclosures[room.identifier])
596
+ has_indoor = True
597
+ # test if the sensor is near any AriBoundary faces
598
+ air_b = model_ab[room.identifier]
599
+ if air_boundary_distance > 0 and len(air_b) != 0:
600
+ for face in air_b:
601
+ fg = face.geometry
602
+ close_pt = fg._plane.closest_point(sensor_pt)
603
+ p_dist = sensor_pt.distance_to_point(close_pt)
604
+ if p_dist <= air_boundary_distance:
605
+ close_pt_2d = fg._plane.xyz_to_xy(close_pt)
606
+ g_dist = fg.polygon2d.distance_to_point(close_pt_2d)
607
+ f_dist = math.sqrt(p_dist ** 2 + g_dist ** 2)
608
+ if f_dist <= air_boundary_distance:
609
+ ab_info = _air_boundary_info(
610
+ f_dist, face, sensor_indices[-1])
611
+ try:
612
+ air_bound_proximity[i].append(ab_info)
613
+ except KeyError:
614
+ air_bound_proximity[i] = [ab_info]
615
+ break # we found the room and we don't need to iterate
616
+ else: # the sensor is completely outside and not a part of a room
617
+ sensor_indices.append(-1)
618
+ has_outdoor = True
619
+
620
+ # write out the enclosure info JSON
621
+ mapper = sorted(enclosures, key=enclosures.__getitem__)
622
+ return {
623
+ 'has_indoor': has_indoor,
624
+ 'has_outdoor': has_outdoor,
625
+ 'mapper': mapper,
626
+ 'sensor_indices': sensor_indices,
627
+ 'air_bound_proximity': air_bound_proximity
628
+ }
629
+
630
+ def to_radiance(self):
631
+ """Return sensors grid as a Radiance string."""
632
+ return "\n".join((ap.to_radiance() for ap in self._sensors))
633
+
634
+ def to_file(self, folder, file_name=None, mkdir=False, ignore_group=False):
635
+ """Write this sensor grid to a Radiance sensors file.
636
+
637
+ Args:
638
+ folder: Target folder. If grid is part of a sensor group identifier it will
639
+ be written to a subfolder with group identifier name.
640
+ file_name: Optional file name without extension. (Default: self.identifier)
641
+ mkdir: A boolean to indicate if the folder should be created in case it
642
+ doesn't exist already. (Default: False).
643
+ ignore_group: A boolean to indicate if creating a new subfolder for sensor
644
+ group should be ignored. (Default: False).
645
+
646
+ Returns:
647
+ Full path to newly created file.
648
+ """
649
+ identifier = file_name or self.identifier + '.pts'
650
+ if not identifier.endswith('.pts'):
651
+ identifier += '.pts'
652
+ if not ignore_group and self.group_identifier:
653
+ folder = os.path.normpath(os.path.join(folder, self.group_identifier))
654
+ mkdir = True # in most cases the subfolder does not exist already
655
+
656
+ return futil.write_to_file_by_name(
657
+ folder, identifier, self.to_radiance() + '\n', mkdir)
658
+
659
+ def to_files(self, folder, count, base_name=None, mkdir=False):
660
+ """Split this sensor grid and write them to several files.
661
+
662
+ This method writes the files directly to the folder and doesn't create a
663
+ subfolder for sensor groups if any. You can add the group subfolder to folder
664
+ before calling the method.
665
+
666
+ Args:
667
+ folder: Target folder.
668
+ count: Number of files.
669
+ base_name: Optional text string for a unique base name for the sensor
670
+ grid files. (Default: self.identifier)
671
+ mkdir: A boolean to indicate if the folder should be created in case it
672
+ doesn't exist already (Default: False).
673
+
674
+ Returns:
675
+ A list of dicts containing the grid name, path to the grid and full path
676
+ to the grid.
677
+ """
678
+ count = typing.int_in_range(count, 1, input_name='file count')
679
+ base_name = base_name or self.identifier
680
+ if count == 1 or self.count == 0:
681
+ name = '%s_0000' % base_name
682
+ full_path = self.to_file(folder, name, mkdir, ignore_group=True)
683
+ return [
684
+ {'name': name if not name.endswith('.pts') else name.replace('.pts', ''),
685
+ 'path': name + '.pts' if not name.endswith('.pts') else name,
686
+ 'full_path': full_path,
687
+ 'count': self.count}
688
+ ]
689
+ # calculate sensor count in each file
690
+ sc = int(round(self.count / count))
691
+ sensors = iter(self._sensors)
692
+ for fc in range(count - 1):
693
+ name = '%s_%04d.pts' % (base_name, fc)
694
+ content = '\n'.join((next(sensors).to_radiance() for _ in range(sc)))
695
+ futil.write_to_file_by_name(folder, name, content + '\n', mkdir)
696
+
697
+ # write whatever is left to the last file
698
+ name = '%s_%04d.pts' % (base_name, count - 1)
699
+ content = '\n'.join((sensor.to_radiance() for sensor in sensors))
700
+ futil.write_to_file_by_name(folder, name, content + '\n', mkdir)
701
+
702
+ grids_info = []
703
+
704
+ for c in range(count):
705
+ name = '%s_%04d' % (base_name, c)
706
+ path = '%s.pts' % name
707
+ full_path = os.path.join(folder, path)
708
+
709
+ grids_info.append({
710
+ 'name': name,
711
+ 'path': path,
712
+ 'full_path': full_path,
713
+ 'count': sc
714
+ })
715
+
716
+ # adjust the count for the last grid
717
+ grids_info[-1]['count'] = self.count - sc * (count - 1)
718
+
719
+ return grids_info
720
+
721
+ def to_dict(self):
722
+ """Convert SensorGrid to a dictionary."""
723
+ base = {
724
+ 'type': 'SensorGrid',
725
+ 'identifier': self.identifier,
726
+ 'sensors': [sen.to_dict() for sen in self.sensors]
727
+ }
728
+ if self._display_name is not None:
729
+ base['display_name'] = self.display_name
730
+ if self._room_identifier is not None:
731
+ base['room_identifier'] = self.room_identifier
732
+ if self._group_identifier is not None:
733
+ base['group_identifier'] = self.group_identifier
734
+ if self._light_path is not None:
735
+ base['light_path'] = self.light_path
736
+ if self._mesh is not None:
737
+ base['mesh'] = self._mesh.to_dict()
738
+ if self._base_geometry is not None:
739
+ base['base_geometry'] = [face.to_dict() for face in self._base_geometry]
740
+ if self._group_identifier is not None:
741
+ base['group_identifier'] = self.group_identifier
742
+ return base
743
+
744
+ def to_json(self, folder, file_name=None, mkdir=False, ignore_group=False):
745
+ """Write this sensor grid to a JSON file.
746
+
747
+ Args:
748
+ folder: Target folder. If grid is part of a sensor group identifier it will
749
+ be written to a subfolder with group identifier name.
750
+ file_name: Optional file name without extension. (Default: self.identifier)
751
+ mkdir: A boolean to indicate if the folder should be created in case it
752
+ doesn't exist already. (Default: False).
753
+ ignore_group: A boolean to indicate if creating a new subfolder for sensor
754
+ group should be ignored. (Default: False).
755
+
756
+ Returns:
757
+ Full path to newly created file.
758
+ """
759
+ identifier = file_name or self.identifier + '.json'
760
+ if not identifier.endswith('.json'):
761
+ identifier += '.json'
762
+ if not ignore_group and self.group_identifier:
763
+ folder = os.path.normpath(os.path.join(folder, self.group_identifier))
764
+ mkdir = True # in most cases the subfolder does not exist already
765
+ return futil.write_to_file_by_name(
766
+ folder, identifier, json.dumps(self.to_dict()), mkdir)
767
+
768
+ def to_radial_grid(self, dir_count=8, start_vector=Vector3D(0, -1, 0),
769
+ mesh_radius=0):
770
+ """Get a radial sensor grid using the positions of this grid as a base.
771
+
772
+ All properties of this grid will be transferred to the new grid, including
773
+ the identifier, room_identifier, etc. The mesh will be recomputed based
774
+ on the input mesh_radius but any base_geometry will be transferred.
775
+
776
+ Note that calling this method on a SensorGrid that is already formatted
777
+ as a radial grid will result in a lot of unwanted duplication of sensors.
778
+
779
+ Args:
780
+ dir_count: A positive integer for the number of radial directions
781
+ to be generated around each position. (Default: 8).
782
+ start_vector: A Vector3D to set the start direction of the generated
783
+ directions. This can be used to orient the resulting sensors to
784
+ specific parts of the scene. It can also change the elevation of the
785
+ resulting directions since this start vector will always be rotated in
786
+ the XY plane to generate the resulting directions. (Default: (0, -1, 0)).
787
+ mesh_radius: An optional number that can be used to generate a Mesh3D
788
+ that is aligned with the resulting sensors and will automatically
789
+ be assigned to the grid's mesh property. Such meshes will resemble
790
+ a circle around each sensor with the specified radius and will
791
+ contain triangular faces that can be colored with simulation results.
792
+ If zero, no mesh will be generated for the sensor grid. (Default: 0).
793
+ """
794
+ new_grid = SensorGrid.from_positions_radial(
795
+ self.identifier, self.positions, dir_count, start_vector, mesh_radius)
796
+ new_grid._display_name = self._display_name
797
+ new_grid._room_identifier = self._room_identifier
798
+ new_grid.group_identifier = self.group_identifier
799
+ new_grid._light_path = self._light_path
800
+ new_grid._base_geometry = self._base_geometry
801
+ return new_grid
802
+
803
+ def move(self, moving_vec):
804
+ """Move this sensor grid along a vector.
805
+
806
+ Args:
807
+ moving_vec: A ladybug_geometry Vector3D with the direction and distance
808
+ to move the sensor.
809
+ """
810
+ for sens in self._sensors:
811
+ sens.move(moving_vec)
812
+ if self._mesh is not None:
813
+ self._mesh = self._mesh.move(moving_vec)
814
+ if self._base_geometry is not None:
815
+ self._base_geometry = \
816
+ tuple(face.move(moving_vec) for face in self._base_geometry)
817
+
818
+ def rotate(self, axis, angle, origin):
819
+ """Rotate this sensor grid by a certain angle around an axis and origin.
820
+
821
+ Args:
822
+ axis: Rotation axis as a Vector3D.
823
+ angle: An angle for rotation in degrees.
824
+ origin: A ladybug_geometry Point3D for the origin around which the
825
+ object will be rotated.
826
+ """
827
+ for sens in self._sensors:
828
+ sens.rotate(axis, angle, origin)
829
+ r_angle = math.radians(angle)
830
+ if self._mesh is not None:
831
+ self._mesh = self._mesh.rotate(axis, r_angle, origin)
832
+ if self._base_geometry is not None:
833
+ self._base_geometry = \
834
+ tuple(face.rotate(axis, r_angle, origin) for face in self._base_geometry)
835
+
836
+ def rotate_xy(self, angle, origin):
837
+ """Rotate this sensor grid counterclockwise in the world XY plane by an angle.
838
+
839
+ Args:
840
+ angle: An angle in degrees.
841
+ origin: A ladybug_geometry Point3D for the origin around which the
842
+ object will be rotated.
843
+ """
844
+ for sens in self._sensors:
845
+ sens.rotate_xy(angle, origin)
846
+ r_angle = math.radians(angle)
847
+ if self._mesh is not None:
848
+ self._mesh = self._mesh.rotate_xy(r_angle, origin)
849
+ if self._base_geometry is not None:
850
+ self._base_geometry = \
851
+ tuple(face.rotate_xy(r_angle, origin) for face in self._base_geometry)
852
+
853
+ def reflect(self, plane):
854
+ """Reflect this sensor grid across a plane.
855
+
856
+ Args:
857
+ plane: A ladybug_geometry Plane across which the object will
858
+ be reflected.
859
+ """
860
+ for sens in self._sensors:
861
+ sens.reflect(plane)
862
+ if self._mesh is not None:
863
+ self._mesh = self._mesh.reflect(plane.n, plane.o)
864
+ if self._base_geometry is not None:
865
+ self._base_geometry = \
866
+ tuple(face.reflect(plane.n, plane.o) for face in self._base_geometry)
867
+
868
+ def scale(self, factor, origin=None):
869
+ """Scale this sensor grid by a factor from an origin point.
870
+
871
+ Args:
872
+ factor: A number representing how much the object should be scaled.
873
+ origin: A ladybug_geometry Point3D representing the origin from which
874
+ to scale. If None, it will be scaled from the World origin (0, 0, 0).
875
+ """
876
+ for sens in self._sensors:
877
+ sens.scale(factor, origin)
878
+ if self._mesh is not None:
879
+ self._mesh = self._mesh.scale(factor, origin)
880
+ if self._base_geometry is not None:
881
+ self._base_geometry = \
882
+ tuple(face.scale(factor, origin) for face in self._base_geometry)
883
+
884
+ def duplicate(self):
885
+ """Get a copy of this object."""
886
+ return self.__copy__()
887
+
888
+ @staticmethod
889
+ def from_face3d_arrays(
890
+ base_identifier, face_arrays, x_dim, y_dim=None, offset=0, flip=False):
891
+ """Get an array of SensorGrids from an matrix (list of lists) of Face3Ds.
892
+
893
+ This method uses the from_face3d classmethod but includes checks to
894
+ catch cases where the input Face3Ds cannot support the generation of
895
+ quad grids. In this case, the invalid SensorGrid will not be generated
896
+ and will be excluded form the output list of SensorGrids.
897
+
898
+ Args:
899
+ base_identifier: Text string for a unique SensorGrid ID, which will be used
900
+ as a base for all of the output SensorGrid IDs. Must not contain spaces
901
+ or special characters.
902
+ faces: An matrix (list of lists) of ladybug_geometry Face3Ds from which
903
+ SensorGrids will be generated.
904
+ x_dim: The x dimension of the grid cells as a number.
905
+ y_dim: The y dimension of the grid cells as a number. Default is None,
906
+ which will assume the same cell dimension for y as is set for x.
907
+ offset: A number for how far to offset the grid from the base face.
908
+ flip: Set to True to have the mesh normals reversed from the direction of
909
+ this face and to have the offset input move the mesh in the opposite
910
+ direction from this face normal. Defaults to False, which means the
911
+ normal direction of the face will be used as the direction of the
912
+ sensor grids.
913
+ """
914
+ grids = []
915
+ for i, faces in enumerate(face_arrays):
916
+ grid_id = '{}_{}'.format(base_identifier, i)
917
+ try:
918
+ grids.append(
919
+ SensorGrid.from_face3d(grid_id, faces, x_dim, y_dim, offset, flip)
920
+ )
921
+ except AssertionError: # none of the Face3Ds make a valid grid
922
+ continue
923
+ return grids
924
+
925
+ @staticmethod
926
+ def radial_positions_mesh(
927
+ positions, dir_count=8, start_vector=Vector3D(0, -1, 0), mesh_radius=1):
928
+ """Generate a Mesh3D resembling a circle around each position.
929
+
930
+ Args:
931
+ positions: A list of (x, y ,z) tuples for position of sensors.
932
+ dir_count: A positive integer for the number of radial directions
933
+ to be generated around each position. (Default: 8).
934
+ start_vector: A Vector3D to set the start direction of the generated
935
+ directions. (Default: (0, -1, 0)).
936
+ mesh_radius: A number for the radius of the radial mesh to be
937
+ generated around each sensor. (Default: 1).
938
+ """
939
+ # set up the start vector and rotation angles
940
+ st_vec = Vector3D(start_vector.x, start_vector.y, 0).normalize()
941
+ st_vec = st_vec * mesh_radius
942
+ inc_ang = (math.pi * 2) / dir_count
943
+ st_vec = st_vec.rotate_xy(-inc_ang / 2)
944
+ # loop through the positions and angles to create the mesh
945
+ verts, faces = [], []
946
+ v_count = 0
947
+ for pt in positions:
948
+ st_pt = Point3D(*pt)
949
+ nxt_pt = st_pt.move(st_vec)
950
+ verts.extend([st_pt, nxt_pt])
951
+ for i in range(dir_count - 1):
952
+ new_pt = verts[-1].rotate_xy(inc_ang, st_pt)
953
+ new_f = (v_count, v_count + i + 1, v_count + i + 2)
954
+ verts.append(new_pt)
955
+ faces.append(new_f)
956
+ faces.append((v_count, v_count + dir_count, v_count + 1))
957
+ v_count += (dir_count + 1)
958
+ return Mesh3D(verts, faces)
959
+
960
+ def __len__(self):
961
+ """Number of sensors in this grid."""
962
+ return len(self.sensors)
963
+
964
+ def __getitem__(self, index):
965
+ """Get a sensor for an index."""
966
+ return self.sensors[index]
967
+
968
+ def __copy__(self):
969
+ new_obj = SensorGrid(self.identifier, (sen.duplicate() for sen in self.sensors))
970
+ new_obj._display_name = self._display_name
971
+ new_obj._room_identifier = self._room_identifier
972
+ new_obj.group_identifier = self.group_identifier
973
+ new_obj._light_path = self._light_path
974
+ new_obj._mesh = self._mesh
975
+ new_obj._base_geometry = self._base_geometry
976
+ return new_obj
977
+
978
+ def __key(self):
979
+ """A tuple based on the object properties, useful for hashing."""
980
+ return (
981
+ self.identifier, self._display_name, self._room_identifier,
982
+ self._room_identifier) + tuple(hash(sensor) for sensor in self.sensors)
983
+
984
+ def __hash__(self):
985
+ return hash(self.__key())
986
+
987
+ def __eq__(self, other):
988
+ return isinstance(other, SensorGrid) and self.__key() == other.__key() and \
989
+ self.light_path == other.light_path
990
+
991
+ def __ne__(self, value):
992
+ return not self.__eq__(value)
993
+
994
+ def __iter__(self):
995
+ """Sensors iterator."""
996
+ return iter(self.sensors)
997
+
998
+ def __str__(self):
999
+ """String repr."""
1000
+ return self.to_radiance()
1001
+
1002
+ def ToString(self):
1003
+ """Overwrite ToString .NET method."""
1004
+ return self.__repr__()
1005
+
1006
+ def __repr__(self):
1007
+ """Get the string representation of the sensor grid."""
1008
+ return 'SensorGrid: {} [{} sensors]'.format(self.display_name, len(self.sensors))