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,1034 @@
1
+ """honeybee radiance ray-tracing command commands."""
2
+ import sys
3
+ import logging
4
+ import os
5
+ import traceback
6
+ import json
7
+ import math
8
+ from collections import OrderedDict
9
+ import click
10
+
11
+ from ladybug_geometry.geometry3d.mesh import Mesh3D
12
+ from honeybee.model import Model
13
+ from honeybee.aperture import Aperture
14
+ from honeybee.boundarycondition import Outdoors
15
+ from honeybee_radiance_folder import ModelFolder
16
+ from honeybee_radiance_folder.gridutil import redistribute_sensors
17
+ from honeybee_radiance_command.rfluxmtx import RfluxmtxOptions, Rfluxmtx
18
+
19
+ from honeybee_radiance.config import folders
20
+ from honeybee_radiance.reader import sensor_count_from_file
21
+ from honeybee_radiance.sensorgrid import SensorGrid
22
+ from honeybee_radiance.reader import parse_from_file
23
+ from honeybee_radiance.geometry.polygon import Polygon
24
+ from honeybee_radiance.dynamic.multiphase import automatic_aperture_grouping
25
+ from honeybee_radiance.dynamic import StateGeometry, RadianceSubFaceState
26
+ from honeybee_radiance.modifier.material.trans import Trans
27
+
28
+ from .octree import _generate_octrees_info
29
+ from .threephase import three_phase
30
+
31
+ _logger = logging.getLogger(__name__)
32
+
33
+
34
+ @click.group(help="Commands to run multi-phase operations in Radiance.")
35
+ def multi_phase():
36
+ pass
37
+
38
+
39
+ multi_phase.add_command(three_phase)
40
+
41
+
42
+ @multi_phase.command("view-matrix")
43
+ @click.argument(
44
+ "receiver-file", type=click.Path(exists=True, file_okay=True, resolve_path=True)
45
+ )
46
+ @click.argument(
47
+ "octree", type=click.Path(exists=True, file_okay=True, resolve_path=True)
48
+ )
49
+ @click.argument(
50
+ "sensor-grid", type=click.Path(exists=True, file_okay=True, resolve_path=True)
51
+ )
52
+ @click.option(
53
+ "--sensor-count",
54
+ type=click.INT,
55
+ show_default=True,
56
+ help="Number of sensors in sensor grid file. Number of sensors will be parsed form"
57
+ " the sensor file if not provided.",
58
+ )
59
+ @click.option("--rad-params", show_default=True, help="Radiance parameters.")
60
+ @click.option(
61
+ "--rad-params-locked",
62
+ show_default=True,
63
+ help="Protected Radiance parameters. "
64
+ "These values will overwrite user input rad parameters.",
65
+ )
66
+ @click.option(
67
+ "--output",
68
+ "-o",
69
+ show_default=True,
70
+ help="Path to output file. If a relative path"
71
+ " is provided it should be relative to project folder.",
72
+ )
73
+ @click.option(
74
+ "--dry-run",
75
+ is_flag=True,
76
+ default=False,
77
+ show_default=True,
78
+ help="A flag to show the command without running it.",
79
+ )
80
+ def view_matrix_command(
81
+ receiver_file,
82
+ octree,
83
+ sensor_grid,
84
+ sensor_count,
85
+ rad_params,
86
+ rad_params_locked,
87
+ output,
88
+ dry_run,
89
+ ):
90
+ """Calculate view matrix for a receiver file.
91
+
92
+ \b
93
+ Args:
94
+ receiver_file: Path to receiver file.
95
+ octree: Path to octree file.
96
+ sensor_grid: Path to sensor grid file.
97
+ """
98
+
99
+ try:
100
+ options = RfluxmtxOptions()
101
+ # parse input radiance parameters
102
+ if rad_params:
103
+ options.update_from_string(rad_params.strip())
104
+ # overwrite input values with protected ones
105
+ if rad_params_locked:
106
+ options.update_from_string(rad_params_locked.strip())
107
+
108
+ if not sensor_count:
109
+ sensor_count = sensor_count_from_file(sensor_grid)
110
+
111
+ options.update_from_string('-aa 0.0 -y {}'.format(sensor_count))
112
+
113
+ # create command.
114
+ rfluxmtx_cmd = Rfluxmtx(
115
+ options=options, output=output, octree=octree, sensors=sensor_grid,
116
+ receivers=receiver_file
117
+ )
118
+
119
+ if dry_run:
120
+ click.echo(rfluxmtx_cmd)
121
+ sys.exit(0)
122
+
123
+ if output:
124
+ parent = os.path.dirname(output)
125
+ if not os.path.isdir(parent):
126
+ os.mkdir(parent)
127
+
128
+ if options.o.value is not None:
129
+ parent = os.path.dirname(options.o.value)
130
+ if not os.path.isdir(parent):
131
+ os.mkdir(parent)
132
+
133
+ env = None
134
+ if folders.env != {}:
135
+ env = folders.env
136
+ env = dict(os.environ, **env) if env else None
137
+ rfluxmtx_cmd.run(env=env)
138
+
139
+ except Exception:
140
+ _logger.exception("Failed to run view-matrix command.")
141
+ traceback.print_exc()
142
+ sys.exit(1)
143
+ else:
144
+ sys.exit(0)
145
+
146
+
147
+ @multi_phase.command("flux-transfer")
148
+ @click.argument(
149
+ "sender-file", type=click.Path(exists=True, file_okay=True, resolve_path=True)
150
+ )
151
+ @click.argument(
152
+ "receiver-file", type=click.Path(exists=True, file_okay=True, resolve_path=True)
153
+ )
154
+ @click.argument(
155
+ "octree", type=click.Path(exists=True, file_okay=True, resolve_path=True)
156
+ )
157
+ @click.option("--rad-params", show_default=True, help="Radiance parameters.")
158
+ @click.option(
159
+ "--rad-params-locked",
160
+ show_default=True,
161
+ help="Protected Radiance parameters. "
162
+ "These values will overwrite user input rad parameters.",
163
+ )
164
+ @click.option(
165
+ "--output",
166
+ "-o",
167
+ show_default=True,
168
+ help="Path to output file. If a relative path"
169
+ " is provided it should be relative to project folder.",
170
+ )
171
+ @click.option(
172
+ "--dry-run",
173
+ is_flag=True,
174
+ default=False,
175
+ show_default=True,
176
+ help="A flag to show the command without running it.",
177
+ )
178
+ def flux_transfer_command(
179
+ sender_file,
180
+ receiver_file,
181
+ octree,
182
+ rad_params,
183
+ rad_params_locked,
184
+ output,
185
+ dry_run,
186
+ ):
187
+ """Calculate flux transfer matrix for a sender file per receiver.
188
+
189
+ This command calculates a flux transfer matrix for the given sender and receiver
190
+ files. This can be used to calculate a flux transfer matrix for input and output
191
+ apertures on a light pipe, or a flux transfer matrix from an aperture to a
192
+ discretized sky (daylight matrix).
193
+
194
+ \b
195
+ Args:
196
+ sender_file: Path to sender file. The controlling parameters in the sender file
197
+ must follow the form: #@rfluxmtx variable=value. At minimum it must specify
198
+ a hemisphere sampling type. If the command is used to calculate e.g. daylight
199
+ matrix the sender file represents an aperture or multiple apertures.
200
+ receiver_file: Path to receiver file. The controlling parameters in the receiver
201
+ file must follow the form: #@rfluxmtx variable=value. At minimum it must
202
+ specify a hemisphere sampling type. If the command is used to calculate e.g.
203
+ daylight matrix the receiver file represents the ground and sky.
204
+ octree: Path to octree file.
205
+ """
206
+
207
+ try:
208
+ options = RfluxmtxOptions()
209
+ # parse input radiance parameters
210
+ if rad_params:
211
+ options.update_from_string(rad_params.strip())
212
+ # overwrite input values with protected ones
213
+ if rad_params_locked:
214
+ options.update_from_string(rad_params_locked.strip())
215
+
216
+ options.update_from_string('-aa 0.0')
217
+
218
+ # create command.
219
+ rfluxmtx_cmd = Rfluxmtx(
220
+ options=options, output=output, octree=octree, sender=sender_file,
221
+ receivers=receiver_file
222
+ )
223
+
224
+ if dry_run:
225
+ click.echo(rfluxmtx_cmd)
226
+ sys.exit(0)
227
+
228
+ env = None
229
+ if folders.env != {}:
230
+ env = folders.env
231
+ env = dict(os.environ, **env) if env else None
232
+ rfluxmtx_cmd.run(env=env)
233
+
234
+ except Exception:
235
+ _logger.exception("Failed to run flux-transfer command.")
236
+ traceback.print_exc()
237
+ sys.exit(1)
238
+ else:
239
+ sys.exit(0)
240
+
241
+
242
+ @multi_phase.command("dmtx-group")
243
+ @click.argument("folder", type=click.STRING)
244
+ @click.argument(
245
+ "octree", type=click.Path(exists=True, file_okay=True, resolve_path=True)
246
+ )
247
+ @click.argument(
248
+ "rflux_sky", type=click.Path(exists=True, file_okay=True, resolve_path=True)
249
+ )
250
+ @click.option(
251
+ "--name", help="Name of output JSON file.", show_default=True,
252
+ default='dmx_aperture_groups'
253
+ )
254
+ @click.option(
255
+ "--size", "-s", type=float, default=0.2, show_default=True,
256
+ help="Aperture grid size. A lower number will give a finer grid and more accurate"
257
+ " results but the calculation time will increase.")
258
+ @click.option(
259
+ "--threshold", "-t", type=float, default=0.001, show_default=True,
260
+ help="A number that determines if two apertures/aperture groups can be clustered. A"
261
+ " higher number is more accurate but will also increase the number of aperture groups.")
262
+ @click.option(
263
+ "--ambient-division", "-ad", type=int, default=1000, show_default=True,
264
+ help="Number of ambient divisions (-ad) for view factor calculation in rfluxmtx."
265
+ " Increasing the number will give more accurate results but also increase the"
266
+ " calculation time.")
267
+ @click.option(
268
+ "--output-folder", help="Output folder into which the files be written.",
269
+ default="dmtx_aperture_groups", show_default=True)
270
+ def dmtx_group_command(
271
+ folder,
272
+ octree,
273
+ rflux_sky,
274
+ name,
275
+ size,
276
+ threshold,
277
+ ambient_division,
278
+ output_folder,
279
+ ):
280
+ """Calculate aperture groups for daylight matrix purposes.
281
+ This command calculates view factor from apertures to sky patches (rfluxmtx). Each
282
+ aperture is represented by a sensor grid, and the view factor for the whole aperture
283
+ is the average of the grid. The apertures are grouped based on the threshold.
284
+
285
+ \b
286
+ Args:
287
+ folder: Path to a Radiance model folder.
288
+ octree: Path to octree file.
289
+ rflux_sky: Path to rflux sky file.
290
+ """
291
+
292
+ def _index_and_min(distance_matrix):
293
+ """Return the minimum value of the distance matrix, as well as the index [j, i] of
294
+ the minimum value of the distance matrix."""
295
+ min_value = min([min(sublist) for sublist in distance_matrix])
296
+ for i, _i in enumerate(distance_matrix):
297
+ for j, _j in enumerate(distance_matrix):
298
+ if distance_matrix[i][j] == min_value:
299
+ index = [j, i]
300
+ break
301
+ return min_value, index
302
+
303
+ def _pairwise_maximum(array1, array2):
304
+ """Return an array of the pairwise maximum of two arrays."""
305
+ pair_array = [array1, array2]
306
+ max_array = list(map(max, zip(*pair_array)))
307
+ return max_array
308
+
309
+ def _tranpose_matrix(matrix):
310
+ """Transposes the distance matrix."""
311
+ matrix = list(map(list, zip(*matrix)))
312
+ return matrix
313
+
314
+ def _rmse_from_matrix(input):
315
+ """Calculates RMSE."""
316
+ rmse = []
317
+ for i, predicted in enumerate(input):
318
+ r_list = []
319
+ for j, observed in enumerate(input):
320
+ error = [(p - o) for p, o in zip(predicted, observed)]
321
+ square_error = [e ** 2 for e in error]
322
+ mean_square_error = sum(square_error) / len(square_error)
323
+ root_mean_square_error = mean_square_error ** 0.5
324
+ r_list.append(root_mean_square_error)
325
+ rmse.append(r_list)
326
+ return rmse
327
+
328
+ def _flatten(container):
329
+ """Flatten an array."""
330
+ if not isinstance(container, list):
331
+ container = [container]
332
+ for i in container:
333
+ if isinstance(i, (list, tuple)):
334
+ for j in _flatten(i):
335
+ yield j
336
+ else:
337
+ yield i
338
+
339
+ def _agglomerative_clustering_complete(distance_matrix, ap_name, threshold=0.001):
340
+ """Cluster apertures based on the threshold."""
341
+
342
+ # Fill the diagonal with 9999 so a diagonal of zeros will NOT be stored as min_value.
343
+ for i in range(len(distance_matrix)):
344
+ distance_matrix[i][i] = 9999
345
+
346
+ # Create starting list of aperture groups. Each aperture starts as its own group.
347
+ ap_groups = ap_name
348
+
349
+ # Set the number of samples and the minimum value of the distance matrix.
350
+ n_samples = len(distance_matrix)
351
+
352
+ # Set the minimum value of the distance matrix and find the indices of the minimum
353
+ # value in the distance matrix.
354
+ min_value, index = _index_and_min(distance_matrix)
355
+
356
+ while n_samples > 1 and min_value < threshold:
357
+ # Combine the two groups and place it at index 0, and remove item at index 1.
358
+ ap_groups[index[0]] = [ap_groups[index[0]], ap_groups[index[1]]]
359
+ ap_groups.pop(index[1])
360
+
361
+ # Update the values in the distance matrix. We need the maximum values between
362
+ # the new cluster and all the remaining apertures or clusters still in the
363
+ # distance matrix.
364
+ distance_matrix[index[0]] = \
365
+ _pairwise_maximum(distance_matrix[index[0]], distance_matrix[index[1]])
366
+ distance_matrix = _tranpose_matrix(distance_matrix)
367
+ distance_matrix[index[0]] = \
368
+ _pairwise_maximum(distance_matrix[index[0]], distance_matrix[index[1]])
369
+
370
+ # Remove the values at index 1 along both axes.
371
+ distance_matrix.pop(index[1])
372
+ distance_matrix = _tranpose_matrix(distance_matrix)
373
+ distance_matrix.pop(index[1])
374
+
375
+ # Update the number of samples that are left in the distance matrix.
376
+ n_samples -= 1
377
+ # Update the minimum value and the indices.
378
+ min_value, index = _index_and_min(distance_matrix)
379
+
380
+ return ap_groups
381
+
382
+ def _aperture_view_factor(
383
+ project_folder, apertures, size=0.2, ambient_division=1000,
384
+ receiver='rflux_sky.sky', octree='scene.oct',
385
+ calc_folder='dmtx_aperture_grouping'):
386
+ """Calculates the view factor for each aperture by sensor points."""
387
+
388
+ # Instantiate dictionary that will store the sensor count for each aperture. We need
389
+ # a OrderedDict so that we can split the rfluxmtx output file by each aperture
390
+ # (sensor count) in the correct order.
391
+ ap_dict = OrderedDict()
392
+
393
+ meshes = []
394
+ # Create a mesh for each aperture and add the the sensor count to dict.
395
+ for aperture in apertures:
396
+ ap_mesh = aperture.geometry.mesh_grid(size, flip=True, generate_centroids=False)
397
+ meshes.append(ap_mesh)
398
+ ap_dict[aperture.display_name] = {'sensor_count': len(ap_mesh.faces)}
399
+
400
+ # Create a sensor grid from joined aperture mesh.
401
+ grid_mesh = SensorGrid.from_mesh3d('aperture_grid', Mesh3D.join_meshes(meshes))
402
+
403
+ # Write sensor grid to pts file.
404
+ sensors = grid_mesh.to_file(os.path.join(project_folder, calc_folder),
405
+ file_name='apertures')
406
+
407
+ # rfluxmtx options
408
+ rfluxOpt = RfluxmtxOptions()
409
+ rfluxOpt.ad = ambient_division
410
+ rfluxOpt.lw = 1.0 / float(rfluxOpt.ad)
411
+ rfluxOpt.I = True
412
+ rfluxOpt.h = True
413
+
414
+ # rfluxmtx command
415
+ rflux = Rfluxmtx()
416
+ rflux.options = rfluxOpt
417
+ rflux.receivers = receiver
418
+ rflux.sensors = sensors
419
+ rflux.octree = octree
420
+ rflux.output = os.path.join(calc_folder, 'apertures_vf.mtx')
421
+
422
+ # Run rfluxmtx command
423
+ env = None
424
+ if folders.env != {}:
425
+ env = folders.env
426
+ env = dict(os.environ, **env) if env else None
427
+ rflux.run(env=env, cwd=project_folder)
428
+
429
+ # Get the output file of the rfluxmtx command.
430
+ mtx_file = os.path.join(project_folder, rflux.output)
431
+
432
+ return mtx_file, ap_dict
433
+
434
+ try:
435
+ model_folder = ModelFolder.from_model_folder(folder)
436
+
437
+ apertures = []
438
+ states = model_folder.aperture_groups_states(full=True)
439
+ ap_group_folder = model_folder.aperture_group_folder(full=True)
440
+ for ap_group in states.keys():
441
+ if 'dmtx' in states[ap_group][0]:
442
+ mtx_file = os.path.join(ap_group_folder,
443
+ os.path.basename(states[ap_group][0]['dmtx']))
444
+ polygon_string = parse_from_file(mtx_file)
445
+ polygon = Polygon.from_string('\n'.join(polygon_string))
446
+ apertures.append(Aperture.from_vertices(ap_group, polygon.vertices))
447
+
448
+ assert len(apertures) != 0, \
449
+ 'Found no valid dynamic apertures. There should at least be one aperture ' \
450
+ 'with transmittance matrix in your model.'
451
+
452
+ # Calculate view factor.
453
+ mtx_file, ap_dict = _aperture_view_factor(
454
+ model_folder.folder, apertures, size=size, ambient_division=ambient_division,
455
+ receiver=rflux_sky, octree=octree, calc_folder=output_folder
456
+ )
457
+
458
+ view_factor = []
459
+ # Read view factor file, convert to one channel output, and divide by Pi.
460
+ with open(mtx_file) as mtx_data:
461
+ for sensor in mtx_data:
462
+ sensor_split = sensor.strip().split()
463
+ if len(sensor_split) % 3 == 0:
464
+ one_channel = sensor_split[::3]
465
+
466
+ def convert_to_vf(x):
467
+ return float(x) / math.pi
468
+ view_factor.append(list(map(convert_to_vf, one_channel)))
469
+
470
+ ap_view_factor = []
471
+ # Split the view factor file by the aperture sensor count.
472
+ for aperture in ap_dict.values():
473
+ sensor_count = aperture['sensor_count']
474
+ ap_vf, view_factor = view_factor[:sensor_count], view_factor[sensor_count:]
475
+ ap_view_factor.append(ap_vf)
476
+
477
+ ap_view_factor_mean = []
478
+ # Get the mean view factor per sky patch for each aperture.
479
+ for aperture in ap_view_factor:
480
+ ap_t = _tranpose_matrix(aperture)
481
+ ap_view_factor_mean.append(
482
+ [sum(sky_patch) / len(sky_patch) for sky_patch in ap_t])
483
+
484
+ # Calculate RMSE between all combinations of averaged aperture view factors.
485
+ rmse = _rmse_from_matrix(ap_view_factor_mean)
486
+
487
+ ap_name = list(ap_dict.keys())
488
+ # Cluster the apertures by the 'complete method'.
489
+ ap_groups = _agglomerative_clustering_complete(rmse, ap_name, threshold)
490
+
491
+ # Flatten the groups. This will break the intercluster structure, but we do not need
492
+ # to know that.
493
+ ap_groups = [list(_flatten(cluster)) for cluster in ap_groups]
494
+
495
+ # Add the aperture group to each aperture in the dictionary and write the aperture
496
+ # group rad files.
497
+ group_names = []
498
+ groups_folder = os.path.join(
499
+ model_folder.folder, output_folder, 'groups'
500
+ )
501
+ if not os.path.isdir(groups_folder):
502
+ os.mkdir(groups_folder)
503
+ for idx, group in enumerate(ap_groups):
504
+ group_name = "group_{}".format(idx)
505
+ group_file = os.path.join(groups_folder, group_name + '.rad')
506
+ xform = []
507
+ group_names.append(
508
+ {'identifier': group_name, 'aperture_groups': group}
509
+ )
510
+
511
+ for ap in group:
512
+ xform.append("!xform ./model/aperture_group/{}..mtx.rad".format(ap))
513
+
514
+ with open(group_file, "w") as file:
515
+ file.write('\n'.join(xform))
516
+
517
+ # Write aperture dictionary to json file.
518
+ output = os.path.join(
519
+ model_folder.folder, output_folder, 'groups', '%s.json' % name
520
+ )
521
+ with open(output, 'w') as fp:
522
+ json.dump(group_names, fp, indent=2)
523
+
524
+ except Exception:
525
+ _logger.exception("Failed to run dmtx-group command.")
526
+ traceback.print_exc()
527
+ sys.exit(1)
528
+ else:
529
+ sys.exit(0)
530
+
531
+
532
+ @multi_phase.command('prepare-multiphase')
533
+ @click.argument(
534
+ 'folder', type=click.Path(exists=True, file_okay=False, dir_okay=True))
535
+ @click.argument('grid-count', type=int)
536
+ @click.option(
537
+ '--grid-divisor', '-d', help='An optional integer to be divided by the '
538
+ 'grid-count to yield a final number of grids to generate. This is useful '
539
+ 'in workflows where the grid-count is being interpreted as a cpu-count '
540
+ 'but there are multiple processors acting on a single grid. To ignore '
541
+ 'this limitation set the value to 1. Default: 1.', type=int, default=1)
542
+ @click.option(
543
+ '--min-sensor-count', '-msc', help='Minimum number of sensors in each '
544
+ 'output grid. Use this number to ensure the number of sensors in output '
545
+ 'grids never gets very small. This input will override the input '
546
+ 'grid-count when specified. To ignore this limitation, set the value to '
547
+ '1. Default: 1.', type=int, default=1)
548
+ @click.option(
549
+ '--sun-path',
550
+ type=click.Path(
551
+ exists=True, file_okay=True, dir_okay=False, resolve_path=False),
552
+ default=None, show_default=True,
553
+ help='Path for a sun-path file that will be added to octrees for direct '
554
+ 'sunlight studies. If sunpath is provided an extra octree for direct_sun '
555
+ 'will be created.'
556
+ )
557
+ @click.option(
558
+ '--phase',
559
+ type=click.Choice(['2', '3', '5']), default='5', show_default=True,
560
+ help='Select a multiphase study for which octrees will be created. 3-phase '
561
+ 'includes 2-phase, and 5-phase includes 3-phase and 2-phase.'
562
+ )
563
+ @click.option(
564
+ '--octree-folder', default='octree', show_default=True,
565
+ help='Output folder into which the octree files be written.')
566
+ @click.option(
567
+ '--grid-folder', default='grid', show_default=True,
568
+ help='Output folder into which the grid files be written.')
569
+ @click.option(
570
+ '--exclude-static/--include-static',
571
+ is_flag=True, default=True, show_default=True,
572
+ help='A flag to indicate if static apertures should be excluded or '
573
+ 'included. If excluded static apertures will not be treated as its own '
574
+ 'dynamic state.'
575
+ )
576
+ @click.option(
577
+ '--default-states/--all-states',
578
+ is_flag=True, default=False, show_default=True,
579
+ help='A flag to indicate if the command should generate octrees and grids '
580
+ 'for all aperture group states or just the default states of aperture '
581
+ 'groups.'
582
+ )
583
+ def prepare_multiphase_command(
584
+ folder, grid_count, grid_divisor, min_sensor_count, sun_path, phase,
585
+ octree_folder, grid_folder, exclude_static, default_states
586
+ ):
587
+ """This command prepares the model folder for simulations with aperture
588
+ groups. It will generate a set of octrees and sensor grids that are unique
589
+ to each state of each aperture group.
590
+
591
+ This command will generate octrees for both default and direct studies for
592
+ aperture groups, creating one octree for each light path, i.e., all other
593
+ light paths are blacked.
594
+
595
+ Sensor grids will be redistributed if they are to be used in a two phase
596
+ simulation. A subfolder for each light path will be created. In this folder
597
+ the redistributed grids are found.
598
+
599
+ If the model folder have aperture groups, a file with states information
600
+ for each grid will be written.
601
+
602
+ \b
603
+ Args:
604
+ folder: Path to a Radiance model folder.
605
+ grid_count: Number of output sensor grids to be created. This number
606
+ is usually equivalent to the number of processes that will be used
607
+ to run the simulations in parallel.
608
+ """
609
+ model_folder = ModelFolder.from_model_folder(folder)
610
+
611
+ # check if sunpath file exist - otherwise continue without it
612
+ if sun_path and not os.path.isfile(sun_path):
613
+ sun_path = None
614
+
615
+ phase = int(phase)
616
+ if phase == 5 and not sun_path:
617
+ raise RuntimeError(
618
+ 'To generate octrees for a 5 Phase study you must provide a '
619
+ 'sunpath.'
620
+ )
621
+
622
+ phases = {
623
+ 2: ['two_phase'],
624
+ 3: ['two_phase', 'three_phase'],
625
+ 5: ['two_phase', 'three_phase', 'five_phase']
626
+ }
627
+
628
+ def _get_grid_states(model_folder):
629
+ states_info = model_folder.aperture_groups_states()
630
+ grid_info = model_folder.grid_info()
631
+ grid_states = {}
632
+
633
+ for grid in grid_info:
634
+ grid_states[grid['full_id']] = {}
635
+ try:
636
+ light_paths = grid['light_path']
637
+ except KeyError:
638
+ light_paths = []
639
+ for light_path in light_paths:
640
+ for elem in light_path:
641
+ if elem != '__static_apertures__':
642
+ grid_states[grid['full_id']][elem] = \
643
+ [s['identifier'] for s in states_info[elem]]
644
+
645
+ grid_states_output = \
646
+ os.path.join(model_folder.folder, 'grid_states.json')
647
+ with open(grid_states_output, 'w') as fp:
648
+ json.dump(grid_states, fp, indent=2)
649
+
650
+ def _get_octrees_and_grids(
651
+ model_folder, grid_count, phase, octree_folder, grid_folder,
652
+ exclude_static, default_states
653
+ ):
654
+ scene_mapping = model_folder.octree_scene_mapping(
655
+ exclude_static=exclude_static, phase=phase,
656
+ default_states=default_states
657
+ )
658
+ grid_mapping = model_folder.grid_mapping(
659
+ exclude_static=exclude_static, phase=phase
660
+ )
661
+ if not os.path.isdir(octree_folder):
662
+ os.mkdir(octree_folder)
663
+ dynamic_mapping = {
664
+ 'two_phase': [],
665
+ 'three_phase': [],
666
+ 'five_phase': []
667
+ }
668
+ for study, states in scene_mapping.items():
669
+ if study == 'two_phase':
670
+ grid_info_dict = {}
671
+
672
+ if not os.path.isdir(grid_folder):
673
+ os.mkdir(grid_folder)
674
+
675
+ grid_count = int(grid_count / grid_divisor)
676
+ grid_count = 1 if grid_count < 1 else grid_count
677
+
678
+ for light_path in grid_mapping['two_phase']:
679
+ grid_info = light_path['grid']
680
+ output_folder = \
681
+ os.path.join(grid_folder, light_path['identifier'])
682
+ _grid_count, _sensor_per_grid, out_grid_info = \
683
+ redistribute_sensors(model_folder.grid_folder(),
684
+ output_folder, grid_count,
685
+ min_sensor_count,
686
+ grid_info=grid_info)
687
+ grid_info_dict[light_path['identifier']] = out_grid_info
688
+
689
+ for state in states:
690
+ light_path = state['light_path']
691
+ if light_path not in grid_info_dict:
692
+ if state['identifier'] == '__three_phase__':
693
+ pass
694
+ else:
695
+ # in this case we do not want to generate an octree for
696
+ # this state
697
+ continue
698
+ info, commands = _generate_octrees_info(
699
+ state, octree_folder, study, sun_path
700
+ )
701
+ for cmd in commands:
702
+ env = None
703
+ if folders.env:
704
+ env = folders.env
705
+ env = dict(os.environ, **env) if env else None
706
+ cmd.run(env=env, cwd=model_folder.folder)
707
+
708
+ # add grid information and folder if two_phase
709
+ if study == 'two_phase':
710
+ info['sensor_grids_folder'] = light_path
711
+ info['sensor_grids_info'] = grid_info_dict[light_path]
712
+
713
+ dynamic_mapping[study].append(info)
714
+
715
+ for study, study_info in dynamic_mapping.items():
716
+ dynamic_output = os.path.join(model_folder.folder, '%s.json' % study)
717
+ with open(dynamic_output, 'w') as fp:
718
+ json.dump(study_info, fp, indent=2)
719
+
720
+ dynamic_output = os.path.join(model_folder.folder, 'multi_phase.json')
721
+ with open(dynamic_output, 'w') as fp:
722
+ json.dump(dynamic_mapping, fp, indent=2)
723
+
724
+ try:
725
+ if model_folder.has_aperture_group or not exclude_static:
726
+ _get_octrees_and_grids(
727
+ model_folder, grid_count, phase, octree_folder, grid_folder,
728
+ exclude_static, default_states
729
+ )
730
+ _get_grid_states(model_folder=model_folder)
731
+ else:
732
+ # no aperture groups and static excluded, write empty files
733
+ dynamic_mapping = []
734
+ for study in phases[phase]:
735
+ study_type = []
736
+ dynamic_mapping.append({study: study_type})
737
+ dynamic_output = os.path.join(model_folder.folder, '%s.json' % study)
738
+ with open(dynamic_output, 'w') as fp:
739
+ json.dump(study_type, fp, indent=2)
740
+
741
+ dynamic_output = \
742
+ os.path.join(model_folder.folder, 'multi_phase.json')
743
+ with open(dynamic_output, 'w') as fp:
744
+ json.dump(dynamic_mapping, fp, indent=2)
745
+
746
+ except Exception:
747
+ _logger.exception('Failed to generate octrees and grids.')
748
+ sys.exit(1)
749
+ else:
750
+ sys.exit(0)
751
+
752
+
753
+ @multi_phase.command('aperture-group')
754
+ @click.argument('model-file', type=click.Path(
755
+ exists=True, file_okay=True, dir_okay=False, resolve_path=True))
756
+ @click.option(
757
+ '--octree', type=click.Path(exists=True, file_okay=True, resolve_path=True),
758
+ help='Path to octree file. The octree will be created from the model-file '
759
+ 'if no file is provided.', default=None
760
+ )
761
+ @click.option(
762
+ '--rflux-sky', type=click.Path(exists=True, file_okay=True, resolve_path=True),
763
+ help='Path to rflux sky file. The rflux sky file will be created if no file '
764
+ 'is provided.', default=None
765
+ )
766
+ @click.option(
767
+ '--size', '-s', type=float, default=0.2, show_default=True,
768
+ help='Aperture grid size. A lower number will give a finer grid and more accurate '
769
+ 'results but the calculation time will increase.')
770
+ @click.option(
771
+ '--threshold', '-t', type=float, default=0.001, show_default=True,
772
+ help='A number that determines if two apertures/aperture groups can be clustered. A '
773
+ 'lower number is more accurate but will also increase the number of aperture groups.')
774
+ @click.option(
775
+ '--ambient-division', '-ad', type=int, default=1000, show_default=True,
776
+ help='Number of ambient divisions (-ad) for view factor calculation in rfluxmtx. '
777
+ 'Increasing the number will give more accurate results but also increase the '
778
+ 'calculation time.')
779
+ @click.option(
780
+ '--room-based/--no-room-based', '-rb/-nrb', help='Flag to note '
781
+ 'whether the apertures should be grouped on a room basis. If grouped on a room '
782
+ 'basis apertures from different room cannot be in the same group.',
783
+ default=True, show_default=True)
784
+ @click.option(
785
+ '--view-factor/--orientation', '-vf/-or', help='Flag to note '
786
+ 'whether the apertures should be grouped by calculating view factors for '
787
+ 'the apertures to a discretized sky or simply by the normal orientation of '
788
+ 'the apertures.',
789
+ default=True, show_default=True)
790
+ @click.option(
791
+ '--vertical-tolerance', '-vt', type=click.FLOAT, default=None,
792
+ show_default=True, help='A float value for vertical tolerance between two '
793
+ 'apertures. If the vertical distance between two apertures is larger than '
794
+ 'this tolerance the apertures cannot be grouped. If no value is given the '
795
+ 'vertical grouping will be skipped.')
796
+ @click.option(
797
+ '--output-folder', help='Output folder into which the files be written.',
798
+ default='.', show_default=True,
799
+ type=click.Path(file_okay=False, dir_okay=True, resolve_path=True))
800
+ @click.option(
801
+ '--output-model', help='Optional file to output the string of the model '
802
+ 'with aperture groups assigned. By default, it will be printed out to stdout.',
803
+ type=click.File('w'), default='-', show_default=True)
804
+ def aperture_group_cli(
805
+ model_file, octree, rflux_sky, size, threshold, ambient_division,
806
+ room_based, view_factor, vertical_tolerance, output_folder, output_model
807
+ ):
808
+ """Calculate aperture groups for exterior apertures.
809
+
810
+ This command calculates view factor from apertures to sky patches (rfluxmtx). Each
811
+ aperture is represented by a sensor grid, and the view factor for the whole aperture
812
+ is the average of the grid. The apertures are grouped based on the threshold.
813
+
814
+ \b
815
+ Args:
816
+ model_file: Full path to a Model JSON file (HBJSON) or a Model pkl (HBpkl) file.
817
+ """
818
+ try:
819
+ # process all of the CLI input so that it can be passed to the function
820
+ no_room_based = not room_based
821
+ orientation = not view_factor
822
+
823
+ # pass the input to the function
824
+ aperture_group(model_file, octree, rflux_sky, size, threshold, ambient_division,
825
+ no_room_based, orientation, vertical_tolerance,
826
+ output_folder, output_model)
827
+ except Exception:
828
+ _logger.exception("Failed to run aperture-group command.")
829
+ traceback.print_exc()
830
+ sys.exit(1)
831
+ else:
832
+ sys.exit(0)
833
+
834
+
835
+ def aperture_group(
836
+ model_file, octree=None, rflux_sky=None,
837
+ size=0.2, threshold=0.001, ambient_division=1000,
838
+ no_room_based=False, orientation=False, vertical_tolerance=None,
839
+ output_folder=None, output_model=None,
840
+ room_based=True, view_factor=True
841
+ ):
842
+ """Automatically calculate aperture groups for exterior apertures.
843
+
844
+ This function calculates view factor from apertures to sky patches (rfluxmtx). Each
845
+ aperture is represented by a sensor grid, and the view factor for the whole aperture
846
+ is the average of the grid. The apertures are grouped based on the threshold.
847
+
848
+ Args:
849
+ model_file: Full path to a Model JSON file (HBJSON) or a Model pkl (HBpkl) file.
850
+ octree: Path to octree file. The octree will be created from the model
851
+ file if no file is provided.
852
+ rflux_sky: Path to rflux sky file. The rflux sky file will be created
853
+ if no file is provided.
854
+ size: Aperture grid size. A lower number will give a finer grid and more
855
+ accurate results but the calculation time will increase. (Default: 0.2).
856
+ threshold: A number that determines if two apertures/aperture groups can
857
+ be clustered. A lower number is more accurate but will also increase
858
+ the number of aperture groups. (Default: 0.001).
859
+ ambient_division: Number of ambient divisions (-ad) for view factor
860
+ calculation in rfluxmtx. Increasing the number will give more accurate
861
+ results but also increase the calculation time. (Default: 1000).
862
+ no_room_based: Boolean to note whether the apertures should be grouped
863
+ on a room basis. If grouped on a room basis apertures from different
864
+ room cannot be in the same group. (Default: False).
865
+ orientation: Boolean to note whether the apertures should be grouped by
866
+ calculating view factors for the apertures to a discretized sky or
867
+ simply by the normal orientation of the apertures. (Default: False).
868
+ vertical_tolerance: A float value for vertical tolerance between two apertures.
869
+ If the vertical distance between two apertures is larger than this
870
+ tolerance the apertures cannot be grouped. If None, the vertical
871
+ grouping will be skipped. (Default: None).
872
+ output_folder: Output folder into which the files be written. If None,
873
+ the files will be written into a folder called aperture_groups
874
+ within the default simulation folder.
875
+ output_model: Optional file to output the JSON string of the Model with
876
+ aperture groups set. If None, the string will simply be returned from
877
+ this method.
878
+ """
879
+ # serialize the model and process simpler attributes
880
+ model = Model.from_file(model_file)
881
+ room_based = not no_room_based
882
+ view_factor = not orientation
883
+
884
+ # perform the automatic aperture grouping
885
+ model = automatic_aperture_grouping(
886
+ model, octree, rflux_sky, size, threshold, ambient_division,
887
+ room_based, view_factor, vertical_tolerance, working_folder=output_folder)
888
+
889
+ # return the more with the dynamic groups
890
+ if output_model is None:
891
+ return json.dumps(model.to_dict())
892
+ elif isinstance(output_model, str):
893
+ with open(output_model, 'w') as of:
894
+ of.write(json.dumps(model.to_dict()))
895
+ else:
896
+ output_model.write(json.dumps(model.to_dict()))
897
+
898
+
899
+ @multi_phase.command('add-aperture-group-blinds')
900
+ @click.argument('model-file', type=click.Path(
901
+ exists=True, file_okay=True, dir_okay=False, resolve_path=True)
902
+ )
903
+ @click.option(
904
+ '--diffuse-transmission', '-dt',
905
+ help='Diffuse transmission of the aperture group blinds. Default is 0.05 '
906
+ '(5%).',
907
+ default=0.05, type=float, show_default=True
908
+ )
909
+ @click.option(
910
+ '--specular-transmission', '-dt',
911
+ help='Specular transmission of the aperture group blinds. Default is 0 '
912
+ '(0%).',
913
+ default=0, type=float, show_default=True
914
+ )
915
+ @click.option(
916
+ '--distance', '-d',
917
+ help='Distance from the aperture parent surface to the blind surface.',
918
+ default=0.001, type=float, show_default=True
919
+ )
920
+ @click.option(
921
+ '--scale', '-s',
922
+ help='Scaling value to scale blind geometry at the center point of the '
923
+ 'aperture.',
924
+ default=1.001, type=float, show_default=True
925
+ )
926
+ @click.option(
927
+ '--create-groups', '-cg', default=False, show_default=True, is_flag=True,
928
+ help='Flag to note whether aperture groups should be created if none exists.'
929
+ )
930
+ @click.option(
931
+ '--output-model', help='Optional name of output HBJSON file as a '
932
+ 'string. If no name is provided the name will be the identifier of the '
933
+ 'model with "blinds" as suffix.',
934
+ default=None, show_default=True, type=click.STRING
935
+ )
936
+ def add_aperture_group_blinds_command(
937
+ model_file, diffuse_transmission, specular_transmission, distance, scale,
938
+ create_groups, output_model
939
+ ):
940
+ """Add a state geometry to aperture groups.
941
+
942
+ This command adds state geometry to all aperture groups in the model. The
943
+ geometry is the same as the aperture geometry but the modifier is changed.
944
+ The geometry is translated inward by a distance which by default is 0.001
945
+ in model units.
946
+
947
+ \b
948
+ Args:
949
+ model_file: Full path to a Model JSON file (HBJSON) or a Model pkl
950
+ (HBpkl) file.
951
+ """
952
+ try:
953
+ model: Model = Model.from_file(model_file)
954
+
955
+ def get_unique_aperture_groups(model):
956
+ unique_aperture_groups = {}
957
+ for ap in model.apertures:
958
+ if isinstance(ap.boundary_condition, Outdoors):
959
+ dgi = ap.properties.radiance.dynamic_group_identifier
960
+ if dgi is not None:
961
+ ap.properties.radiance.remove_states()
962
+ if dgi in unique_aperture_groups:
963
+ unique_aperture_groups[dgi].append(ap)
964
+ else:
965
+ unique_aperture_groups[dgi] = [ap]
966
+
967
+ return unique_aperture_groups
968
+
969
+ unique_aperture_groups = get_unique_aperture_groups(model)
970
+ if unique_aperture_groups: # there are aperture groups in the model already
971
+ pass
972
+ elif not unique_aperture_groups and create_groups: # no aperture groups, create them
973
+ model.solve_adjacency()
974
+ model = automatic_aperture_grouping(
975
+ model, room_based=True, view_factor_or_orientation=False)
976
+ unique_aperture_groups = get_unique_aperture_groups(model)
977
+ else:
978
+ raise ValueError(
979
+ 'No aperture groups found in the model. Either provide a model with aperture '
980
+ 'groups or use the --create-groups option to calculate the aperture groups as '
981
+ 'part of this command.'
982
+ )
983
+
984
+ for apertures in unique_aperture_groups.values():
985
+ shades = []
986
+ # create the shades
987
+ for ap in apertures:
988
+ vec = ap.normal * distance
989
+ in_vec = vec.reverse()
990
+ base_geo = ap.geometry.move(in_vec)
991
+ if base_geo.is_convex:
992
+ base_geo = base_geo.scale(scale, base_geo.centroid)
993
+ else:
994
+ plane = base_geo.plane
995
+ origin = base_geo.polygon2d.pole_of_inaccessibility(0.01)
996
+ origin_xyz = plane.xy_to_xyz(origin)
997
+ base_geo = base_geo.scale(scale, origin_xyz)
998
+ diff_ref = 0.2
999
+ trans_mod = Trans.from_reflected_specularity(
1000
+ identifier='generic-blind-trans', r_reflectance=diff_ref,
1001
+ g_reflectance=diff_ref, b_reflectance=diff_ref,
1002
+ transmitted_diff=diffuse_transmission,
1003
+ transmitted_spec=specular_transmission)
1004
+ # state geometry
1005
+ state_geo = StateGeometry('{}_blind'.format(ap.identifier), base_geo, trans_mod)
1006
+ shades.append(state_geo)
1007
+
1008
+ # blind state
1009
+ blind_state = RadianceSubFaceState(shades=shades)
1010
+
1011
+ # default state and blind state
1012
+ states = [RadianceSubFaceState(), blind_state]
1013
+ apertures[0].properties.radiance.states = [state.duplicate() for state in states]
1014
+
1015
+ # remove shades from following apertures to ensure they aren't double-counted
1016
+ states_wo_shades = []
1017
+ for state in states:
1018
+ new_state = state.duplicate()
1019
+ new_state.remove_shades()
1020
+ states_wo_shades.append(new_state)
1021
+ for ap in apertures[1:]:
1022
+ ap.properties.radiance.states = \
1023
+ [state.duplicate() for state in states_wo_shades]
1024
+
1025
+ if output_model is None:
1026
+ output_model = '{}_blinds'.format(model.identifier)
1027
+ model.to_hbjson(output_model, '.')
1028
+
1029
+ except Exception:
1030
+ _logger.exception("Failed to run add-aperture-group-blinds command.")
1031
+ traceback.print_exc()
1032
+ sys.exit(1)
1033
+ else:
1034
+ sys.exit(0)