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.
- honeybee_radiance/__init__.py +11 -0
- honeybee_radiance/__main__.py +4 -0
- honeybee_radiance/_extend_honeybee.py +93 -0
- honeybee_radiance/cli/__init__.py +88 -0
- honeybee_radiance/cli/dc.py +400 -0
- honeybee_radiance/cli/edit.py +529 -0
- honeybee_radiance/cli/glare.py +118 -0
- honeybee_radiance/cli/grid.py +859 -0
- honeybee_radiance/cli/lib.py +458 -0
- honeybee_radiance/cli/modifier.py +133 -0
- honeybee_radiance/cli/mtx.py +226 -0
- honeybee_radiance/cli/multiphase.py +1034 -0
- honeybee_radiance/cli/octree.py +640 -0
- honeybee_radiance/cli/postprocess.py +1186 -0
- honeybee_radiance/cli/raytrace.py +219 -0
- honeybee_radiance/cli/rpict.py +125 -0
- honeybee_radiance/cli/schedule.py +56 -0
- honeybee_radiance/cli/setconfig.py +63 -0
- honeybee_radiance/cli/sky.py +545 -0
- honeybee_radiance/cli/study.py +66 -0
- honeybee_radiance/cli/sunpath.py +331 -0
- honeybee_radiance/cli/threephase.py +255 -0
- honeybee_radiance/cli/translate.py +400 -0
- honeybee_radiance/cli/util.py +121 -0
- honeybee_radiance/cli/view.py +261 -0
- honeybee_radiance/cli/viewfactor.py +347 -0
- honeybee_radiance/config.json +6 -0
- honeybee_radiance/config.py +427 -0
- honeybee_radiance/dictutil.py +50 -0
- honeybee_radiance/dynamic/__init__.py +5 -0
- honeybee_radiance/dynamic/group.py +479 -0
- honeybee_radiance/dynamic/multiphase.py +557 -0
- honeybee_radiance/dynamic/state.py +718 -0
- honeybee_radiance/dynamic/stategeo.py +352 -0
- honeybee_radiance/geometry/__init__.py +13 -0
- honeybee_radiance/geometry/bubble.py +42 -0
- honeybee_radiance/geometry/cone.py +215 -0
- honeybee_radiance/geometry/cup.py +54 -0
- honeybee_radiance/geometry/cylinder.py +197 -0
- honeybee_radiance/geometry/geometrybase.py +37 -0
- honeybee_radiance/geometry/instance.py +40 -0
- honeybee_radiance/geometry/mesh.py +38 -0
- honeybee_radiance/geometry/polygon.py +174 -0
- honeybee_radiance/geometry/ring.py +214 -0
- honeybee_radiance/geometry/source.py +182 -0
- honeybee_radiance/geometry/sphere.py +178 -0
- honeybee_radiance/geometry/tube.py +46 -0
- honeybee_radiance/lib/__init__.py +1 -0
- honeybee_radiance/lib/_loadmodifiers.py +72 -0
- honeybee_radiance/lib/_loadmodifiersets.py +69 -0
- honeybee_radiance/lib/modifiers.py +58 -0
- honeybee_radiance/lib/modifiersets.py +63 -0
- honeybee_radiance/lightpath.py +204 -0
- honeybee_radiance/lightsource/__init__.py +1 -0
- honeybee_radiance/lightsource/_gendaylit.py +479 -0
- honeybee_radiance/lightsource/dictutil.py +49 -0
- honeybee_radiance/lightsource/ground.py +160 -0
- honeybee_radiance/lightsource/sky/__init__.py +7 -0
- honeybee_radiance/lightsource/sky/_skybase.py +177 -0
- honeybee_radiance/lightsource/sky/certainirradiance.py +232 -0
- honeybee_radiance/lightsource/sky/cie.py +378 -0
- honeybee_radiance/lightsource/sky/climatebased.py +501 -0
- honeybee_radiance/lightsource/sky/hemisphere.py +160 -0
- honeybee_radiance/lightsource/sky/skydome.py +113 -0
- honeybee_radiance/lightsource/sky/skymatrix.py +163 -0
- honeybee_radiance/lightsource/sky/strutil.py +34 -0
- honeybee_radiance/lightsource/sky/sunmatrix.py +212 -0
- honeybee_radiance/lightsource/sunpath.py +247 -0
- honeybee_radiance/modifier/__init__.py +3 -0
- honeybee_radiance/modifier/material/__init__.py +30 -0
- honeybee_radiance/modifier/material/absdf.py +477 -0
- honeybee_radiance/modifier/material/antimatter.py +54 -0
- honeybee_radiance/modifier/material/ashik2.py +51 -0
- honeybee_radiance/modifier/material/brtdfunc.py +81 -0
- honeybee_radiance/modifier/material/bsdf.py +292 -0
- honeybee_radiance/modifier/material/dielectric.py +53 -0
- honeybee_radiance/modifier/material/glass.py +431 -0
- honeybee_radiance/modifier/material/glow.py +246 -0
- honeybee_radiance/modifier/material/illum.py +51 -0
- honeybee_radiance/modifier/material/interface.py +49 -0
- honeybee_radiance/modifier/material/light.py +206 -0
- honeybee_radiance/modifier/material/materialbase.py +36 -0
- honeybee_radiance/modifier/material/metal.py +167 -0
- honeybee_radiance/modifier/material/metal2.py +41 -0
- honeybee_radiance/modifier/material/metdata.py +41 -0
- honeybee_radiance/modifier/material/metfunc.py +41 -0
- honeybee_radiance/modifier/material/mirror.py +340 -0
- honeybee_radiance/modifier/material/mist.py +86 -0
- honeybee_radiance/modifier/material/plasdata.py +58 -0
- honeybee_radiance/modifier/material/plasfunc.py +59 -0
- honeybee_radiance/modifier/material/plastic.py +354 -0
- honeybee_radiance/modifier/material/plastic2.py +58 -0
- honeybee_radiance/modifier/material/prism1.py +57 -0
- honeybee_radiance/modifier/material/prism2.py +48 -0
- honeybee_radiance/modifier/material/spotlight.py +50 -0
- honeybee_radiance/modifier/material/trans.py +518 -0
- honeybee_radiance/modifier/material/trans2.py +49 -0
- honeybee_radiance/modifier/material/transdata.py +50 -0
- honeybee_radiance/modifier/material/transfunc.py +53 -0
- honeybee_radiance/modifier/mixture/__init__.py +6 -0
- honeybee_radiance/modifier/mixture/mixdata.py +49 -0
- honeybee_radiance/modifier/mixture/mixfunc.py +54 -0
- honeybee_radiance/modifier/mixture/mixpict.py +52 -0
- honeybee_radiance/modifier/mixture/mixtext.py +66 -0
- honeybee_radiance/modifier/mixture/mixturebase.py +28 -0
- honeybee_radiance/modifier/modifierbase.py +40 -0
- honeybee_radiance/modifier/pattern/__init__.py +9 -0
- honeybee_radiance/modifier/pattern/brightdata.py +49 -0
- honeybee_radiance/modifier/pattern/brightfunc.py +47 -0
- honeybee_radiance/modifier/pattern/brighttext.py +81 -0
- honeybee_radiance/modifier/pattern/colordata.py +56 -0
- honeybee_radiance/modifier/pattern/colorfunc.py +47 -0
- honeybee_radiance/modifier/pattern/colorpict.py +54 -0
- honeybee_radiance/modifier/pattern/colortext.py +73 -0
- honeybee_radiance/modifier/pattern/patternbase.py +34 -0
- honeybee_radiance/modifier/texture/__init__.py +4 -0
- honeybee_radiance/modifier/texture/texdata.py +29 -0
- honeybee_radiance/modifier/texture/texfunc.py +26 -0
- honeybee_radiance/modifier/texture/texturebase.py +27 -0
- honeybee_radiance/modifierset.py +1091 -0
- honeybee_radiance/mutil.py +60 -0
- honeybee_radiance/postprocess/__init__.py +1 -0
- honeybee_radiance/postprocess/annual.py +108 -0
- honeybee_radiance/postprocess/annualdaylight.py +425 -0
- honeybee_radiance/postprocess/annualglare.py +201 -0
- honeybee_radiance/postprocess/annualirradiance.py +187 -0
- honeybee_radiance/postprocess/electriclight.py +119 -0
- honeybee_radiance/postprocess/en17037.py +261 -0
- honeybee_radiance/postprocess/leed.py +304 -0
- honeybee_radiance/postprocess/solartracking.py +90 -0
- honeybee_radiance/primitive.py +554 -0
- honeybee_radiance/properties/__init__.py +1 -0
- honeybee_radiance/properties/_base.py +390 -0
- honeybee_radiance/properties/aperture.py +197 -0
- honeybee_radiance/properties/door.py +198 -0
- honeybee_radiance/properties/face.py +123 -0
- honeybee_radiance/properties/model.py +1291 -0
- honeybee_radiance/properties/room.py +490 -0
- honeybee_radiance/properties/shade.py +186 -0
- honeybee_radiance/properties/shademesh.py +116 -0
- honeybee_radiance/putil.py +44 -0
- honeybee_radiance/reader.py +214 -0
- honeybee_radiance/sensor.py +166 -0
- honeybee_radiance/sensorgrid.py +1008 -0
- honeybee_radiance/view.py +1101 -0
- honeybee_radiance/writer.py +951 -0
- honeybee_radiance-1.66.190.dist-info/METADATA +89 -0
- honeybee_radiance-1.66.190.dist-info/RECORD +152 -0
- honeybee_radiance-1.66.190.dist-info/WHEEL +5 -0
- honeybee_radiance-1.66.190.dist-info/entry_points.txt +2 -0
- honeybee_radiance-1.66.190.dist-info/licenses/LICENSE +661 -0
- 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))
|