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