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,261 @@
1
+ """Functions for post-processing EN 17037 daylight outputs."""
2
+ import json
3
+ import os
4
+
5
+ from .annual import filter_schedule_by_hours, _process_input_folder
6
+
7
+
8
+ def _daylight_autonomy(values, occ_pattern, threshold, total_hours):
9
+ """Calculate annual daylight autonomy for a sensor.
10
+
11
+ Args:
12
+ values: Hourly illuminance values as numbers.
13
+ occ_pattern: A list of 0 and 1 values for hours of occupancy.
14
+ threshold: Threshold value for daylight autonomy.
15
+ total_hours: An integer for the total number of occupied hours,
16
+ which can be used to avoid having to sum occ pattern each time.
17
+
18
+ Returns:
19
+ daylight autonomy
20
+ """
21
+ da = 0
22
+ for is_occ, value in zip(occ_pattern, values):
23
+ if is_occ == 0:
24
+ continue
25
+ if value > threshold:
26
+ da += 1
27
+
28
+ return round(100.0 * da / total_hours, 2)
29
+
30
+
31
+ def en17037_metrics_to_files(
32
+ ill_file, occ_pattern, output_folder, grid_name=None, total_hours=None
33
+ ):
34
+ """Compute annual EN 17037 metrics for an ill file and write the results to a folder.
35
+
36
+ This function generates 6 different files for daylight autonomy based on the varying
37
+ level of recommendation in EN 17037.
38
+
39
+ Args:
40
+ ill_file: Path to an ill file generated by Radiance. The ill file should be
41
+ tab separated and shot NOT have a header. The results for each sensor point
42
+ should be available in a row and and each column should be the illuminance
43
+ value for a sun_up_hour. The number of columns should match the number of
44
+ sun up hours.
45
+ occ_pattern: A list of 0 and 1 values for hours of occupancy.
46
+ output_folder: An output folder where the results will be written to. The folder
47
+ will be created if not exist.
48
+ grid_name: An optional name for grid name which will be used to name the output
49
+ files. If None the name of the input file will be used.
50
+ total_hours: An integer for the total number of occupied hours in the
51
+ occupancy schedule. If None, it will be assumed that all of the
52
+ occupied hours are sun-up hours and are already accounted for
53
+ in the the occ_pattern.
54
+ """
55
+ if not os.path.isdir(output_folder):
56
+ os.makedirs(output_folder)
57
+
58
+ recommendations = {
59
+ 'minimum_illuminance': {
60
+ 'minimum': 100,
61
+ 'medium': 300,
62
+ 'high': 500
63
+ },
64
+ 'target_illuminance': {
65
+ 'minimum': 300,
66
+ 'medium': 500,
67
+ 'high': 750
68
+ }
69
+ }
70
+
71
+ grid_name = grid_name or os.path.split(ill_file)[-1][-4:]
72
+ da_folders = []
73
+
74
+ for target_type, thresholds in recommendations.items():
75
+ type_folder = os.path.join(output_folder, target_type)
76
+ if not os.path.isdir(type_folder):
77
+ os.makedirs(type_folder)
78
+
79
+ for level, threshold in thresholds.items():
80
+ level_folder = os.path.join(type_folder, level)
81
+ if not os.path.isdir(level_folder):
82
+ os.makedirs(level_folder)
83
+
84
+ da_file = os.path.join(
85
+ level_folder, 'da', '%s.da' % grid_name).replace('\\', '/')
86
+ folder = os.path.dirname(da_file)
87
+ if not os.path.isdir(folder):
88
+ os.makedirs(folder)
89
+ sda_file = os.path.join(
90
+ level_folder, 'sda', '%s.sda' % grid_name).replace('\\', '/')
91
+ folder = os.path.dirname(sda_file)
92
+ if not os.path.isdir(folder):
93
+ os.makedirs(folder)
94
+
95
+ da = []
96
+ with open(ill_file) as results, open(da_file, 'w') as daf:
97
+ for pt_res in results:
98
+ values = (float(res) for res in pt_res.split())
99
+ dar = _daylight_autonomy(values, occ_pattern, threshold, total_hours)
100
+ daf.write(str(dar) + '\n')
101
+ da.append(dar)
102
+
103
+ space_target = 50 if target_type == 'target_illuminance' else 95
104
+ pass_fail = [int(val > space_target) for val in da]
105
+
106
+ sda = sum(pass_fail) / len(pass_fail)
107
+ with open(sda_file, 'w') as sdaf:
108
+ sdaf.write(str(sda))
109
+
110
+ da_folders.append(os.path.join(level_folder, 'da'))
111
+
112
+ return da_folders
113
+
114
+
115
+ # TODO - support a list of schedules/schedule folder to match the input grids
116
+ def en17037_to_folder(
117
+ results_folder, schedule, grids_filter='*', sub_folder='metrics'
118
+ ):
119
+ """Compute annual EN 17037 metrics in a folder and write them in a subfolder.
120
+
121
+ This folder is an output folder of annual daylight recipe. Folder should include
122
+ grids_info.json and sun-up-hours.txt - the script uses the list in grids_info.json
123
+ to find the result files for each sensor grid.
124
+
125
+ Args:
126
+ results_folder: Results folder.
127
+ schedule: An annual schedule for 8760 hours of the year as a list of values. This
128
+ should be a daylight hours schedule.
129
+ grids_filter: A pattern to filter the grids. By default all the grids will be
130
+ processed.
131
+ sub_folder: An optional relative path for subfolder to copy results files.
132
+ Default: metrics
133
+
134
+ Returns:
135
+ str -- Path to results folder.
136
+
137
+ """
138
+ grids, sun_up_hours = _process_input_folder(results_folder, grids_filter)
139
+ occ_pattern, total_occ, sun_down_occ_hours = \
140
+ filter_schedule_by_hours(sun_up_hours=sun_up_hours, schedule=schedule)
141
+
142
+ if total_occ != 4380:
143
+ raise ValueError(
144
+ 'There are %s occupied hours in the schedule. According to EN 17037 the '
145
+ 'schedule must consist of the daylight hours which is defined '
146
+ 'as the half of the year with the largest quantity of daylight' % total_occ)
147
+
148
+ metrics_folder = os.path.join(results_folder, sub_folder)
149
+ if not os.path.isdir(metrics_folder):
150
+ os.makedirs(metrics_folder)
151
+
152
+ for grid in grids:
153
+ ill_file = os.path.join(results_folder, '%s.ill' % grid['full_id'])
154
+ da_folders = en17037_metrics_to_files(
155
+ ill_file, occ_pattern, metrics_folder, grid['full_id'], total_occ
156
+ )
157
+
158
+ # copy info.json to all results folders
159
+ for folder_name in da_folders:
160
+ grid_info = os.path.join(metrics_folder, folder_name, 'grids_info.json')
161
+ with open(grid_info, 'w') as outf:
162
+ json.dump(grids, outf, indent=2)
163
+
164
+ # create info for available results. This file will be used by honeybee-vtk for
165
+ # results visualization
166
+ config_file = os.path.join(metrics_folder, 'config.json')
167
+
168
+ cfg = _annual_daylight_en17037_config()
169
+
170
+ with open(config_file, 'w') as outf:
171
+ json.dump(cfg, outf)
172
+
173
+ return metrics_folder
174
+
175
+
176
+ def _annual_daylight_en17037_config():
177
+ """Return vtk-config for annual daylight EN 17037. """
178
+ cfg = {
179
+ "data": [
180
+ {
181
+ "identifier": "Daylight Autonomy - target 300 lux",
182
+ "object_type": "grid",
183
+ "unit": "Percentage",
184
+ "path": "target_illuminance/minimum/da",
185
+ "hide": False,
186
+ "legend_parameters": {
187
+ "hide_legend": False,
188
+ "min": 0,
189
+ "max": 100,
190
+ "color_set": "nuanced",
191
+ },
192
+ },
193
+ {
194
+ "identifier": "Daylight Autonomy - target 500 lux",
195
+ "object_type": "grid",
196
+ "unit": "Percentage",
197
+ "path": "target_illuminance/medium/da",
198
+ "hide": False,
199
+ "legend_parameters": {
200
+ "hide_legend": False,
201
+ "min": 0,
202
+ "max": 100,
203
+ "color_set": "nuanced",
204
+ },
205
+ },
206
+ {
207
+ "identifier": "Daylight Autonomy - target 750 lux",
208
+ "object_type": "grid",
209
+ "unit": "Percentage",
210
+ "path": "target_illuminance/high/da",
211
+ "hide": False,
212
+ "legend_parameters": {
213
+ "hide_legend": False,
214
+ "min": 0,
215
+ "max": 100,
216
+ "color_set": "nuanced",
217
+ },
218
+ },
219
+ {
220
+ "identifier": "Daylight Autonomy - minimum 100 lux",
221
+ "object_type": "grid",
222
+ "unit": "Percentage",
223
+ "path": "minimum_illuminance/minimum/da",
224
+ "hide": False,
225
+ "legend_parameters": {
226
+ "hide_legend": False,
227
+ "min": 0,
228
+ "max": 100,
229
+ "color_set": "nuanced",
230
+ },
231
+ },
232
+ {
233
+ "identifier": "Daylight Autonomy - minimum 300 lux",
234
+ "object_type": "grid",
235
+ "unit": "Percentage",
236
+ "path": "minimum_illuminance/medium/da",
237
+ "hide": False,
238
+ "legend_parameters": {
239
+ "hide_legend": False,
240
+ "min": 0,
241
+ "max": 100,
242
+ "color_set": "nuanced",
243
+ },
244
+ },
245
+ {
246
+ "identifier": "Daylight Autonomy - minimum 500 lux",
247
+ "object_type": "grid",
248
+ "unit": "Percentage",
249
+ "path": "minimum_illuminance/high/da",
250
+ "hide": False,
251
+ "legend_parameters": {
252
+ "hide_legend": False,
253
+ "min": 0,
254
+ "max": 100,
255
+ "color_set": "nuanced",
256
+ },
257
+ },
258
+ ]
259
+ }
260
+
261
+ return cfg
@@ -0,0 +1,304 @@
1
+ """Functions for post-processing LEED daylight outputs."""
2
+ import json
3
+ import os
4
+ import shutil
5
+ import math
6
+
7
+ from honeybee.model import Model
8
+ from honeybee.units import conversion_factor_to_meters
9
+ from ..writer import _filter_by_pattern
10
+
11
+
12
+ def _process_input_folder(folder, filter_pattern):
13
+ """Process and input annual daylight results folder."""
14
+ info = os.path.join(folder, 'grids_info.json')
15
+ with open(info) as data_f:
16
+ data = json.load(data_f)
17
+ grids = _filter_by_pattern(data, filter=filter_pattern)
18
+ return grids
19
+
20
+
21
+ def ill_pass_fail_from_folder(
22
+ results_folder, glare_control=True, grids_filter='*'):
23
+ """Compute a list of LEED pass/fail values from a list of illuminance results.
24
+
25
+ Args:
26
+ results_folder: Folder containing illuminance result (.res) files for
27
+ a single irradiance simulation.
28
+ glare_control: A boolean for whether the model has "view-preserving automatic
29
+ (with manual override) glare-control devices," which means that illuminance
30
+ only needs to be above 300 lux and not between 300 and 3000 lux.
31
+ grids_filter: A pattern to filter the grids. By default all the grids will be
32
+ processed.
33
+
34
+ Returns:
35
+ A list of lists where each sub-list represents a sensor grid and contains
36
+ zero/one values for whether each sensor fails/passes the LEED illuminance
37
+ criteria.
38
+ """
39
+ pass_fail = []
40
+ grids = _process_input_folder(results_folder, grids_filter)
41
+ for grid in grids:
42
+ res_file = os.path.join(results_folder, '%s.res' % grid['full_id'])
43
+ with open(res_file) as inf:
44
+ values = [float(line) for line in inf]
45
+ grid_pf = []
46
+ for val in values:
47
+ if val > 300:
48
+ pf = 1 if glare_control or val < 3000 else 0
49
+ grid_pf.append(pf)
50
+ else:
51
+ grid_pf.append(0)
52
+ pass_fail.append(grid_pf)
53
+ return pass_fail
54
+
55
+
56
+ def _pass_fail_to_files(
57
+ folder, sub_folder, pass_fail_comb, pass_fail_9, pass_fail_3, filter_pattern):
58
+ """Write pass/fail matrices into files that can be loaded and visualized later.
59
+
60
+ Args:
61
+ folder: Project folder for a LEED daylight illuminance simulation.
62
+ sub_folder: Relative path for a subfolder to write the pass/fail files for
63
+ each sensor grid.
64
+ pass_fail_comb: Matrix of ones/zeros for combined pass/failing.
65
+ pass_fail_9: Matrix of ones/zeros for 9AM pass/failing.
66
+ pass_fail_3: Matrix of ones/zeros for 3PM pass/failing.
67
+ filter_pattern: Pattern used to filter the grids.
68
+ """
69
+ # get the grids_info.json and determine which grids we are working with
70
+ res_folder_9 = os.path.join(folder, '9AM', 'results')
71
+ info_json = os.path.join(res_folder_9, 'grids_info.json')
72
+ with open(info_json) as data_f:
73
+ data = json.load(data_f)
74
+ grids = _filter_by_pattern(data, filter=filter_pattern)
75
+
76
+ # create the directories into which the files will be written
77
+ output_folder = os.path.join(folder, sub_folder)
78
+ folder_comb = os.path.join(output_folder, 'combined')
79
+ folder_9 = os.path.join(output_folder, '9AM')
80
+ folder_3 = os.path.join(output_folder, '3PM')
81
+ for sub_dir in (folder_comb, folder_9, folder_3):
82
+ if not os.path.isdir(sub_dir):
83
+ os.makedirs(sub_dir)
84
+ shutil.copyfile(info_json, os.path.join(sub_dir, 'grids_info.json'))
85
+
86
+ # loop through each grid and write the results into files
87
+ for g_d, res_c, res_9, res_3 in zip(grids, pass_fail_comb, pass_fail_9, pass_fail_3):
88
+ g_file_name = '%s.res' % g_d['full_id']
89
+ file_c = os.path.join(folder_comb, g_file_name)
90
+ file_9 = os.path.join(folder_9, g_file_name)
91
+ file_3 = os.path.join(folder_3, g_file_name)
92
+
93
+ with open(file_c, 'w') as fc, open(file_9, 'w') as f9, open(file_3, 'w') as f3:
94
+ for rc, r9, r3 in zip(res_c, res_9, res_3):
95
+ fc.write(str(rc) + '\n')
96
+ f9.write(str(r9) + '\n')
97
+ f3.write(str(r3) + '\n')
98
+
99
+
100
+ def _sum_passing_area(pass_fails, grid_areas):
101
+ """Compute the sum of passing area given aligned pass_fail and grid_area matrices.
102
+ """
103
+ area_passing = 0
104
+ for pf, ga in zip(pass_fails, grid_areas):
105
+ if pf == 1:
106
+ area_passing += ga
107
+ return area_passing
108
+
109
+
110
+ def _sum_all_passing_area(pass_fails, grid_areas):
111
+ """Compute the sum of passing area given aligned pass_fail and grid_area matrices.
112
+ """
113
+ area_passing = 0
114
+ for p_fails, g_areas in zip(pass_fails, grid_areas):
115
+ for pf, ga in zip(p_fails, g_areas):
116
+ if pf == 1:
117
+ area_passing += ga
118
+ return area_passing
119
+
120
+
121
+ def _space_by_space_summary(
122
+ folder, sub_folder, grid_areas, units_conversion,
123
+ pass_fail_comb, pass_fail_9, pass_fail_3, filter_pattern):
124
+ """Write a CSV with space-by-space information for the percentage of sensors passing.
125
+
126
+ Args:
127
+ folder: Project folder for a LEED daylight illuminance simulation.
128
+ sub_folder: Relative path for a subfolder to write the pass/fail files for
129
+ each sensor grid.
130
+ grid_areas: A matrix of numbers for the area occupied by each sensor.
131
+ units_conversion: A number for the conversion factor from the grid_areas units
132
+ to Meters.
133
+ pass_fail_comb: Matrix of ones/zeros for combined pass/failing.
134
+ pass_fail_9: Matrix of ones/zeros for 9AM pass/failing.
135
+ pass_fail_3: Matrix of ones/zeros for 3PM pass/failing.
136
+ filter_pattern: Pattern used to filter the grids.
137
+ """
138
+ # get the grids_info.json and determine which grids we are working with
139
+ res_folder_9 = os.path.join(folder, '9AM', 'results')
140
+ info_json = os.path.join(res_folder_9, 'grids_info.json')
141
+ with open(info_json) as data_f:
142
+ data = json.load(data_f)
143
+ grids = _filter_by_pattern(data, filter=filter_pattern)
144
+
145
+ # define the header row of the CSV
146
+ csv_data = [['Space Name', 'Sensor Count']]
147
+ if len(grid_areas) == len(pass_fail_9): # compute passing floor area for each grid
148
+ csv_data[0].extend(['Area (m2)', 'Area (ft2)', 'Spacing (m)'])
149
+ csv_data[0].extend(['% Passing 9AM', '% Passing 3PM', '% Passing Combined'])
150
+
151
+ # loop through each grid and get the rows of the CSV
152
+ if len(grid_areas) == len(pass_fail_9):
153
+ all_data = zip(grids, grid_areas, pass_fail_comb, pass_fail_9, pass_fail_3)
154
+ for gr, gr_a, res_c, res_9, res_3 in all_data:
155
+ csv_row = [gr['name'], gr['count']]
156
+ total_a = sum(gr_a)
157
+ csv_row.append(round(total_a * units_conversion, 3))
158
+ csv_row.append(round(csv_row[2] / 0.305, 3))
159
+ csv_row.append(round(math.sqrt(csv_row[2] / csv_row[1]), 3))
160
+ csv_row.append(round(100 * (_sum_passing_area(res_9, gr_a) / total_a), 2))
161
+ csv_row.append(round(100 * (_sum_passing_area(res_3, gr_a) / total_a), 2))
162
+ csv_row.append(round(100 * (_sum_passing_area(res_c, gr_a) / total_a), 2))
163
+ csv_data.append(csv_row)
164
+ else:
165
+ all_data = zip(grids, pass_fail_comb, pass_fail_9, pass_fail_3)
166
+ for gr, res_c, res_9, res_3 in all_data:
167
+ csv_row = [gr['name'], gr['count']]
168
+ total_count = csv_row[1]
169
+ csv_row.append(round(100 * (sum(res_9) / total_count), 2))
170
+ csv_row.append(round(100 * (sum(res_3) / total_count), 2))
171
+ csv_row.append(round(100 * (sum(res_c) / total_count), 2))
172
+ csv_data.append(csv_row)
173
+
174
+ # write the results into a CSV
175
+ output_file = os.path.join(folder, sub_folder, 'space_summary.csv')
176
+ with open(output_file, 'w') as of:
177
+ for row in csv_data:
178
+ of.write(','.join((str(v) for v in row)) + '\n')
179
+
180
+
181
+ def leed_illuminance_to_folder(
182
+ folder, glare_control=True, grids_filter='*', sub_folder=None):
183
+ """Estimate LEED daylight credits from two point-in-time illuminance folders.
184
+
185
+ Args:
186
+ folder: Project folder for a LEED illuminance simulation. It should contain
187
+ a HBJSON model and two sub-folders of complete point-in-time illuminance
188
+ simulations labeled "9AM" and "3PM". These two sub-folders should each
189
+ have results folders that include a grids_info.json and .res files with
190
+ illuminance values for each sensor. If Meshes are found for the sensor
191
+ grids in the HBJSON file, they will be used to compute percentages
192
+ of occupied floor area that pass vs. fail. Otherwise, all sensors will
193
+ be assumed to represent an equal amount of floor area.
194
+ glare_control: A boolean for whether the model has "view-preserving automatic
195
+ (with manual override) glare-control devices," which means that illuminance
196
+ only needs to be above 300 lux and not between 300 and 3000 lux.
197
+ grids_filter: A pattern to filter the grids. By default all the grids will be
198
+ processed.
199
+ sub_folder: Relative path for a subfolder to write the pass/fail files for
200
+ each sensor grid and a space-by-space summary CSV. If None, the files
201
+ will not be written and only the summary dictionary will be calculated.
202
+
203
+ Returns:
204
+ A dictionary with a summary of LEED credits in the format below. All
205
+ percentages are between 0 and 100 and the floor areas are in the units system
206
+ of the HBJSON model. If no sensor grid meshes were found in the HBJSON
207
+ model and no areas could be associated with each mesh face, the output
208
+ will not contain floor_area keys and each sensor will be assumed to
209
+ occupy a similar area.
210
+
211
+ .. code-block:: python
212
+
213
+ {
214
+ "credits": 2,
215
+ "percentage_passing": 76.2,
216
+ "percentage_passing_9AM": 78.5,
217
+ "percentage_passing_3PM": 82.4,
218
+ "sensor_count_passing": 762,
219
+ "sensor_count_passing_9AM": 785,
220
+ "sensor_count_passing_3PM": 824,
221
+ "total_sensor_count": 1000,
222
+ "floor_area_passing": 762.0,
223
+ "floor_area_passing_9AM": 785.0,
224
+ "floor_area_passing_3PM": 824.0,
225
+ "total_floor_area": 1000.0
226
+ }
227
+ """
228
+ # first load the results into pass/fail matrices of ones/zeros
229
+ res_folder_9 = os.path.join(folder, '9AM', 'results')
230
+ res_folder_3 = os.path.join(folder, '3PM', 'results')
231
+ pass_fail_9 = ill_pass_fail_from_folder(res_folder_9, glare_control, grids_filter)
232
+ pass_fail_3 = ill_pass_fail_from_folder(res_folder_3, glare_control, grids_filter)
233
+
234
+ # determine which sensors pass for both hours
235
+ pass_fail_comb = []
236
+ for p_fails9, p_fails3 in zip(pass_fail_9, pass_fail_3):
237
+ p_fails_comb = []
238
+ for pf9, pf3 in zip(p_fails9, p_fails3):
239
+ if pf9 == 1 and pf3 == 1:
240
+ p_fails_comb.append(1)
241
+ else:
242
+ p_fails_comb.append(0)
243
+ pass_fail_comb.append(p_fails_comb)
244
+
245
+ # next, check to see if there is a HBJSON with sensor grid meshes for areas
246
+ grid_areas, units_conversion = [], 1
247
+ for base_file in os.listdir(folder):
248
+ if base_file.endswith('.hbjson') or base_file.endswith('.hbpkl'):
249
+ hb_model = Model.from_file(os.path.join(folder, base_file))
250
+ units_conversion = conversion_factor_to_meters(hb_model.units)
251
+ filt_grids = _filter_by_pattern(
252
+ hb_model.properties.radiance.sensor_grids, filter=grids_filter)
253
+ for s_grid in filt_grids:
254
+ if s_grid.mesh is not None:
255
+ grid_areas.append(s_grid.mesh.face_areas)
256
+
257
+ # write the pass/fail criteria into the sub-directory if specified
258
+ if sub_folder:
259
+ _pass_fail_to_files(
260
+ folder, sub_folder, pass_fail_comb, pass_fail_9, pass_fail_3, grids_filter)
261
+ _space_by_space_summary(
262
+ folder, sub_folder, grid_areas, units_conversion,
263
+ pass_fail_comb, pass_fail_9, pass_fail_3, grids_filter)
264
+
265
+ # setup the summary dictionary with the results
266
+ summary_dict = {
267
+ 'sensor_count_passing': sum(sum(pf) for pf in pass_fail_comb),
268
+ 'sensor_count_passing_9AM': sum(sum(pf9) for pf9 in pass_fail_9),
269
+ 'sensor_count_passing_3PM': sum(sum(pf3) for pf3 in pass_fail_3),
270
+ 'total_sensor_count': sum(len(pf9) for pf9 in pass_fail_9)
271
+ }
272
+
273
+ # determine the percentage passing from either mesh areas or sensor counts
274
+ if len(grid_areas) == len(pass_fail_9): # compute passing floor area for each grid
275
+ area_pass_comb = _sum_all_passing_area(pass_fail_comb, grid_areas)
276
+ area_pass_9 = _sum_all_passing_area(pass_fail_9, grid_areas)
277
+ area_pass_3 = _sum_all_passing_area(pass_fail_3, grid_areas)
278
+ area_total = sum(sum(sar) for sar in grid_areas)
279
+ summary_dict['floor_area_passing'] = area_pass_comb
280
+ summary_dict['floor_area_passing_9AM'] = area_pass_9
281
+ summary_dict['floor_area_passing_3PM'] = area_pass_3
282
+ summary_dict['total_floor_area'] = area_total
283
+ pct_pass = (area_pass_comb / area_total) * 100
284
+ pct_pass_9 = (area_pass_9 / area_total) * 100
285
+ pct_pass_3 = (area_pass_3 / area_total) * 100
286
+ else:
287
+ total_count = summary_dict['total_sensor_count']
288
+ pct_pass = (summary_dict['sensor_count_passing'] / total_count) * 100
289
+ pct_pass_9 = (summary_dict['sensor_count_passing_9AM'] / total_count) * 100
290
+ pct_pass_3 = (summary_dict['sensor_count_passing_3PM'] / total_count) * 100
291
+
292
+ # lastly, estimate the number of LEED credits from the percentage passing
293
+ summary_dict['percentage_passing'] = pct_pass
294
+ summary_dict['percentage_passing_9AM'] = pct_pass_9
295
+ summary_dict['percentage_passing_3PM'] = pct_pass_3
296
+ if pct_pass >= 90:
297
+ summary_dict['credits'] = 3
298
+ elif pct_pass >= 75:
299
+ summary_dict['credits'] = 2
300
+ elif pct_pass >= 55:
301
+ summary_dict['credits'] = 1
302
+ else:
303
+ summary_dict['credits'] = 0
304
+ return summary_dict
@@ -0,0 +1,90 @@
1
+ """Functions for post-processing results of dynamic objects that track the sun."""
2
+ import os
3
+ import json
4
+ import shutil
5
+ import math
6
+
7
+ from ladybug_geometry.geometry3d.pointvector import Vector3D
8
+ from ladybug.sunpath import Sunpath
9
+
10
+
11
+ def post_process_solar_tracking(
12
+ result_folders, sun_up_file, location, north=0, tracking_increment=5,
13
+ destination_folder=None):
14
+ """Postprocess a list of result folders to account for dynamic solar tracking.
15
+
16
+ This function essentially takes .ill files for each state of a dynamic tracking
17
+ system and produces a single .ill file that models the tracking behavior.
18
+
19
+ Args:
20
+ result_folders: A list of folders containing .ill files and each representing
21
+ a state of the dynamic solar tracking system. These file should be
22
+ ordered from eastern-most to wester-most, tracing the path of the
23
+ tracking system over the day. The names of the .ill files should be
24
+ the same in each folder (representing the same sensor grid in a
25
+ different state).
26
+ sun_up_file: Path to a sun-up-hours.txt that contains the sun-up hours of
27
+ the simulation.
28
+ location: A Ladybug Location object to be used to generate sun poisitions.
29
+ north_: A number between -360 and 360 for the counterclockwise difference
30
+ between the North and the positive Y-axis in degrees. (Default: 0).
31
+ tracking_increment: An integer for the increment angle of each state in
32
+ degrees. (Default: 5).
33
+ destination_folder: A path to a destination folder where the final .ill
34
+ files of the dynamic tracking system will be written. If None, all
35
+ files will be written into the directory above the first result_folder.
36
+ (Default: None).
37
+ """
38
+ # get the orientation angles of the panels for each model
39
+ st_angle = int(90 - (len(result_folders) * tracking_increment / 2)) + 1
40
+ end_angle = int(90 + (len(result_folders) * tracking_increment / 2))
41
+ angles = list(range(st_angle, end_angle, tracking_increment))
42
+
43
+ # create a sun path ang get the sun-up hours to be used to get solar positions
44
+ sp = Sunpath.from_location(location, north)
45
+ with open(sun_up_file) as suh_file:
46
+ sun_up_hours = [float(hour) for hour in suh_file.readlines()]
47
+
48
+ # for each hour of the sun_up_hours, figure out which file is the one to use
49
+ mtx_to_use, ground_vec = [], Vector3D(1, 0, 0)
50
+ for hoy in sun_up_hours:
51
+ sun = sp.calculate_sun_from_hoy(hoy)
52
+ vec = Vector3D(sun.sun_vector_reversed.x, 0, sun.sun_vector_reversed.z)
53
+ orient = math.degrees(ground_vec.angle(vec))
54
+ for i, ang in enumerate(angles):
55
+ if ang > orient:
56
+ mtx_to_use.append(i)
57
+ break
58
+ else:
59
+ mtx_to_use.append(-1)
60
+
61
+ # parse the grids_info in the first folder to understand the sensor grids
62
+ grids_info_file = os.path.join(result_folders[0], 'grids_info.json')
63
+ with open(grids_info_file) as gi_file:
64
+ grids_data = json.load(gi_file)
65
+ grid_ids = [g['full_id'] for g in grids_data]
66
+
67
+ # prepare the destination folder and copy the grids_info to it
68
+ if destination_folder is None:
69
+ destination_folder = os.path.dirname(result_folders[0])
70
+ if not os.path.isdir(destination_folder):
71
+ os.mkdir(destination_folder)
72
+ shutil.copyfile(grids_info_file, os.path.join(destination_folder, 'grids_info.json'))
73
+
74
+ # convert the .ill files of each sensor grid into a single .ill file
75
+ for grid_id in grid_ids:
76
+ grid_mtx = []
77
+ for i, model in enumerate(result_folders):
78
+ grid_file = os.path.join(model, '{}.ill'.format(grid_id))
79
+ with open(grid_file) as ill_file:
80
+ grid_mtx.append([lin.split() for lin in ill_file])
81
+ grid_ill = []
82
+ for i, hoy_mtx in enumerate(mtx_to_use):
83
+ hoy_vals = []
84
+ for pt in range(len(grid_mtx[0])):
85
+ hoy_vals.append(grid_mtx[hoy_mtx][pt][i])
86
+ grid_ill.append(hoy_vals)
87
+ dest_file = os.path.join(destination_folder, '{}.ill'.format(grid_id))
88
+ with open(dest_file, 'w') as ill_file:
89
+ for row in zip(*grid_ill):
90
+ ill_file.write(' '.join(row) + '\n')