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,557 @@
1
+ # coding=utf-8
2
+ """Functions for auto-assigning aperture groups for multiphase studies."""
3
+ from __future__ import division
4
+ import os
5
+ import math
6
+ import json
7
+ from collections import OrderedDict
8
+
9
+ from ladybug_geometry.geometry3d.mesh import Mesh3D
10
+ from ladybug.futil import write_to_file_by_name
11
+ from honeybee.boundarycondition import Outdoors
12
+ from honeybee.typing import clean_rad_string
13
+ from honeybee.config import folders as hb_folders
14
+ from honeybee_radiance_command.oconv import Oconv
15
+ from honeybee_radiance_command.rfluxmtx import RfluxmtxOptions, Rfluxmtx
16
+
17
+ from honeybee_radiance.config import folders
18
+ from honeybee_radiance.sensorgrid import SensorGrid
19
+ from honeybee_radiance.lightsource.sky.skydome import SkyDome
20
+
21
+
22
+ def _index_and_min(distance_matrix):
23
+ """Return the minimum value of the distance matrix, as well as the index [j, i] of
24
+ the minimum value of the distance matrix."""
25
+ min_value = min([min(sublist) for sublist in distance_matrix])
26
+ for i, _i in enumerate(distance_matrix):
27
+ for j, _j in enumerate(distance_matrix):
28
+ if distance_matrix[i][j] == min_value:
29
+ index = [j, i]
30
+ break
31
+ return min_value, index
32
+
33
+
34
+ def _pairwise_maximum(array1, array2):
35
+ """Return an array of the pairwise maximum of two arrays."""
36
+ pair_array = [array1, array2]
37
+ max_array = list(map(max, zip(*pair_array)))
38
+ return max_array
39
+
40
+
41
+ def _transpose_matrix(matrix):
42
+ """Transposes the distance matrix."""
43
+ matrix = list(map(list, zip(*matrix)))
44
+ return matrix
45
+
46
+
47
+ def _rmse_from_matrix(vf_matrix_dict):
48
+ """Calculates RMSE."""
49
+ rmse = []
50
+ for predicted in vf_matrix_dict.values():
51
+ r_list = []
52
+ for observed in vf_matrix_dict.values():
53
+ error = [(p - o) for p, o in zip(predicted, observed)]
54
+ square_error = [e ** 2 for e in error]
55
+ mean_square_error = sum(square_error) / len(square_error)
56
+ root_mean_square_error = mean_square_error ** 0.5
57
+ r_list.append(root_mean_square_error)
58
+ rmse.append(r_list)
59
+ return rmse
60
+
61
+
62
+ def _flatten(container):
63
+ """Flatten an array."""
64
+ if not isinstance(container, list):
65
+ container = [container]
66
+ for i in container:
67
+ if isinstance(i, (list, tuple)):
68
+ for j in _flatten(i):
69
+ yield j
70
+ else:
71
+ yield i
72
+
73
+
74
+ def _agglomerative_clustering_complete(distance_matrix, apertures, threshold=0.001):
75
+ """Cluster apertures based on the threshold."""
76
+
77
+ # Fill the diagonal with 9999 so a diagonal of zeros will NOT be stored
78
+ # as min_value.
79
+ for i in range(len(distance_matrix)):
80
+ distance_matrix[i][i] = 9999
81
+
82
+ # Create starting list of aperture groups. Each aperture starts as its
83
+ # own group.
84
+ ap_groups = apertures
85
+
86
+ # Set the number of samples and the minimum value of the distance
87
+ # matrix.
88
+ n_samples = len(distance_matrix)
89
+
90
+ # Set the minimum value of the distance matrix and find the indices of
91
+ # the minimum value in the distance matrix.
92
+ min_value, index = _index_and_min(distance_matrix)
93
+
94
+ while n_samples > 1 and min_value < threshold:
95
+ # Combine the two groups and place it at index 0, and remove item
96
+ # at index 1.
97
+ ap_groups[index[0]] = [ap_groups[index[0]], ap_groups[index[1]]]
98
+ ap_groups.pop(index[1])
99
+
100
+ # Update the values in the distance matrix. We need the maximum
101
+ # values between the new cluster and all the remaining apertures or
102
+ # clusters still in the distance matrix.
103
+ distance_matrix[index[0]] = \
104
+ _pairwise_maximum(distance_matrix[index[0]], distance_matrix[index[1]])
105
+ distance_matrix = _transpose_matrix(distance_matrix)
106
+ distance_matrix[index[0]] = \
107
+ _pairwise_maximum(distance_matrix[index[0]], distance_matrix[index[1]])
108
+
109
+ # Remove the values at index 1 along both axes.
110
+ distance_matrix.pop(index[1])
111
+ distance_matrix = _transpose_matrix(distance_matrix)
112
+ distance_matrix.pop(index[1])
113
+
114
+ # Update the number of samples that are left in the distance matrix.
115
+ n_samples -= 1
116
+ # Update the minimum value and the indices.
117
+ min_value, index = _index_and_min(distance_matrix)
118
+
119
+ return ap_groups
120
+
121
+
122
+ def aperture_view_factor(
123
+ project_folder, apertures, size=0.2, ambient_division=1000,
124
+ receiver='rflux_sky.sky', octree='scene.oct',
125
+ calc_folder='aperture_grouping'
126
+ ):
127
+ """Calculates the view factor for each aperture by sensor points."""
128
+
129
+ # Instantiate dictionary that will store the sensor count for each
130
+ # aperture. We need a OrderedDict so that we can split the rfluxmtx
131
+ # output file by each aperture (sensor count) in the correct order.
132
+ ap_dict = OrderedDict()
133
+
134
+ meshes = []
135
+ # Create a mesh for each aperture and add the the sensor count to dict.
136
+ for aperture in apertures:
137
+ ap_mesh = aperture.geometry.mesh_grid(size, generate_centroids=False)
138
+ meshes.append(ap_mesh)
139
+ ap_dict[aperture.identifier] = \
140
+ {
141
+ 'sensor_count': len(ap_mesh.faces),
142
+ 'aperture': aperture
143
+ }
144
+
145
+ # Create a sensor grid from joined aperture mesh.
146
+ mesh = Mesh3D.join_meshes(meshes).offset_mesh(0.001)
147
+ grid_mesh = SensorGrid.from_mesh3d('aperture_grid', mesh)
148
+
149
+ # Write sensor grid to pts file.
150
+ sensors = grid_mesh.to_file(os.path.join(project_folder, calc_folder),
151
+ file_name='apertures')
152
+
153
+ # Rfluxmtx options.
154
+ rflux_opt = RfluxmtxOptions()
155
+ rflux_opt.ad = ambient_division
156
+ rflux_opt.lw = 1.0 / float(rflux_opt.ad)
157
+ rflux_opt.I = True
158
+ rflux_opt.h = True
159
+
160
+ # Rfluxmtx command.
161
+ rflux = Rfluxmtx()
162
+ rflux.options = rflux_opt
163
+ rflux.receivers = receiver
164
+ rflux.sensors = sensors
165
+ rflux.octree = octree
166
+ rflux.output = os.path.join(calc_folder, 'apertures_vf.mtx')
167
+
168
+ # Run rfluxmtx command.
169
+ env = None
170
+ if folders.env != {}:
171
+ env = folders.env
172
+ env = dict(os.environ, **env) if env else None
173
+ rflux.run(env=env, cwd=project_folder)
174
+
175
+ # Get the output file of the rfluxmtx command.
176
+ mtx_file = os.path.join(project_folder, rflux.output)
177
+
178
+ return mtx_file, ap_dict
179
+
180
+
181
+ def aperture_view_factor_postprocess(mtx_file, ap_dict, room_apertures, room_based=True):
182
+ view_factor = []
183
+ # Read view factor file, convert to one channel output, and divide by
184
+ # Pi.
185
+ with open(mtx_file) as mtx_data:
186
+ for sensor in mtx_data:
187
+ sensor_split = sensor.strip().split()
188
+ if len(sensor_split) % 3 == 0:
189
+ one_channel = sensor_split[::3]
190
+
191
+ def convert_to_vf(x):
192
+ return float(x) / math.pi
193
+ view_factor.append(list(map(convert_to_vf, one_channel)))
194
+
195
+ ap_view_factor = OrderedDict()
196
+ # Split the view factor file by the aperture sensor count.
197
+ for ap_id, value in ap_dict.items():
198
+ sensor_count = value['sensor_count']
199
+ ap_vf, view_factor = view_factor[:sensor_count], view_factor[sensor_count:]
200
+ ap_view_factor[ap_id] = ap_vf
201
+
202
+ ap_view_factor_mean = OrderedDict()
203
+ # Get the mean view factor per sky patch for each aperture.
204
+ for ap_id, ap_vf in ap_view_factor.items():
205
+ ap_t = _transpose_matrix(ap_vf)
206
+ ap_view_factor_mean[ap_id] = \
207
+ [sum(sky_patch) / len(sky_patch) for sky_patch in ap_t]
208
+
209
+ if room_based: # Restructure ap_view_factor_mean.
210
+ _ap_view_factor_mean = {}
211
+ for room_id, data in room_apertures.items():
212
+ _ap_view_factor_mean[room_id] = OrderedDict()
213
+ for ap in data['apertures']:
214
+ ap_id = ap.identifier
215
+ _ap_view_factor_mean[room_id][ap_id] = ap_view_factor_mean[ap_id]
216
+ ap_view_factor_mean = _ap_view_factor_mean
217
+
218
+ # Calculate RMSE between all combinations of averaged aperture view factors.
219
+ if room_based:
220
+ rmse = OrderedDict()
221
+ for room_id, vf_matrix_dict in ap_view_factor_mean.items():
222
+ _rmse = _rmse_from_matrix(vf_matrix_dict)
223
+ rmse[room_id] = _rmse
224
+ else:
225
+ rmse = _rmse_from_matrix(ap_view_factor_mean)
226
+
227
+ return rmse
228
+
229
+
230
+ def cluster_view_factor(rmse, room_apertures, apertures, threshold,
231
+ room_based=True, vertical_tolerance=None):
232
+ # Cluster the apertures by the 'complete method'.
233
+ if room_based:
234
+ ap_groups = {}
235
+ for room_id, _rmse in rmse.items():
236
+ ap_groups[room_id] = {}
237
+ apertures = room_apertures[room_id]['apertures']
238
+ _room_ap_groups = \
239
+ _agglomerative_clustering_complete(_rmse, apertures, threshold)
240
+ # Flatten the groups. This will break the inter-cluster
241
+ # structure, but we do not need to know that.
242
+ grouped_apertures = [list(_flatten(cluster)) for cluster in _room_ap_groups]
243
+ if vertical_tolerance:
244
+ # Check groups by vertical tolerance.
245
+ vertical_groups = []
246
+ for ap_group in grouped_apertures:
247
+ vert_dist_matrix = []
248
+ for ap_1 in ap_group:
249
+ vert_dist_list = []
250
+ for ap_2 in ap_group:
251
+ vert_dist = abs(ap_1.center.z - ap_2.center.z)
252
+ vert_dist_list.append(vert_dist)
253
+ vert_dist_matrix.append(vert_dist_list)
254
+ _ap_groups = _agglomerative_clustering_complete(
255
+ vert_dist_matrix, ap_group, vertical_tolerance
256
+ )
257
+ _ap_groups = [list(_flatten(cluster)) for cluster in _ap_groups]
258
+ vertical_groups.extend(_ap_groups)
259
+ grouped_apertures = vertical_groups
260
+
261
+ ap_groups[room_id]['aperture_groups'] = grouped_apertures
262
+ ap_groups[room_id]['display_name'] = room_apertures[room_id]['display_name']
263
+ else:
264
+ ap_groups = _agglomerative_clustering_complete(rmse, apertures, threshold)
265
+ # Flatten the groups. This will break the inter-cluster structure,
266
+ # but we do not need to know that.
267
+ ap_groups = [list(_flatten(cluster)) for cluster in ap_groups]
268
+ if vertical_tolerance:
269
+ # Check groups by vertical tolerance.
270
+ vertical_groups = []
271
+ for ap_group in ap_groups:
272
+ vert_dist_matrix = []
273
+ for ap_1 in ap_group:
274
+ vert_dist_list = []
275
+ for ap_2 in ap_group:
276
+ vert_dist = abs(ap_1.center.z - ap_2.center.z)
277
+ vert_dist_list.append(vert_dist)
278
+ vert_dist_matrix.append(vert_dist_list)
279
+ _ap_groups = _agglomerative_clustering_complete(
280
+ vert_dist_matrix, ap_group, vertical_tolerance)
281
+ _ap_groups = [list(_flatten(cluster)) for cluster in _ap_groups]
282
+ vertical_groups.extend(_ap_groups)
283
+ ap_groups = vertical_groups
284
+
285
+ return ap_groups
286
+
287
+
288
+ def cluster_orientation(room_apertures, apertures, room_based=True, vertical_tolerance=None):
289
+ if room_based:
290
+ ap_groups = {}
291
+ for room_id, data in room_apertures.items():
292
+ _normal_list = []
293
+ grouped_apertures = []
294
+ ap_groups[room_id] = {}
295
+ for ap in data['apertures']:
296
+ # check if normal is already in list
297
+ n_bools = [ap.normal.is_equivalent(n, tolerance=0.05)
298
+ for n in _normal_list]
299
+ if not any(n_bools):
300
+ _normal_list.append(ap.normal)
301
+ # append empty list for new group
302
+ grouped_apertures.append([])
303
+ for idx, n in enumerate(_normal_list):
304
+ if n.is_equivalent(ap.normal, tolerance=0.05):
305
+ group_index = idx
306
+ grouped_apertures[group_index].append(ap)
307
+ if vertical_tolerance:
308
+ # Check groups by vertical tolerance.
309
+ vertical_groups = []
310
+ for ap_group in grouped_apertures:
311
+ vert_dist_matrix = []
312
+ for ap_1 in ap_group:
313
+ vert_dist_list = []
314
+ for ap_2 in ap_group:
315
+ vert_dist = abs(ap_1.center.z - ap_2.center.z)
316
+ vert_dist_list.append(vert_dist)
317
+ vert_dist_matrix.append(vert_dist_list)
318
+ _ap_groups = _agglomerative_clustering_complete(
319
+ vert_dist_matrix, ap_group, vertical_tolerance
320
+ )
321
+ _ap_groups = [list(_flatten(cluster)) for cluster in _ap_groups]
322
+ vertical_groups.extend(_ap_groups)
323
+ grouped_apertures = vertical_groups
324
+
325
+ ap_groups[room_id]['aperture_groups'] = grouped_apertures
326
+ ap_groups[room_id]['display_name'] = data['display_name']
327
+ else:
328
+ _normal_list = []
329
+ grouped_apertures = []
330
+ for ap in apertures:
331
+ # check if normal is already in list
332
+ n_bools = [ap.normal.is_equivalent(n, tolerance=0.05)
333
+ for n in _normal_list]
334
+ if not any(n_bools):
335
+ _normal_list.append(ap.normal)
336
+ # append empty list for new group
337
+ grouped_apertures.append([])
338
+ for idx, n in enumerate(_normal_list):
339
+ if n.is_equivalent(ap.normal, tolerance=0.05):
340
+ group_index = idx
341
+ grouped_apertures[group_index].append(ap)
342
+ ap_groups = grouped_apertures
343
+ if vertical_tolerance:
344
+ # Check groups by vertical tolerance.
345
+ vertical_groups = []
346
+ for ap_group in ap_groups:
347
+ vert_dist_matrix = []
348
+ for ap_1 in ap_group:
349
+ vert_dist_list = []
350
+ for ap_2 in ap_group:
351
+ vert_dist = abs(ap_1.center.z - ap_2.center.z)
352
+ vert_dist_list.append(vert_dist)
353
+ vert_dist_matrix.append(vert_dist_list)
354
+ _ap_groups = _agglomerative_clustering_complete(
355
+ vert_dist_matrix, ap_group, vertical_tolerance)
356
+ _ap_groups = [list(_flatten(cluster)) for cluster in _ap_groups]
357
+ vertical_groups.extend(_ap_groups)
358
+ ap_groups = vertical_groups
359
+
360
+ return ap_groups
361
+
362
+
363
+ def cluster_output(ap_groups, room_apertures, room_based=True):
364
+ # Add the aperture group to each aperture in the dictionary.
365
+ group_names = []
366
+ group_dict = {}
367
+ if room_based:
368
+ for room_id, data in ap_groups.items():
369
+ for idx, group in enumerate(data['aperture_groups']):
370
+ ap_ids = [ap.identifier for ap in group]
371
+ group_name = '{}_ApertureGroup_{}'.format(data['display_name'], idx)
372
+ group_name = clean_rad_string(group_name)
373
+ group_names.append(
374
+ {'identifier': group_name, 'apertures': ap_ids}
375
+ )
376
+ for ap_id in ap_ids:
377
+ group_dict[ap_id] = group_name
378
+ else:
379
+ for idx, group in enumerate(ap_groups):
380
+ ap_ids = [ap.identifier for ap in group]
381
+ group_name = 'ApertureGroup_{}'.format(idx)
382
+ group_name = clean_rad_string(group_name)
383
+ group_names.append(
384
+ {'identifier': group_name, 'apertures': ap_ids}
385
+ )
386
+ for ap_id in ap_ids:
387
+ group_dict[ap_id] = group_name
388
+
389
+ return group_names, group_dict
390
+
391
+
392
+ def automatic_aperture_grouping(
393
+ model, octree=None, rflux_sky=None, size=0.2, threshold=0.001, ambient_division=1000,
394
+ room_based=True, view_factor_or_orientation=True,
395
+ vertical_tolerance=None, states=None, working_folder=None
396
+ ):
397
+ """Automatically calculate aperture groups for exterior apertures.
398
+
399
+ This function calculates view factor from apertures to sky patches (rfluxmtx). Each
400
+ aperture is represented by a sensor grid, and the view factor for the whole aperture
401
+ is the average of the grid. The apertures are grouped based on the threshold.
402
+
403
+ Args:
404
+ model: A Honeybee Model object to which aperture groups will be assigned.
405
+ octree: Optional path to octree file to be used for view factor calculation.
406
+ If None, the octree will be created from the model.
407
+ rflux_sky: Optional path to an rflux sky file. If None, the rflux sky
408
+ file will be auto-created.
409
+ size: Aperture grid size. A lower number will give a finer grid and more
410
+ accurate results but the calculation time will increase. (Default: 0.2).
411
+ threshold: A number that determines if two apertures/aperture groups can
412
+ be clustered. A lower number is more accurate but will also increase
413
+ the number of aperture groups. (Default: 0.001).
414
+ ambient_division: Number of ambient divisions (-ad) for view factor
415
+ calculation in rfluxmtx. Increasing the number will give more accurate
416
+ results but also increase the calculation time. (Default: 1000).
417
+ room_based: Boolean to note whether the apertures should be grouped
418
+ on a room basis. If grouped on a room basis apertures from different
419
+ room cannot be in the same group. (Default: False).
420
+ view_factor_or_orientation: Boolean to note whether the apertures should
421
+ be grouped by calculating view factors for the apertures to a
422
+ discretized sky (True) or simply by the normal orientation of the
423
+ apertures (False). (Default: False).
424
+ vertical_tolerance: A float value for vertical tolerance between two apertures.
425
+ If the vertical distance between two apertures is larger than this
426
+ tolerance the apertures cannot be grouped. If None, the vertical
427
+ grouping will be skipped. (Default: None).
428
+ states: An optional list of Honeybee State objects to be applied to all
429
+ the generated groups. These states should be ordered based on how
430
+ they will be switched on. The first state is the default state and,
431
+ typically, higher states are more shaded. If the objects in the group
432
+ have no states, the modifiers already assigned the apertures will
433
+ be used for all states.
434
+ working_folder: Path to a folder into which the files be written. If None,
435
+ the files will be written into a folder called aperture_groups
436
+ within the default simulation folder.
437
+
438
+ Returns:
439
+ A Model with Aperture groups automatically assigned.
440
+ """
441
+ # serialize the model, set the output folder, and process simpler attributes
442
+ if working_folder is None:
443
+ working_folder = os.path.join(hb_folders.default_simulation_folder,
444
+ 'aperture_groups')
445
+ if not os.path.isdir(working_folder):
446
+ os.makedirs(working_folder)
447
+ view_factor = view_factor_or_orientation
448
+
449
+ # Get all room-based apertures with Outdoors boundary condition
450
+ apertures = []
451
+ room_apertures = {}
452
+ for room in model.rooms:
453
+ for face in room.faces:
454
+ for ap in face.apertures:
455
+ if isinstance(ap.boundary_condition, Outdoors):
456
+ apertures.append(ap)
457
+ if room.identifier not in room_apertures:
458
+ room_apertures[room.identifier] = {}
459
+ if 'apertures' not in room_apertures[room.identifier]:
460
+ room_apertures[room.identifier]['apertures'] = \
461
+ [ap]
462
+ else:
463
+ room_apertures[room.identifier]['apertures'].append(ap)
464
+ if 'display_name' not in room_apertures[room.identifier]:
465
+ room_apertures[room.identifier]['display_name'] = \
466
+ room.display_name
467
+
468
+ assert len(apertures) != 0, \
469
+ 'Found no Honeybee Apertures. There should at least be one Aperture ' \
470
+ 'in the model to compute aperture groups.'
471
+
472
+ if view_factor:
473
+ if not octree:
474
+ # write octree
475
+ model_content, modifier_content = model.to.rad(model, minimal=True)
476
+ scene_file, mat_file = 'scene.rad', 'scene.mat'
477
+ write_to_file_by_name(working_folder, scene_file, model_content)
478
+ write_to_file_by_name(working_folder, mat_file, modifier_content)
479
+ octree = 'scene.oct'
480
+ oconv = Oconv(inputs=[mat_file, scene_file], output=octree)
481
+ oconv.options.f = True
482
+
483
+ # run Oconv command
484
+ env = None
485
+ if folders.env != {}:
486
+ env = folders.env
487
+ env = dict(os.environ, **env) if env else None
488
+ oconv.run(env, cwd=working_folder)
489
+
490
+ if not rflux_sky:
491
+ rflux_sky = SkyDome()
492
+ rflux_sky = rflux_sky.to_file(working_folder, name='rflux_sky.sky')
493
+
494
+ # Calculate view factor.
495
+ mtx_file, ap_dict = aperture_view_factor(
496
+ working_folder, apertures, size=size, ambient_division=ambient_division,
497
+ receiver=rflux_sky, octree=octree, calc_folder=working_folder
498
+ )
499
+ rmse = aperture_view_factor_postprocess(
500
+ mtx_file, ap_dict, room_apertures, room_based
501
+ )
502
+
503
+ # cluster apertures into groups
504
+ if view_factor:
505
+ ap_groups = cluster_view_factor(
506
+ rmse, room_apertures, apertures, threshold, room_based, vertical_tolerance)
507
+ else:
508
+ ap_groups = cluster_orientation(
509
+ room_apertures, apertures, room_based, vertical_tolerance
510
+ )
511
+
512
+ # process clusters
513
+ group_names, group_dict = \
514
+ cluster_output(ap_groups, room_apertures, room_based)
515
+
516
+ # Write aperture groups to JSON file.
517
+ dyn_gr = os.path.join(working_folder, 'aperture_groups.json')
518
+ with open(dyn_gr, 'w') as fp:
519
+ json.dump(group_names, fp, indent=2)
520
+
521
+ # Write dynamic group identifiers to JSON file.
522
+ dyn_gr_ids = os.path.join(working_folder, 'dynamic_group_identifiers.json')
523
+ with open(dyn_gr_ids, 'w') as fp:
524
+ json.dump(group_dict, fp, indent=2)
525
+
526
+ # assign dynamic group identifiers for each aperture
527
+ group_ap_dict = {}
528
+ for room in model.rooms:
529
+ for face in room.faces:
530
+ for ap in face.apertures:
531
+ if isinstance(ap.boundary_condition, Outdoors):
532
+ dyn_group_id = group_dict[ap.identifier]
533
+ ap.properties.radiance.dynamic_group_identifier = \
534
+ dyn_group_id
535
+ try:
536
+ group_ap_dict[dyn_group_id].append(ap)
537
+ except KeyError:
538
+ group_ap_dict[dyn_group_id] = [ap]
539
+
540
+ # assign any states if they are connected
541
+ if states is not None and len(states) != 0:
542
+ for group_aps in group_ap_dict.values():
543
+ # assign states (including shades) to the first aperture
544
+ group_aps[0].properties.radiance.states = \
545
+ [state.duplicate() for state in states]
546
+ # remove shades from following apertures to ensure they aren't double-counted
547
+ states_wo_shades = []
548
+ for state in states:
549
+ new_state = state.duplicate()
550
+ new_state.remove_shades()
551
+ states_wo_shades.append(new_state)
552
+ for ap in group_aps[1:]:
553
+ ap.properties.radiance.states = \
554
+ [state.duplicate() for state in states_wo_shades]
555
+
556
+ # return the model
557
+ return model