honeybee-radiance-postprocess 0.4.555__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.
- honeybee_radiance_postprocess/__init__.py +1 -0
- honeybee_radiance_postprocess/__main__.py +4 -0
- honeybee_radiance_postprocess/annual.py +73 -0
- honeybee_radiance_postprocess/annualdaylight.py +289 -0
- honeybee_radiance_postprocess/annualirradiance.py +35 -0
- honeybee_radiance_postprocess/breeam/__init__.py +1 -0
- honeybee_radiance_postprocess/breeam/breeam.py +552 -0
- honeybee_radiance_postprocess/cli/__init__.py +33 -0
- honeybee_radiance_postprocess/cli/abnt.py +392 -0
- honeybee_radiance_postprocess/cli/breeam.py +96 -0
- honeybee_radiance_postprocess/cli/datacollection.py +133 -0
- honeybee_radiance_postprocess/cli/grid.py +295 -0
- honeybee_radiance_postprocess/cli/leed.py +143 -0
- honeybee_radiance_postprocess/cli/merge.py +161 -0
- honeybee_radiance_postprocess/cli/mtxop.py +161 -0
- honeybee_radiance_postprocess/cli/postprocess.py +1092 -0
- honeybee_radiance_postprocess/cli/schedule.py +103 -0
- honeybee_radiance_postprocess/cli/translate.py +216 -0
- honeybee_radiance_postprocess/cli/two_phase.py +252 -0
- honeybee_radiance_postprocess/cli/util.py +121 -0
- honeybee_radiance_postprocess/cli/viewfactor.py +157 -0
- honeybee_radiance_postprocess/cli/well.py +110 -0
- honeybee_radiance_postprocess/data_type.py +102 -0
- honeybee_radiance_postprocess/dynamic.py +273 -0
- honeybee_radiance_postprocess/electriclight.py +24 -0
- honeybee_radiance_postprocess/en17037.py +304 -0
- honeybee_radiance_postprocess/helper.py +266 -0
- honeybee_radiance_postprocess/ies/__init__.py +1 -0
- honeybee_radiance_postprocess/ies/lm.py +224 -0
- honeybee_radiance_postprocess/ies/lm_schedule.py +248 -0
- honeybee_radiance_postprocess/leed/__init__.py +1 -0
- honeybee_radiance_postprocess/leed/leed.py +801 -0
- honeybee_radiance_postprocess/leed/leed_schedule.py +256 -0
- honeybee_radiance_postprocess/metrics.py +439 -0
- honeybee_radiance_postprocess/reader.py +80 -0
- honeybee_radiance_postprocess/results/__init__.py +4 -0
- honeybee_radiance_postprocess/results/annual_daylight.py +752 -0
- honeybee_radiance_postprocess/results/annual_irradiance.py +196 -0
- honeybee_radiance_postprocess/results/results.py +1416 -0
- honeybee_radiance_postprocess/type_hints.py +38 -0
- honeybee_radiance_postprocess/util.py +211 -0
- honeybee_radiance_postprocess/vis_metadata.py +49 -0
- honeybee_radiance_postprocess/well/__init__.py +1 -0
- honeybee_radiance_postprocess/well/well.py +509 -0
- honeybee_radiance_postprocess-0.4.555.dist-info/METADATA +79 -0
- honeybee_radiance_postprocess-0.4.555.dist-info/RECORD +50 -0
- honeybee_radiance_postprocess-0.4.555.dist-info/WHEEL +5 -0
- honeybee_radiance_postprocess-0.4.555.dist-info/entry_points.txt +2 -0
- honeybee_radiance_postprocess-0.4.555.dist-info/licenses/LICENSE +661 -0
- honeybee_radiance_postprocess-0.4.555.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1092 @@
|
|
1
|
+
"""honeybee radiance daylight postprocessing commands."""
|
2
|
+
from pathlib import Path
|
3
|
+
import sys
|
4
|
+
import os
|
5
|
+
import logging
|
6
|
+
import json
|
7
|
+
import click
|
8
|
+
try:
|
9
|
+
import cupy as np
|
10
|
+
is_gpu = True
|
11
|
+
except ImportError:
|
12
|
+
is_gpu = False
|
13
|
+
import numpy as np
|
14
|
+
|
15
|
+
from ladybug.location import Location
|
16
|
+
from ladybug.wea import Wea
|
17
|
+
from honeybee_radiance_postprocess.results.annual_daylight import AnnualDaylight
|
18
|
+
from honeybee_radiance_postprocess.results.annual_irradiance import AnnualIrradiance
|
19
|
+
from honeybee_radiance_postprocess.metrics import da_array2d, cda_array2d, \
|
20
|
+
udi_array2d, udi_lower_array2d, udi_upper_array2d
|
21
|
+
from honeybee_radiance_postprocess.reader import binary_to_array
|
22
|
+
|
23
|
+
from ..annual import occupancy_schedule_8_to_6
|
24
|
+
from ..dynamic import DynamicSchedule
|
25
|
+
from ..en17037 import en17037_to_folder
|
26
|
+
from ..util import filter_array
|
27
|
+
from .two_phase import two_phase
|
28
|
+
from .leed import leed
|
29
|
+
from .abnt import abnt
|
30
|
+
from .well import well
|
31
|
+
from .breeam import breeam
|
32
|
+
from ..helper import model_grid_areas, grid_summary
|
33
|
+
|
34
|
+
_logger = logging.getLogger(__name__)
|
35
|
+
|
36
|
+
|
37
|
+
@click.group(help='Commands to post-process Radiance results.')
|
38
|
+
def post_process():
|
39
|
+
pass
|
40
|
+
|
41
|
+
post_process.add_command(two_phase)
|
42
|
+
post_process.add_command(leed)
|
43
|
+
post_process.add_command(abnt)
|
44
|
+
post_process.add_command(well)
|
45
|
+
post_process.add_command(breeam)
|
46
|
+
|
47
|
+
|
48
|
+
@post_process.command('annual-daylight')
|
49
|
+
@click.argument(
|
50
|
+
'folder',
|
51
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True)
|
52
|
+
)
|
53
|
+
@click.option(
|
54
|
+
'--schedule', '-sch', help='Path to an annual schedule file. Values should be 0-1 '
|
55
|
+
'separated by new line. If not provided an 8-5 annual schedule will be created.',
|
56
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False, resolve_path=True)
|
57
|
+
)
|
58
|
+
@click.option(
|
59
|
+
'--threshold', '-t', help='Threshold illuminance level for daylight autonomy.',
|
60
|
+
default=300, type=int, show_default=True
|
61
|
+
)
|
62
|
+
@click.option(
|
63
|
+
'--lower-threshold', '-lt',
|
64
|
+
help='Minimum threshold for useful daylight illuminance.', default=100, type=int,
|
65
|
+
show_default=True
|
66
|
+
)
|
67
|
+
@click.option(
|
68
|
+
'--upper-threshold', '-ut',
|
69
|
+
help='Maximum threshold for useful daylight illuminance.', default=3000, type=int,
|
70
|
+
show_default=True
|
71
|
+
)
|
72
|
+
@click.option(
|
73
|
+
'--states', '-st', help='A JSON file with a dictionary of states. If states are not '
|
74
|
+
'provided the default states will be used for any aperture groups.', default=None,
|
75
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False, resolve_path=True)
|
76
|
+
)
|
77
|
+
@click.option(
|
78
|
+
'--grids-filter', '-gf', help='A pattern to filter the grids.', default='*',
|
79
|
+
show_default=True
|
80
|
+
)
|
81
|
+
@click.option(
|
82
|
+
'--sub-folder', '-sf', help='Optional relative path for subfolder to write output '
|
83
|
+
'metric files.', default='metrics'
|
84
|
+
)
|
85
|
+
def annual_metrics(
|
86
|
+
folder, schedule, threshold, lower_threshold, upper_threshold, states,
|
87
|
+
grids_filter, sub_folder
|
88
|
+
):
|
89
|
+
"""Compute annual metrics in a folder and write them in a subfolder.
|
90
|
+
|
91
|
+
\b
|
92
|
+
This command generates 5 files for each input grid.
|
93
|
+
da/{grid-name}.da -> Daylight Autonomy
|
94
|
+
cda/{grid-name}.cda -> Continuos Daylight Autonomy
|
95
|
+
udi/{grid-name}.udi -> Useful Daylight Illuminance
|
96
|
+
udi_lower/{grid-name}_upper.udi -> Upper Useful Daylight Illuminance
|
97
|
+
udi_upper/{grid-name}_lower.udi -> Lower Useful Daylight Illuminance
|
98
|
+
|
99
|
+
\b
|
100
|
+
Args:
|
101
|
+
folder: Results folder. This folder is an output folder of annual
|
102
|
+
daylight recipe. Folder should include grids_info.json and
|
103
|
+
sun-up-hours.txt.
|
104
|
+
"""
|
105
|
+
# optional input - only check if the file exist otherwise ignore
|
106
|
+
if schedule and os.path.isfile(schedule):
|
107
|
+
with open(schedule) as hourly_schedule:
|
108
|
+
schedule = [int(float(v)) for v in hourly_schedule]
|
109
|
+
else:
|
110
|
+
schedule = None
|
111
|
+
|
112
|
+
if states:
|
113
|
+
states = DynamicSchedule.from_json(states)
|
114
|
+
|
115
|
+
try:
|
116
|
+
results = AnnualDaylight(folder, schedule=schedule)
|
117
|
+
results.annual_metrics_to_folder(
|
118
|
+
sub_folder, threshold=threshold, min_t=lower_threshold,
|
119
|
+
max_t=upper_threshold, states=states, grids_filter=grids_filter
|
120
|
+
)
|
121
|
+
except Exception:
|
122
|
+
_logger.exception('Failed to calculate annual metrics.')
|
123
|
+
sys.exit(1)
|
124
|
+
else:
|
125
|
+
sys.exit(0)
|
126
|
+
|
127
|
+
|
128
|
+
@post_process.command('annual-daylight-en17037')
|
129
|
+
@click.argument(
|
130
|
+
'folder',
|
131
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True)
|
132
|
+
)
|
133
|
+
@click.argument(
|
134
|
+
'schedule',
|
135
|
+
type=click.Path(exists=True, file_okay=True, dir_okay=False, resolve_path=True)
|
136
|
+
)
|
137
|
+
@click.option(
|
138
|
+
'--states', '-st', help='A JSON file with a dictionary of states. If states are not '
|
139
|
+
'provided the default states will be used for any aperture groups.', default=None,
|
140
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False, resolve_path=True)
|
141
|
+
)
|
142
|
+
@click.option(
|
143
|
+
'--grids-filter', '-gf', help='A pattern to filter the grids.', default='*',
|
144
|
+
show_default=True
|
145
|
+
)
|
146
|
+
@click.option(
|
147
|
+
'--sub_folder', '-sf', help='Optional relative path for subfolder to write output '
|
148
|
+
'metric files.', default='en17037'
|
149
|
+
)
|
150
|
+
def annual_en17037_metrics(
|
151
|
+
folder, schedule, states, grids_filter, sub_folder
|
152
|
+
):
|
153
|
+
"""Compute annual EN 17037 metrics in a folder and write them in a subfolder.
|
154
|
+
|
155
|
+
\b
|
156
|
+
This command generates multiple files for each input grid. Files for target
|
157
|
+
illuminance and minimum illuminance will be calculated for three levels of
|
158
|
+
recommendation: minimum, medium, high.
|
159
|
+
|
160
|
+
\b
|
161
|
+
Args:
|
162
|
+
folder: Results folder. This folder is an output folder of annual
|
163
|
+
daylight recipe. Folder should include grids_info.json and
|
164
|
+
sun-up-hours.txt.
|
165
|
+
schedule: Path to an annual schedule file. Values should be 0-1
|
166
|
+
separated by new line. This should be a daylight hours schedule.
|
167
|
+
"""
|
168
|
+
with open(schedule) as hourly_schedule:
|
169
|
+
schedule = [int(float(v)) for v in hourly_schedule]
|
170
|
+
|
171
|
+
if states:
|
172
|
+
states = DynamicSchedule.from_json(states)
|
173
|
+
|
174
|
+
try:
|
175
|
+
en17037_to_folder(
|
176
|
+
folder, schedule, states=states, grids_filter=grids_filter,
|
177
|
+
sub_folder=sub_folder)
|
178
|
+
except Exception:
|
179
|
+
_logger.exception('Failed to calculate annual EN 17037 metrics.')
|
180
|
+
sys.exit(1)
|
181
|
+
else:
|
182
|
+
sys.exit(0)
|
183
|
+
|
184
|
+
|
185
|
+
@post_process.command('average-values')
|
186
|
+
@click.argument(
|
187
|
+
'folder',
|
188
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True)
|
189
|
+
)
|
190
|
+
@click.option(
|
191
|
+
'--hoys-file', '-h', help='Path to an HOYs file. Values must be separated by '
|
192
|
+
'new line. If not provided the data will not be filtered by HOYs.',
|
193
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False, resolve_path=True)
|
194
|
+
)
|
195
|
+
@click.option(
|
196
|
+
'--states', '-st', help='A JSON file with a dictionary of states. If states '
|
197
|
+
'are not provided the default states will be used for any aperture groups.',
|
198
|
+
default=None, show_default=True,
|
199
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False, resolve_path=True)
|
200
|
+
)
|
201
|
+
@click.option(
|
202
|
+
'--grids-filter', '-gf', help='A pattern to filter the grids.', default='*',
|
203
|
+
show_default=True
|
204
|
+
)
|
205
|
+
@click.option(
|
206
|
+
'--total/--direct', is_flag=True, default=True, help='Switch between total '
|
207
|
+
'and direct results. Default is total.'
|
208
|
+
)
|
209
|
+
@click.option(
|
210
|
+
'--sub-folder', '-sf', help='Optional relative path for subfolder to write output '
|
211
|
+
'metric files.', default='metrics'
|
212
|
+
)
|
213
|
+
def average_values(
|
214
|
+
folder, hoys_file, states, grids_filter, total, sub_folder
|
215
|
+
):
|
216
|
+
"""Get average values for each sensor over a given period.
|
217
|
+
|
218
|
+
\b
|
219
|
+
Args:
|
220
|
+
folder: Results folder. This folder is an output folder of annual daylight
|
221
|
+
recipe. Folder should include grids_info.json and sun-up-hours.txt. The
|
222
|
+
command uses the list in grids_info.json to find the result files for each
|
223
|
+
sensor grid.
|
224
|
+
"""
|
225
|
+
try:
|
226
|
+
if hoys_file:
|
227
|
+
with open(hoys_file) as hoys:
|
228
|
+
hoys = [float(h) for h in hoys.readlines()]
|
229
|
+
else:
|
230
|
+
hoys = []
|
231
|
+
|
232
|
+
if states:
|
233
|
+
states = DynamicSchedule.from_json(states)
|
234
|
+
|
235
|
+
res_type = 'total' if total is True else 'direct'
|
236
|
+
|
237
|
+
results = AnnualDaylight(folder)
|
238
|
+
results.average_values_to_folder(
|
239
|
+
sub_folder, hoys=hoys, states=states, grids_filter=grids_filter,
|
240
|
+
res_type=res_type)
|
241
|
+
except Exception:
|
242
|
+
_logger.exception('Failed to calculate average values.')
|
243
|
+
sys.exit(1)
|
244
|
+
else:
|
245
|
+
sys.exit(0)
|
246
|
+
|
247
|
+
|
248
|
+
@post_process.command('median-values')
|
249
|
+
@click.argument(
|
250
|
+
'folder',
|
251
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True)
|
252
|
+
)
|
253
|
+
@click.option(
|
254
|
+
'--hoys-file', '-h', help='Path to an HOYs file. Values must be separated by '
|
255
|
+
'new line. If not provided the data will not be filtered by HOYs.',
|
256
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False, resolve_path=True)
|
257
|
+
)
|
258
|
+
@click.option(
|
259
|
+
'--states', '-st', help='A JSON file with a dictionary of states. If states '
|
260
|
+
'are not provided the default states will be used for any aperture groups.',
|
261
|
+
default=None, show_default=True,
|
262
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False, resolve_path=True)
|
263
|
+
)
|
264
|
+
@click.option(
|
265
|
+
'--grids-filter', '-gf', help='A pattern to filter the grids.', default='*',
|
266
|
+
show_default=True
|
267
|
+
)
|
268
|
+
@click.option(
|
269
|
+
'--total/--direct', is_flag=True, default=True, help='Switch between total '
|
270
|
+
'and direct results. Default is total.'
|
271
|
+
)
|
272
|
+
@click.option(
|
273
|
+
'--sub-folder', '-sf', help='Optional relative path for subfolder to write output '
|
274
|
+
'metric files.', default='metrics'
|
275
|
+
)
|
276
|
+
def median_values(
|
277
|
+
folder, hoys_file, states, grids_filter, total, sub_folder
|
278
|
+
):
|
279
|
+
"""Get median values for each sensor over a given period.
|
280
|
+
|
281
|
+
\b
|
282
|
+
Args:
|
283
|
+
folder: Results folder. This folder is an output folder of annual daylight
|
284
|
+
recipe. Folder should include grids_info.json and sun-up-hours.txt. The
|
285
|
+
command uses the list in grids_info.json to find the result files for each
|
286
|
+
sensor grid.
|
287
|
+
"""
|
288
|
+
try:
|
289
|
+
if hoys_file:
|
290
|
+
with open(hoys_file) as hoys:
|
291
|
+
hoys = [float(h) for h in hoys.readlines()]
|
292
|
+
else:
|
293
|
+
hoys = []
|
294
|
+
|
295
|
+
if states:
|
296
|
+
states = DynamicSchedule.from_json(states)
|
297
|
+
|
298
|
+
res_type = 'total' if total is True else 'direct'
|
299
|
+
|
300
|
+
results = AnnualDaylight(folder)
|
301
|
+
results.median_values_to_folder(
|
302
|
+
sub_folder, hoys=hoys, states=states, grids_filter=grids_filter,
|
303
|
+
res_type=res_type)
|
304
|
+
except Exception:
|
305
|
+
_logger.exception('Failed to calculate median values.')
|
306
|
+
sys.exit(1)
|
307
|
+
else:
|
308
|
+
sys.exit(0)
|
309
|
+
|
310
|
+
|
311
|
+
@post_process.command('cumulative-values')
|
312
|
+
@click.argument(
|
313
|
+
'folder',
|
314
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True)
|
315
|
+
)
|
316
|
+
@click.option(
|
317
|
+
'--hoys-file', '-h', help='Path to an HOYs file. Values must be separated by '
|
318
|
+
'new line. If not provided the data will not be filtered by HOYs.',
|
319
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False, resolve_path=True)
|
320
|
+
)
|
321
|
+
@click.option(
|
322
|
+
'--states', '-st', help='A JSON file with a dictionary of states. If states '
|
323
|
+
'are not provided the default states will be used for any aperture groups.',
|
324
|
+
default=None, show_default=True,
|
325
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False, resolve_path=True)
|
326
|
+
)
|
327
|
+
@click.option(
|
328
|
+
'--grids-filter', '-gf', help='A pattern to filter the grids.', default='*',
|
329
|
+
show_default=True
|
330
|
+
)
|
331
|
+
@click.option(
|
332
|
+
'--total/--direct', is_flag=True, default=True, help='Switch between total '
|
333
|
+
'and direct results. Default is total.'
|
334
|
+
)
|
335
|
+
@click.option(
|
336
|
+
'--sub-folder', '-sf', help='Optional relative path for subfolder to write output '
|
337
|
+
'metric files.', default='metrics'
|
338
|
+
)
|
339
|
+
def cumulative_values(
|
340
|
+
folder, hoys_file, states, grids_filter, total, sub_folder
|
341
|
+
):
|
342
|
+
"""Get cumulative values for each sensor over a given period.
|
343
|
+
|
344
|
+
\b
|
345
|
+
Args:
|
346
|
+
folder: Results folder. This folder is an output folder of annual daylight
|
347
|
+
recipe. Folder should include grids_info.json and sun-up-hours.txt. The
|
348
|
+
command uses the list in grids_info.json to find the result files for each
|
349
|
+
sensor grid.
|
350
|
+
"""
|
351
|
+
try:
|
352
|
+
if hoys_file:
|
353
|
+
with open(hoys_file) as hoys:
|
354
|
+
hoys = [float(h) for h in hoys.readlines()]
|
355
|
+
else:
|
356
|
+
hoys = []
|
357
|
+
|
358
|
+
if states:
|
359
|
+
states = DynamicSchedule.from_json(states)
|
360
|
+
|
361
|
+
res_type = 'total' if total is True else 'direct'
|
362
|
+
|
363
|
+
results = AnnualDaylight(folder)
|
364
|
+
results.cumulative_values_to_folder(
|
365
|
+
sub_folder, hoys=hoys, states=states, grids_filter=grids_filter,
|
366
|
+
res_type=res_type)
|
367
|
+
except Exception:
|
368
|
+
_logger.exception('Failed to calculate cumulative values.')
|
369
|
+
sys.exit(1)
|
370
|
+
else:
|
371
|
+
sys.exit(0)
|
372
|
+
|
373
|
+
|
374
|
+
@post_process.command('peak-values')
|
375
|
+
@click.argument(
|
376
|
+
'folder',
|
377
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True)
|
378
|
+
)
|
379
|
+
@click.option(
|
380
|
+
'--hoys-file', '-h', help='Path to an HOYs file. Values must be separated by '
|
381
|
+
'new line. If not provided the data will not be filtered by HOYs.',
|
382
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False, resolve_path=True)
|
383
|
+
)
|
384
|
+
@click.option(
|
385
|
+
'--states', '-st', help='A JSON file with a dictionary of states. If states '
|
386
|
+
'are not provided the default states will be used for any aperture groups.',
|
387
|
+
default=None, show_default=True,
|
388
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False, resolve_path=True)
|
389
|
+
)
|
390
|
+
@click.option(
|
391
|
+
'--grids-filter', '-gf', help='A pattern to filter the grids.', default='*',
|
392
|
+
show_default=True
|
393
|
+
)
|
394
|
+
@click.option(
|
395
|
+
'--total/--direct', is_flag=True, default=True, help='Switch between total '
|
396
|
+
'and direct results. Default is total.'
|
397
|
+
)
|
398
|
+
@click.option(
|
399
|
+
'--coincident/--non-coincident', is_flag=True, default=False, show_default=True,
|
400
|
+
help='Boolean to indicate whether output values represent the the peak value for '
|
401
|
+
'each sensor throughout the entire analysis (False) or they represent the highest '
|
402
|
+
'overall value across each sensor grid at a particular timestep (True).'
|
403
|
+
)
|
404
|
+
@click.option(
|
405
|
+
'--sub-folder', '-sf', help='Optional relative path for subfolder to write output '
|
406
|
+
'metric files.', default='metrics'
|
407
|
+
)
|
408
|
+
def peak_values(
|
409
|
+
folder, hoys_file, states, grids_filter, total, coincident, sub_folder
|
410
|
+
):
|
411
|
+
"""Get peak values for each sensor over a given period.
|
412
|
+
|
413
|
+
\b
|
414
|
+
Args:
|
415
|
+
folder: Results folder. This folder is an output folder of annual daylight
|
416
|
+
recipe. Folder should include grids_info.json and sun-up-hours.txt. The
|
417
|
+
command uses the list in grids_info.json to find the result files for each
|
418
|
+
sensor grid.
|
419
|
+
"""
|
420
|
+
try:
|
421
|
+
if hoys_file:
|
422
|
+
with open(hoys_file) as hoys:
|
423
|
+
hoys = [float(h) for h in hoys.readlines()]
|
424
|
+
else:
|
425
|
+
hoys = []
|
426
|
+
|
427
|
+
if states:
|
428
|
+
states = DynamicSchedule.from_json(states)
|
429
|
+
|
430
|
+
res_type = 'total' if total is True else 'direct'
|
431
|
+
|
432
|
+
results = AnnualDaylight(folder)
|
433
|
+
results.peak_values_to_folder(
|
434
|
+
sub_folder, hoys=hoys, states=states, grids_filter=grids_filter,
|
435
|
+
coincident=coincident, res_type=res_type)
|
436
|
+
except Exception:
|
437
|
+
_logger.exception('Failed to calculate peak values.')
|
438
|
+
sys.exit(1)
|
439
|
+
else:
|
440
|
+
sys.exit(0)
|
441
|
+
|
442
|
+
|
443
|
+
@post_process.command('annual-to-data')
|
444
|
+
@click.argument(
|
445
|
+
'folder',
|
446
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True)
|
447
|
+
)
|
448
|
+
@click.option(
|
449
|
+
'--states', '-st', help='A JSON file with a dictionary of states. If states '
|
450
|
+
'are not provided the default states will be used for any aperture groups.',
|
451
|
+
default=None, show_default=True,
|
452
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False, resolve_path=True)
|
453
|
+
)
|
454
|
+
@click.option(
|
455
|
+
'--grids-filter', '-gf', help='A pattern to filter the grids.', default='*',
|
456
|
+
show_default=True
|
457
|
+
)
|
458
|
+
@click.option(
|
459
|
+
'--sensor-index', '-si', help='A JSON file with a dictionary of sensor indices '
|
460
|
+
'for each grid. If not provided all sensors will be used.',
|
461
|
+
default=None, show_default=True,
|
462
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False, resolve_path=True)
|
463
|
+
)
|
464
|
+
@click.option(
|
465
|
+
'--total/--direct', is_flag=True, default=True, help='Switch between total '
|
466
|
+
'and direct results. Default is total.'
|
467
|
+
)
|
468
|
+
@click.option(
|
469
|
+
'--output-file', '-f', help='Optional file to output the JSON strings of '
|
470
|
+
'the data collections. By default, it will be printed to stdout',
|
471
|
+
type=click.File('w'), default='-', show_default=True
|
472
|
+
)
|
473
|
+
def annual_to_data(
|
474
|
+
folder, states, grids_filter, sensor_index, total, output_file
|
475
|
+
):
|
476
|
+
"""Get annual data collections as JSON files.
|
477
|
+
|
478
|
+
\b
|
479
|
+
Args:
|
480
|
+
folder: Results folder. This folder is an output folder of annual daylight
|
481
|
+
recipe. Folder should include grids_info.json and sun-up-hours.txt. The
|
482
|
+
command uses the list in grids_info.json to find the result files for each
|
483
|
+
sensor grid.
|
484
|
+
"""
|
485
|
+
if states:
|
486
|
+
states = DynamicSchedule.from_json(states)
|
487
|
+
|
488
|
+
if sensor_index:
|
489
|
+
with open(sensor_index) as json_file:
|
490
|
+
sensor_index = json.load(json_file)
|
491
|
+
|
492
|
+
res_type = 'total' if total is True else 'direct'
|
493
|
+
|
494
|
+
try:
|
495
|
+
results = AnnualDaylight(folder)
|
496
|
+
data_cs, grids_info, sensor_index = results.annual_data(
|
497
|
+
states=states, grids_filter=grids_filter,
|
498
|
+
sensor_index=sensor_index, res_type=res_type)
|
499
|
+
data_colls = [[data.to_dict() for data in data_list] for data_list in data_cs]
|
500
|
+
output_file.write(json.dumps(data_colls))
|
501
|
+
except Exception:
|
502
|
+
_logger.exception('Failed to create data collections.')
|
503
|
+
sys.exit(1)
|
504
|
+
else:
|
505
|
+
sys.exit(0)
|
506
|
+
|
507
|
+
|
508
|
+
@post_process.command('point-in-time')
|
509
|
+
@click.argument(
|
510
|
+
'folder',
|
511
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True)
|
512
|
+
)
|
513
|
+
@click.argument(
|
514
|
+
'hoy', type=click.FLOAT
|
515
|
+
)
|
516
|
+
@click.option(
|
517
|
+
'--states', '-st', help='A JSON file with a dictionary of states. If states '
|
518
|
+
'are not provided the default states will be used for any aperture groups.',
|
519
|
+
default=None, show_default=True,
|
520
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False, resolve_path=True)
|
521
|
+
)
|
522
|
+
@click.option(
|
523
|
+
'--grids-filter', '-gf', help='A pattern to filter the grids.', default='*',
|
524
|
+
show_default=True
|
525
|
+
)
|
526
|
+
@click.option(
|
527
|
+
'--total/--direct', is_flag=True, default=True, help='Switch between total '
|
528
|
+
'and direct results. Default is total.'
|
529
|
+
)
|
530
|
+
@click.option(
|
531
|
+
'--sub-folder', '-sf', help='Optional relative path for subfolder to write output '
|
532
|
+
'metric files.', default='metrics'
|
533
|
+
)
|
534
|
+
def point_in_time(
|
535
|
+
folder, hoy, states, grids_filter, total, sub_folder
|
536
|
+
):
|
537
|
+
"""Get point in time values.
|
538
|
+
|
539
|
+
\b
|
540
|
+
Args:
|
541
|
+
folder: Results folder. This folder is an output folder of annual daylight
|
542
|
+
recipe. Folder should include grids_info.json and sun-up-hours.txt. The
|
543
|
+
command uses the list in grids_info.json to find the result files for each
|
544
|
+
sensor grid.
|
545
|
+
hoy: An HOY (point-in-time) for which to get the point-in-time values.
|
546
|
+
"""
|
547
|
+
try:
|
548
|
+
if states:
|
549
|
+
states = DynamicSchedule.from_json(states)
|
550
|
+
|
551
|
+
res_type = 'total' if total is True else 'direct'
|
552
|
+
|
553
|
+
results = AnnualDaylight(folder)
|
554
|
+
results.point_in_time_to_folder(
|
555
|
+
sub_folder, datetime=hoy, states=states, grids_filter=grids_filter,
|
556
|
+
res_type=res_type)
|
557
|
+
except Exception:
|
558
|
+
_logger.exception('Failed to point in time values.')
|
559
|
+
sys.exit(1)
|
560
|
+
else:
|
561
|
+
sys.exit(0)
|
562
|
+
|
563
|
+
|
564
|
+
@post_process.command('annual-sunlight-exposure')
|
565
|
+
@click.argument(
|
566
|
+
'folder',
|
567
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True)
|
568
|
+
)
|
569
|
+
@click.option(
|
570
|
+
'--schedule', '-sch', help='Path to an annual schedule file. Values should be 0-1 '
|
571
|
+
'separated by new line. If not provided an 8-5 annual schedule will be created.',
|
572
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False, resolve_path=True)
|
573
|
+
)
|
574
|
+
@click.option(
|
575
|
+
'--direct-threshold', '-dt', help='The threshold that determines if a '
|
576
|
+
'sensor is overlit.',
|
577
|
+
default=1000, type=float, show_default=True
|
578
|
+
)
|
579
|
+
@click.option(
|
580
|
+
'--occ-hours', '-oh', help='The number of occupied hours that cannot '
|
581
|
+
'receive more than the direct_threshold.', default=250, type=int,
|
582
|
+
show_default=True
|
583
|
+
)
|
584
|
+
@click.option(
|
585
|
+
'--states', '-st', help='A JSON file with a dictionary of states. If states are not '
|
586
|
+
'provided the default states will be used for any aperture groups.', default=None,
|
587
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False, resolve_path=True)
|
588
|
+
)
|
589
|
+
@click.option(
|
590
|
+
'--grids-filter', '-gf', help='A pattern to filter the grids.', default='*',
|
591
|
+
show_default=True
|
592
|
+
)
|
593
|
+
@click.option(
|
594
|
+
'--sub-folder', '-sf', help='Optional relative path for subfolder to write output '
|
595
|
+
'metric files.', default='metrics'
|
596
|
+
)
|
597
|
+
def annual_sunlight_exposure(
|
598
|
+
folder, schedule, direct_threshold, occ_hours, states, grids_filter,
|
599
|
+
sub_folder
|
600
|
+
):
|
601
|
+
"""Compute annual sunlight exposure in a folder and write them in a subfolder.
|
602
|
+
|
603
|
+
\b
|
604
|
+
This command generates 2 files for each input grid.
|
605
|
+
ase/{grid-name}.ase -> Annual Sunlight Exposure
|
606
|
+
hours_above/{grid-name}.hours -> Number of overlit hours for each sensor
|
607
|
+
|
608
|
+
\b
|
609
|
+
Args:
|
610
|
+
folder: Results folder. This folder is an output folder of annual
|
611
|
+
daylight recipe. Folder should include grids_info.json and
|
612
|
+
sun-up-hours.txt.
|
613
|
+
"""
|
614
|
+
# optional input - only check if the file exist otherwise ignore
|
615
|
+
if schedule and os.path.isfile(schedule):
|
616
|
+
with open(schedule) as hourly_schedule:
|
617
|
+
schedule = [int(float(v)) for v in hourly_schedule]
|
618
|
+
else:
|
619
|
+
schedule = None
|
620
|
+
|
621
|
+
if states:
|
622
|
+
states = DynamicSchedule.from_json(states)
|
623
|
+
|
624
|
+
try:
|
625
|
+
results = AnnualDaylight(folder, schedule=schedule)
|
626
|
+
results.annual_sunlight_exposure_to_folder(
|
627
|
+
sub_folder, direct_threshold=direct_threshold, occ_hours=occ_hours,
|
628
|
+
states=states, grids_filter=grids_filter
|
629
|
+
)
|
630
|
+
except Exception:
|
631
|
+
_logger.exception('Failed to calculate annual sunlight exposure.')
|
632
|
+
sys.exit(1)
|
633
|
+
else:
|
634
|
+
sys.exit(0)
|
635
|
+
|
636
|
+
|
637
|
+
@post_process.command('annual-daylight-file')
|
638
|
+
@click.argument(
|
639
|
+
'file',
|
640
|
+
type=click.Path(exists=True, file_okay=True, dir_okay=False, resolve_path=True)
|
641
|
+
)
|
642
|
+
@click.argument(
|
643
|
+
'sun-up-hours',
|
644
|
+
type=click.Path(exists=True, file_okay=True, dir_okay=False, resolve_path=True)
|
645
|
+
)
|
646
|
+
@click.option(
|
647
|
+
'--schedule', '-sch', help='Path to an annual schedule file. Values should be 0-1 '
|
648
|
+
'separated by new line. If not provided an 8-5 annual schedule will be created.',
|
649
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False, resolve_path=True)
|
650
|
+
)
|
651
|
+
@click.option(
|
652
|
+
'--threshold', '-t', help='Threshold illuminance level for daylight autonomy.',
|
653
|
+
default=300, type=int, show_default=True
|
654
|
+
)
|
655
|
+
@click.option(
|
656
|
+
'--lower-threshold', '-lt',
|
657
|
+
help='Minimum threshold for useful daylight illuminance.', default=100, type=int,
|
658
|
+
show_default=True
|
659
|
+
)
|
660
|
+
@click.option(
|
661
|
+
'--upper-threshold', '-ut',
|
662
|
+
help='Maximum threshold for useful daylight illuminance.', default=3000, type=int,
|
663
|
+
show_default=True
|
664
|
+
)
|
665
|
+
@click.option(
|
666
|
+
'--study-info',
|
667
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False, resolve_path=True),
|
668
|
+
help='Optional study info file. This option is needed if the time step is '
|
669
|
+
'larger than 1.'
|
670
|
+
)
|
671
|
+
@click.option(
|
672
|
+
'--grid-name', '-gn', help='Optional name of each metric file.',
|
673
|
+
default=None, show_default=True
|
674
|
+
)
|
675
|
+
@click.option(
|
676
|
+
'--sub-folder', '-sf', help='Optional relative path for subfolder to write output '
|
677
|
+
'metric files.', default='metrics'
|
678
|
+
)
|
679
|
+
def annual_metrics_file(
|
680
|
+
file, sun_up_hours, schedule, threshold, lower_threshold, upper_threshold,
|
681
|
+
study_info, grid_name, sub_folder
|
682
|
+
):
|
683
|
+
"""Compute annual metrics for a single file and write the metrics in a
|
684
|
+
subfolder.
|
685
|
+
|
686
|
+
\b
|
687
|
+
This command generates 5 files for each input grid.
|
688
|
+
da/{grid-name}.da -> Daylight Autonomy
|
689
|
+
cda/{grid-name}.cda -> Continuos Daylight Autonomy
|
690
|
+
udi/{grid-name}.udi -> Useful Daylight Illuminance
|
691
|
+
udi_lower/{grid-name}_upper.udi -> Upper Useful Daylight Illuminance
|
692
|
+
udi_upper/{grid-name}_lower.udi -> Lower Useful Daylight Illuminance
|
693
|
+
|
694
|
+
\b
|
695
|
+
Args:
|
696
|
+
file: Annual illuminance file. This can be either a NumPy file or a
|
697
|
+
binary Radiance file.
|
698
|
+
sun_up_hours: A file with the sun up hours of the study.
|
699
|
+
"""
|
700
|
+
file = Path(file)
|
701
|
+
# load file to array
|
702
|
+
try:
|
703
|
+
array = np.load(file)
|
704
|
+
except Exception:
|
705
|
+
array = binary_to_array(file)
|
706
|
+
|
707
|
+
if study_info and os.path.isfile(study_info):
|
708
|
+
with open(study_info) as si_file:
|
709
|
+
study_info = json.load(si_file)
|
710
|
+
timestep = study_info['timestep']
|
711
|
+
study_hours = study_info['study_hours']
|
712
|
+
else:
|
713
|
+
timestep = 1
|
714
|
+
study_hours = \
|
715
|
+
Wea.from_annual_values(Location(), [0] * 8760, [0] * 8760).hoys
|
716
|
+
|
717
|
+
# read sun up hours
|
718
|
+
sun_up_hours = np.loadtxt(sun_up_hours)
|
719
|
+
# optional input - only check if the file exist otherwise ignore
|
720
|
+
if schedule and os.path.isfile(schedule):
|
721
|
+
with open(schedule) as hourly_schedule:
|
722
|
+
schedule = [int(float(v)) for v in hourly_schedule]
|
723
|
+
else:
|
724
|
+
schedule = occupancy_schedule_8_to_6(timestep=timestep)
|
725
|
+
|
726
|
+
if grid_name is None:
|
727
|
+
grid_name = file.stem
|
728
|
+
|
729
|
+
sun_up_hours_mask = np.where(np.isin(np.array(study_hours), np.array(sun_up_hours)))[0]
|
730
|
+
sun_down_hours_mask = np.where(~np.isin(np.array(study_hours), np.array(sun_up_hours)))[0]
|
731
|
+
occ_mask = np.array(schedule, dtype=int)[sun_up_hours_mask]
|
732
|
+
sun_down_occ_hours = np.array(schedule, dtype=int)[sun_down_hours_mask].sum()
|
733
|
+
total_hours = sum(schedule)
|
734
|
+
|
735
|
+
array_filter = np.apply_along_axis(
|
736
|
+
filter_array, 1, array, mask=occ_mask)
|
737
|
+
|
738
|
+
try:
|
739
|
+
da = da_array2d(array_filter, total_occ=total_hours, threshold=threshold)
|
740
|
+
cda = cda_array2d(array_filter, total_occ=total_hours, threshold=threshold)
|
741
|
+
udi = udi_array2d(
|
742
|
+
array_filter, total_occ=total_hours, min_t=lower_threshold,
|
743
|
+
max_t=upper_threshold)
|
744
|
+
udi_lower = udi_lower_array2d(
|
745
|
+
array_filter, total_occ=total_hours, min_t=lower_threshold,
|
746
|
+
sun_down_occ_hours=sun_down_occ_hours)
|
747
|
+
udi_upper = udi_upper_array2d(
|
748
|
+
array_filter, total_occ=total_hours, max_t=upper_threshold)
|
749
|
+
|
750
|
+
sub_folder = Path(sub_folder)
|
751
|
+
pattern = {
|
752
|
+
'da': da, 'cda': cda, 'udi_lower': udi_lower, 'udi': udi,
|
753
|
+
'udi_upper': udi_upper
|
754
|
+
}
|
755
|
+
for metric, data in pattern.items():
|
756
|
+
metric_folder = sub_folder.joinpath(metric)
|
757
|
+
extension = metric.split('_')[0]
|
758
|
+
output_file = metric_folder.joinpath(f'{grid_name}.{extension}')
|
759
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
760
|
+
np.savetxt(output_file, data, fmt='%.2f')
|
761
|
+
except Exception:
|
762
|
+
_logger.exception('Failed to calculate annual metrics.')
|
763
|
+
sys.exit(1)
|
764
|
+
else:
|
765
|
+
sys.exit(0)
|
766
|
+
|
767
|
+
|
768
|
+
@post_process.command('grid-summary')
|
769
|
+
@click.argument(
|
770
|
+
'folder',
|
771
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True)
|
772
|
+
)
|
773
|
+
@click.option(
|
774
|
+
'--model', '-m', help='An optional HBJSON model file. This will be used to '
|
775
|
+
'find the area of the grids. The area is used when calculating percentages '
|
776
|
+
'of floor area.',
|
777
|
+
type=click.Path(exists=True, file_okay=True, dir_okay=False, resolve_path=True)
|
778
|
+
)
|
779
|
+
@click.option(
|
780
|
+
'--grids-info', '-gi', help='An optional JSON file with grid information. '
|
781
|
+
'If no file is provided the command will look for a file in the folder.',
|
782
|
+
default=None, show_default=True,
|
783
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False, resolve_path=True)
|
784
|
+
)
|
785
|
+
@click.option(
|
786
|
+
'--name', '-n', help='Optional filename of grid summary.',
|
787
|
+
type=str, default='grid_summary', show_default=True
|
788
|
+
)
|
789
|
+
@click.option(
|
790
|
+
'--grid-metrics', '-gm', help='An optional JSON file with additional '
|
791
|
+
'custom metrics to calculate.', default=None, show_default=True,
|
792
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False, resolve_path=True)
|
793
|
+
)
|
794
|
+
@click.option(
|
795
|
+
'--sub-folder/--main-folder', is_flag=True, default=True,
|
796
|
+
help='If sub-folder is selected it will look for any sub-folders in the '
|
797
|
+
'folder argument. If main-folder is selected it will look for result files '
|
798
|
+
'in the folder argument.'
|
799
|
+
)
|
800
|
+
def grid_summary_metric(
|
801
|
+
folder, model, grids_info, name, grid_metrics, sub_folder
|
802
|
+
):
|
803
|
+
"""Calculate a grid summary.
|
804
|
+
|
805
|
+
If the grids info file is omitted it is a requirement that there is a grids
|
806
|
+
info file in the main folder or in each sub folder.
|
807
|
+
|
808
|
+
\b
|
809
|
+
Args:
|
810
|
+
folder: A folder with results.
|
811
|
+
"""
|
812
|
+
try:
|
813
|
+
# create Path object
|
814
|
+
folder = Path(folder)
|
815
|
+
|
816
|
+
# get grids information
|
817
|
+
if grids_info and Path(grids_info).is_file():
|
818
|
+
with open(grids_info) as gi:
|
819
|
+
grids_info = json.load(gi)
|
820
|
+
else:
|
821
|
+
grids_info = None
|
822
|
+
|
823
|
+
# get grid metrics
|
824
|
+
if grid_metrics and Path(grid_metrics).is_file():
|
825
|
+
with open(grid_metrics) as gm:
|
826
|
+
grid_metrics = json.load(gm)
|
827
|
+
else:
|
828
|
+
grid_metrics = None
|
829
|
+
|
830
|
+
# check to see if there is a HBJSON with sensor grid meshes for areas
|
831
|
+
if grids_info and model:
|
832
|
+
grid_areas = model_grid_areas(model, grids_info)
|
833
|
+
else:
|
834
|
+
grid_areas = None
|
835
|
+
|
836
|
+
grid_summary(
|
837
|
+
folder, grid_areas=grid_areas, grids_info=grids_info, name=name,
|
838
|
+
grid_metrics=grid_metrics, sub_folder=sub_folder)
|
839
|
+
|
840
|
+
except Exception:
|
841
|
+
_logger.exception('Failed to calculate grid summary.')
|
842
|
+
sys.exit(1)
|
843
|
+
else:
|
844
|
+
sys.exit(0)
|
845
|
+
|
846
|
+
|
847
|
+
@post_process.command('annual-uniformity-ratio')
|
848
|
+
@click.argument(
|
849
|
+
'folder',
|
850
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True)
|
851
|
+
)
|
852
|
+
@click.option(
|
853
|
+
'--schedule', '-sch', help='Path to an annual schedule file. Values should be 0-1 '
|
854
|
+
'separated by new line. If not provided an 8-5 annual schedule will be created.',
|
855
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False, resolve_path=True)
|
856
|
+
)
|
857
|
+
@click.option(
|
858
|
+
'--threshold', '-t', help='A threshold for the uniformity ratio. Defaults '
|
859
|
+
'to 0.5.',
|
860
|
+
default=0.5, type=click.FloatRange(0, 1), show_default=True
|
861
|
+
)
|
862
|
+
@click.option(
|
863
|
+
'--states', '-st', help='A JSON file with a dictionary of states. If states are not '
|
864
|
+
'provided the default states will be used for any aperture groups.', default=None,
|
865
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False, resolve_path=True)
|
866
|
+
)
|
867
|
+
@click.option(
|
868
|
+
'--grids-filter', '-gf', help='A pattern to filter the grids.', default='*',
|
869
|
+
show_default=True
|
870
|
+
)
|
871
|
+
@click.option(
|
872
|
+
'--sub-folder', '-sf', help='Optional relative path for subfolder to write '
|
873
|
+
'annual uniformity ratio.', default='annual_uniformity_ratio'
|
874
|
+
)
|
875
|
+
def annual_uniformity_ratio(
|
876
|
+
folder, schedule, threshold, states, grids_filter, sub_folder
|
877
|
+
):
|
878
|
+
"""Calculate annual uniformity ratio and write it to a folder.
|
879
|
+
|
880
|
+
\b
|
881
|
+
Args:
|
882
|
+
folder: Results folder. This folder is an output folder of annual
|
883
|
+
daylight recipe. Folder should include grids_info.json and
|
884
|
+
sun-up-hours.txt.
|
885
|
+
"""
|
886
|
+
# optional input - only check if the file exist otherwise ignore
|
887
|
+
if schedule and os.path.isfile(schedule):
|
888
|
+
with open(schedule) as hourly_schedule:
|
889
|
+
schedule = [int(float(v)) for v in hourly_schedule]
|
890
|
+
else:
|
891
|
+
schedule = None
|
892
|
+
|
893
|
+
if states:
|
894
|
+
states = DynamicSchedule.from_json(states)
|
895
|
+
|
896
|
+
try:
|
897
|
+
results = AnnualDaylight(folder, schedule=schedule)
|
898
|
+
results.annual_uniformity_ratio_to_folder(
|
899
|
+
sub_folder, threshold=threshold, states=states,
|
900
|
+
grids_filter=grids_filter
|
901
|
+
)
|
902
|
+
except Exception:
|
903
|
+
_logger.exception('Failed to calculate annual uniformity ratio.')
|
904
|
+
sys.exit(1)
|
905
|
+
else:
|
906
|
+
sys.exit(0)
|
907
|
+
|
908
|
+
|
909
|
+
@post_process.command('annual-irradiance')
|
910
|
+
@click.argument(
|
911
|
+
'folder',
|
912
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True)
|
913
|
+
)
|
914
|
+
@click.option(
|
915
|
+
'--schedule', '-sch', help='Path to an annual schedule file. Values should be 0-1 '
|
916
|
+
'separated by new line. If not provided an 8-5 annual schedule will be created.',
|
917
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False, resolve_path=True)
|
918
|
+
)
|
919
|
+
@click.option(
|
920
|
+
'--states', '-st', help='A JSON file with a dictionary of states. If states are not '
|
921
|
+
'provided the default states will be used for any aperture groups.', default=None,
|
922
|
+
type=click.Path(exists=False, file_okay=True, dir_okay=False, resolve_path=True)
|
923
|
+
)
|
924
|
+
@click.option(
|
925
|
+
'--grids-filter', '-gf', help='A pattern to filter the grids.', default='*',
|
926
|
+
show_default=True
|
927
|
+
)
|
928
|
+
@click.option(
|
929
|
+
'--sub-folder', '-sf', help='Optional relative path for subfolder to write output '
|
930
|
+
'metric files.', default='metrics'
|
931
|
+
)
|
932
|
+
def annual_irradiance_metrics(
|
933
|
+
folder, schedule, states, grids_filter, sub_folder
|
934
|
+
):
|
935
|
+
"""Compute irradiance metrics in a folder and write them in a subfolder.
|
936
|
+
|
937
|
+
\b
|
938
|
+
This command generates 3 files for each input grid.
|
939
|
+
average_irradiance/{grid-name}.res -> Average Irradiance (W/m2)
|
940
|
+
peak_irradiance/{grid-name}.res -> Peak Irradiance (W/m2)
|
941
|
+
cumulative_radiation/{grid-name}.res -> Cumulative Radiation (kWh/m2)
|
942
|
+
|
943
|
+
\b
|
944
|
+
Args:
|
945
|
+
folder: Results folder. This folder is an output folder of annual
|
946
|
+
irradiance recipe. Folder should include grids_info.json and
|
947
|
+
sun-up-hours.txt.
|
948
|
+
"""
|
949
|
+
# optional input - only check if the file exist otherwise ignore
|
950
|
+
if schedule and os.path.isfile(schedule):
|
951
|
+
with open(schedule) as hourly_schedule:
|
952
|
+
schedule = [int(float(v)) for v in hourly_schedule]
|
953
|
+
else:
|
954
|
+
schedule = None
|
955
|
+
|
956
|
+
if states:
|
957
|
+
states = DynamicSchedule.from_json(states)
|
958
|
+
|
959
|
+
try:
|
960
|
+
results = AnnualIrradiance(folder, schedule=schedule)
|
961
|
+
results.annual_metrics_to_folder(
|
962
|
+
sub_folder, states=states, grids_filter=grids_filter
|
963
|
+
)
|
964
|
+
except Exception:
|
965
|
+
_logger.exception('Failed to calculate annual irradiance metrics.')
|
966
|
+
sys.exit(1)
|
967
|
+
else:
|
968
|
+
sys.exit(0)
|
969
|
+
|
970
|
+
|
971
|
+
@post_process.command('convert-to-binary')
|
972
|
+
@click.argument(
|
973
|
+
'input-matrix', type=click.Path(exists=True, file_okay=True, resolve_path=True)
|
974
|
+
)
|
975
|
+
@click.option(
|
976
|
+
'--minimum', type=float, default='-inf', help='Minimum range for values to be '
|
977
|
+
'converted to 1.'
|
978
|
+
)
|
979
|
+
@click.option(
|
980
|
+
'--maximum', type=float, default='+inf', help='Maximum range for values to be '
|
981
|
+
'converted to 1.'
|
982
|
+
)
|
983
|
+
@click.option(
|
984
|
+
'--include-max/--exclude-max', is_flag=True, help='A flag to include the maximum '
|
985
|
+
'threshold itself. By default the threshold value will be included.', default=True
|
986
|
+
)
|
987
|
+
@click.option(
|
988
|
+
'--include-min/--exclude-min', is_flag=True, help='A flag to include the minimum '
|
989
|
+
'threshold itself. By default the threshold value will be included.', default=True
|
990
|
+
)
|
991
|
+
@click.option(
|
992
|
+
'--comply/--reverse', is_flag=True, help='A flag to reverse the selection logic. '
|
993
|
+
'This is useful for cases that you want to all the values outside a certain range '
|
994
|
+
'to be converted to 1. By default the input logic will be used as is.', default=True
|
995
|
+
)
|
996
|
+
@click.option(
|
997
|
+
'--name', '-n', help='Name of output file.', default='binary',
|
998
|
+
show_default=True
|
999
|
+
)
|
1000
|
+
@click.option(
|
1001
|
+
'--output-folder', '-of', help='Output folder.', default='.',
|
1002
|
+
type=click.Path(exists=False, file_okay=False, dir_okay=True, resolve_path=True)
|
1003
|
+
)
|
1004
|
+
def convert_matrix_to_binary(
|
1005
|
+
input_matrix, minimum, maximum, include_max, include_min, comply, name, output_folder
|
1006
|
+
):
|
1007
|
+
"""Postprocess a Radiance matrix and convert it to 0-1 values.
|
1008
|
+
|
1009
|
+
\b
|
1010
|
+
This command is useful for translating Radiance results to outputs like
|
1011
|
+
sunlight hours. Input matrix must be in ASCII or binary format. The input
|
1012
|
+
Radiance file must have a header.
|
1013
|
+
|
1014
|
+
Args:
|
1015
|
+
input-matrix: A Radiance matrix file.
|
1016
|
+
"""
|
1017
|
+
array = binary_to_array(input_matrix)
|
1018
|
+
minimum = float(minimum)
|
1019
|
+
maximum = float(maximum)
|
1020
|
+
try:
|
1021
|
+
if include_max and include_min:
|
1022
|
+
boolean_array = (array >= minimum) & (array <= maximum)
|
1023
|
+
elif not include_max and not include_min:
|
1024
|
+
boolean_array = (array > minimum) & (array < maximum)
|
1025
|
+
elif include_max and not include_min:
|
1026
|
+
boolean_array = (array > minimum) & (array <= maximum)
|
1027
|
+
elif not include_max and include_min:
|
1028
|
+
boolean_array = (array >= minimum) & (array < maximum)
|
1029
|
+
|
1030
|
+
if not comply:
|
1031
|
+
# this will invert the boolean array
|
1032
|
+
boolean_array = ~boolean_array
|
1033
|
+
|
1034
|
+
binary_array = boolean_array.astype(int)
|
1035
|
+
output_file = Path(output_folder, name)
|
1036
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
1037
|
+
np.save(output_file, binary_array)
|
1038
|
+
except Exception:
|
1039
|
+
_logger.exception('Failed to convert the input file to binary format.')
|
1040
|
+
sys.exit(1)
|
1041
|
+
else:
|
1042
|
+
sys.exit(0)
|
1043
|
+
|
1044
|
+
|
1045
|
+
@post_process.command('direct-sun-hours')
|
1046
|
+
@click.argument(
|
1047
|
+
'input-matrix', type=click.Path(exists=True, file_okay=True, resolve_path=True)
|
1048
|
+
)
|
1049
|
+
@click.option(
|
1050
|
+
'--divisor', type=float, default=1, help='An optional number, that the summed '
|
1051
|
+
'row will be divided by. For example, this can be a timestep, which can be used '
|
1052
|
+
'to ensure that a summed row of irradiance yields cumulative radiation over '
|
1053
|
+
'the entire time period of the matrix.'
|
1054
|
+
)
|
1055
|
+
@click.option(
|
1056
|
+
'--output-folder', '-of', help='Output folder.', default='.',
|
1057
|
+
type=click.Path(exists=False, file_okay=False, dir_okay=True, resolve_path=True)
|
1058
|
+
)
|
1059
|
+
def direct_sun_hours(
|
1060
|
+
input_matrix, divisor, output_folder
|
1061
|
+
):
|
1062
|
+
"""Post-process a Radiance matrix to direct sun hours and cumulative direct
|
1063
|
+
sun hours.
|
1064
|
+
|
1065
|
+
\b
|
1066
|
+
This command will convert values in the Radiance matrix file to 0-1 values.
|
1067
|
+
The output will be a direct sun hours file, and a cumulative direct sun hours
|
1068
|
+
file where the values are the summed values for each row.
|
1069
|
+
|
1070
|
+
Args:
|
1071
|
+
input-matrix: A Radiance matrix file.
|
1072
|
+
"""
|
1073
|
+
array = binary_to_array(input_matrix)
|
1074
|
+
|
1075
|
+
try:
|
1076
|
+
boolean_array = (array > 0) & (array <= np.inf)
|
1077
|
+
|
1078
|
+
direct_sun_hours_array = boolean_array.astype(np.uint8)
|
1079
|
+
cumulative_array = direct_sun_hours_array.sum(axis=1) / divisor
|
1080
|
+
|
1081
|
+
direct_sun_hours_file = Path(output_folder, 'direct_sun_hours')
|
1082
|
+
direct_sun_hours_file.parent.mkdir(parents=True, exist_ok=True)
|
1083
|
+
np.save(direct_sun_hours_file, direct_sun_hours_array)
|
1084
|
+
|
1085
|
+
cumulative_file = Path(output_folder, 'cumulative.res')
|
1086
|
+
cumulative_file.parent.mkdir(parents=True, exist_ok=True)
|
1087
|
+
np.savetxt(cumulative_file, cumulative_array, fmt='%.2f')
|
1088
|
+
except Exception:
|
1089
|
+
_logger.exception('Failed to convert the input file to direct sun hours.')
|
1090
|
+
sys.exit(1)
|
1091
|
+
else:
|
1092
|
+
sys.exit(0)
|