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.
Files changed (50) hide show
  1. honeybee_radiance_postprocess/__init__.py +1 -0
  2. honeybee_radiance_postprocess/__main__.py +4 -0
  3. honeybee_radiance_postprocess/annual.py +73 -0
  4. honeybee_radiance_postprocess/annualdaylight.py +289 -0
  5. honeybee_radiance_postprocess/annualirradiance.py +35 -0
  6. honeybee_radiance_postprocess/breeam/__init__.py +1 -0
  7. honeybee_radiance_postprocess/breeam/breeam.py +552 -0
  8. honeybee_radiance_postprocess/cli/__init__.py +33 -0
  9. honeybee_radiance_postprocess/cli/abnt.py +392 -0
  10. honeybee_radiance_postprocess/cli/breeam.py +96 -0
  11. honeybee_radiance_postprocess/cli/datacollection.py +133 -0
  12. honeybee_radiance_postprocess/cli/grid.py +295 -0
  13. honeybee_radiance_postprocess/cli/leed.py +143 -0
  14. honeybee_radiance_postprocess/cli/merge.py +161 -0
  15. honeybee_radiance_postprocess/cli/mtxop.py +161 -0
  16. honeybee_radiance_postprocess/cli/postprocess.py +1092 -0
  17. honeybee_radiance_postprocess/cli/schedule.py +103 -0
  18. honeybee_radiance_postprocess/cli/translate.py +216 -0
  19. honeybee_radiance_postprocess/cli/two_phase.py +252 -0
  20. honeybee_radiance_postprocess/cli/util.py +121 -0
  21. honeybee_radiance_postprocess/cli/viewfactor.py +157 -0
  22. honeybee_radiance_postprocess/cli/well.py +110 -0
  23. honeybee_radiance_postprocess/data_type.py +102 -0
  24. honeybee_radiance_postprocess/dynamic.py +273 -0
  25. honeybee_radiance_postprocess/electriclight.py +24 -0
  26. honeybee_radiance_postprocess/en17037.py +304 -0
  27. honeybee_radiance_postprocess/helper.py +266 -0
  28. honeybee_radiance_postprocess/ies/__init__.py +1 -0
  29. honeybee_radiance_postprocess/ies/lm.py +224 -0
  30. honeybee_radiance_postprocess/ies/lm_schedule.py +248 -0
  31. honeybee_radiance_postprocess/leed/__init__.py +1 -0
  32. honeybee_radiance_postprocess/leed/leed.py +801 -0
  33. honeybee_radiance_postprocess/leed/leed_schedule.py +256 -0
  34. honeybee_radiance_postprocess/metrics.py +439 -0
  35. honeybee_radiance_postprocess/reader.py +80 -0
  36. honeybee_radiance_postprocess/results/__init__.py +4 -0
  37. honeybee_radiance_postprocess/results/annual_daylight.py +752 -0
  38. honeybee_radiance_postprocess/results/annual_irradiance.py +196 -0
  39. honeybee_radiance_postprocess/results/results.py +1416 -0
  40. honeybee_radiance_postprocess/type_hints.py +38 -0
  41. honeybee_radiance_postprocess/util.py +211 -0
  42. honeybee_radiance_postprocess/vis_metadata.py +49 -0
  43. honeybee_radiance_postprocess/well/__init__.py +1 -0
  44. honeybee_radiance_postprocess/well/well.py +509 -0
  45. honeybee_radiance_postprocess-0.4.555.dist-info/METADATA +79 -0
  46. honeybee_radiance_postprocess-0.4.555.dist-info/RECORD +50 -0
  47. honeybee_radiance_postprocess-0.4.555.dist-info/WHEEL +5 -0
  48. honeybee_radiance_postprocess-0.4.555.dist-info/entry_points.txt +2 -0
  49. honeybee_radiance_postprocess-0.4.555.dist-info/licenses/LICENSE +661 -0
  50. 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)