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,752 @@
1
+ import json
2
+ from pathlib import Path
3
+ from typing import Tuple, List
4
+ try:
5
+ import cupy as np
6
+ is_gpu = True
7
+ except ImportError:
8
+ is_gpu = False
9
+ import numpy as np
10
+
11
+ from collections import defaultdict
12
+
13
+ from ladybug.analysisperiod import AnalysisPeriod
14
+ from ladybug.datacollection import HourlyContinuousCollection
15
+ from ladybug.datatype.illuminance import Illuminance
16
+ from ladybug.datatype.fraction import Fraction
17
+ from ladybug.header import Header
18
+
19
+ from ..annual import occupancy_schedule_8_to_6
20
+ from ..metrics import da_array2d, cda_array2d, udi_array2d, udi_lower_array2d, \
21
+ udi_upper_array2d, ase_array2d
22
+ from ..util import filter_array, filter_array2d
23
+ from ..annualdaylight import _annual_daylight_vis_metadata
24
+ from ..electriclight import array_to_dimming_fraction
25
+ from .. import type_hints
26
+ from ..dynamic import DynamicSchedule, ApertureGroupSchedule
27
+ from .results import Results
28
+
29
+ is_cpu = not is_gpu
30
+
31
+
32
+ class AnnualDaylight(Results):
33
+ """Annual Daylight Results class.
34
+
35
+ Args:
36
+ folder: Path to results folder.
37
+ schedule: 8760 values as a list. Values must be either 0 or 1. Values of 1
38
+ indicates occupied hours. If no schedule is provided a default schedule
39
+ will be used. (Default: None).
40
+ load_arrays: Set to True to load all NumPy arrays. If False the arrays will be
41
+ loaded only once they are needed. In both cases the loaded array(s) will be
42
+ stored in a dictionary under the arrays property. (Default: False).
43
+
44
+ Properties:
45
+ * schedule
46
+ * occ_pattern
47
+ * total_occ
48
+ * sun_down_occ_hours
49
+ * occ_mask
50
+ * arrays
51
+ * valid_states
52
+ * datatype
53
+ """
54
+ def __init__(self, folder, schedule: list = None, load_arrays: bool = False,
55
+ cache_arrays: bool = False):
56
+ """Initialize Results."""
57
+ Results.__init__(self, folder, datatype=Illuminance('Illuminance'),
58
+ schedule=schedule, unit='lux', load_arrays=load_arrays,
59
+ cache_arrays=cache_arrays)
60
+
61
+ def daylight_autonomy(
62
+ self, threshold: float = 300, states: DynamicSchedule = None,
63
+ grids_filter: str = '*') -> type_hints.annual_metric:
64
+ """Calculate daylight autonomy.
65
+
66
+ Args:
67
+ threshold: Threshold value for daylight autonomy. Defaults to 300.
68
+ states: A dictionary of states. Defaults to None.
69
+ grids_filter: The name of a grid or a pattern to filter the grids.
70
+ Defaults to '*'.
71
+
72
+ Returns:
73
+ Tuple: A tuple with the daylight autonomy and grid information.
74
+ """
75
+ grids_info = self._filter_grids(grids_filter=grids_filter)
76
+
77
+ da = []
78
+ for grid_info in grids_info:
79
+ array = self._array_from_states(grid_info, states=states, res_type='total')
80
+ if np.any(array):
81
+ array_filter = filter_array2d(array, mask=self.occ_mask)
82
+ results = da_array2d(
83
+ array_filter, total_occ=self.total_occ, threshold=threshold)
84
+ else:
85
+ results = np.zeros(grid_info['count'])
86
+ da.append(results)
87
+
88
+ return da, grids_info
89
+
90
+ def continuous_daylight_autonomy(
91
+ self, threshold: float = 300, states: DynamicSchedule = None,
92
+ grids_filter: str = '*') -> type_hints.annual_metric:
93
+ """Calculate continuous daylight autonomy.
94
+
95
+ Args:
96
+ threshold: Threshold value for daylight autonomy. Defaults to 300.
97
+ states: A dictionary of states. Defaults to None.
98
+ grids_filter: The name of a grid or a pattern to filter the grids.
99
+ Defaults to '*'.
100
+
101
+ Returns:
102
+ Tuple: A tuple with the continuous daylight autonomy and grid
103
+ information.
104
+ """
105
+ grids_info = self._filter_grids(grids_filter=grids_filter)
106
+
107
+ cda = []
108
+ for grid_info in grids_info:
109
+ array = self._array_from_states(grid_info, states=states, res_type='total')
110
+ if np.any(array):
111
+ array_filter = filter_array2d(array, mask=self.occ_mask)
112
+ results = cda_array2d(
113
+ array_filter, total_occ=self.total_occ, threshold=threshold)
114
+ else:
115
+ results = np.zeros(grid_info['count'])
116
+ cda.append(results)
117
+
118
+ return cda, grids_info
119
+
120
+ def useful_daylight_illuminance(
121
+ self, min_t: float = 100, max_t: float = 3000, states: DynamicSchedule = None,
122
+ grids_filter: str = '*') -> type_hints.annual_metric:
123
+ """Calculate useful daylight illuminance.
124
+
125
+ Args:
126
+ min_t: Minimum threshold for useful daylight illuminance. Defaults to 100.
127
+ max_t: Maximum threshold for useful daylight illuminance. Defaults to 3000.
128
+ states: A dictionary of states. Defaults to None.
129
+ grids_filter: The name of a grid or a pattern to filter the grids.
130
+ Defaults to '*'.
131
+
132
+ Returns:
133
+ Tuple: A tuple with the useful daylight illuminance and grid information.
134
+ """
135
+ grids_info = self._filter_grids(grids_filter=grids_filter)
136
+
137
+ udi = []
138
+ for grid_info in grids_info:
139
+ array = self._array_from_states(grid_info, states=states, res_type='total')
140
+ if np.any(array):
141
+ array_filter = filter_array2d(array, mask=self.occ_mask)
142
+ results = udi_array2d(
143
+ array_filter, total_occ=self.total_occ, min_t=min_t, max_t=max_t)
144
+ else:
145
+ results = np.zeros(grid_info['count'])
146
+ udi.append(results)
147
+
148
+ return udi, grids_info
149
+
150
+ def useful_daylight_illuminance_lower(
151
+ self, min_t: float = 100, states: DynamicSchedule = None,
152
+ grids_filter: str = '*') -> type_hints.annual_metric:
153
+ """Calculate lower than useful daylight illuminance.
154
+
155
+ Args:
156
+ min_t: Minimum threshold for useful daylight illuminance. Defaults to 100.
157
+ states: A dictionary of states. Defaults to None.
158
+ grids_filter: The name of a grid or a pattern to filter the grids.
159
+ Defaults to '*'.
160
+
161
+ Returns:
162
+ Tuple: A tuple with the lower than useful daylight illuminance and
163
+ grid information.
164
+ """
165
+ grids_info = self._filter_grids(grids_filter=grids_filter)
166
+ sun_down_occ_hours = self.sun_down_occ_hours
167
+
168
+ udi_lower = []
169
+ for grid_info in grids_info:
170
+ array = self._array_from_states(grid_info, states=states, res_type='total')
171
+ if np.any(array):
172
+ array_filter = filter_array2d(array, mask=self.occ_mask)
173
+ results = udi_lower_array2d(
174
+ array_filter, total_occ=self.total_occ,
175
+ min_t=min_t, sun_down_occ_hours=sun_down_occ_hours)
176
+ else:
177
+ results = np.zeros(grid_info['count'])
178
+ udi_lower.append(results)
179
+
180
+ return udi_lower, grids_info
181
+
182
+ def useful_daylight_illuminance_upper(
183
+ self, max_t: float = 3000, states: DynamicSchedule = None,
184
+ grids_filter: str = '*') -> type_hints.annual_metric:
185
+ """Calculate higher than useful daylight illuminance.
186
+
187
+ Args:
188
+ max_t: Maximum threshold for useful daylight illuminance. Defaults to 3000.
189
+ states: A dictionary of states. Defaults to None.
190
+ grids_filter: The name of a grid or a pattern to filter the grids.
191
+ Defaults to '*'.
192
+
193
+ Returns:
194
+ Tuple: A tuple with the higher than useful daylight illuminance and
195
+ grid information.
196
+ """
197
+ grids_info = self._filter_grids(grids_filter=grids_filter)
198
+
199
+ udi_upper = []
200
+ for grid_info in grids_info:
201
+ array = self._array_from_states(grid_info, states=states, res_type='total')
202
+ if np.any(array):
203
+ array_filter = filter_array2d(array, mask=self.occ_mask)
204
+ results = udi_upper_array2d(
205
+ array_filter, total_occ=self.total_occ, max_t=max_t)
206
+ else:
207
+ results = np.zeros(grid_info['count'])
208
+ udi_upper.append(results)
209
+
210
+ return udi_upper, grids_info
211
+
212
+ def annual_metrics(
213
+ self, threshold: float = 300, min_t: float = 100,
214
+ max_t: float = 3000, states: DynamicSchedule = None,
215
+ grids_filter: str = '*') -> type_hints.annual_daylight_metrics:
216
+ """Calculate multiple annual daylight metrics.
217
+
218
+ This method will calculate the following metrics:
219
+ * Daylight autonomy
220
+ * Continuous daylight autonomy
221
+ * Useful daylight illuminance
222
+ * Lower than useful daylight illuminance
223
+ * Higher than useful daylight illuminance
224
+
225
+ Args:
226
+ threshold: Threshold value for daylight autonomy. Defaults to 300.
227
+ min_t: Minimum threshold for useful daylight illuminance. Defaults to 100.
228
+ max_t: Maximum threshold for useful daylight illuminance. Defaults to 3000.
229
+ states: A dictionary of states. Defaults to None.
230
+ grids_filter: The name of a grid or a pattern to filter the grids.
231
+ Defaults to '*'.
232
+
233
+ Returns:
234
+ Tuple: A tuple with the five annual daylight metrics and grid information.
235
+ """
236
+ grids_info = self._filter_grids(grids_filter=grids_filter)
237
+ sun_down_occ_hours = self.sun_down_occ_hours
238
+
239
+ da = []
240
+ cda = []
241
+ udi = []
242
+ udi_lower = []
243
+ udi_upper = []
244
+ for grid_info in grids_info:
245
+ array = self._array_from_states(grid_info, states=states, res_type='total')
246
+ if np.any(array):
247
+ array_filter = filter_array2d(array, mask=self.occ_mask)
248
+ da_results = da_array2d(
249
+ array_filter, total_occ=self.total_occ, threshold=threshold)
250
+ cda_results = cda_array2d(
251
+ array_filter, total_occ=self.total_occ, threshold=threshold)
252
+ udi_results = udi_array2d(
253
+ array_filter, total_occ=self.total_occ, min_t=min_t, max_t=max_t)
254
+ udi_lower_results = udi_lower_array2d(
255
+ array_filter, total_occ=self.total_occ, min_t=min_t,
256
+ sun_down_occ_hours=sun_down_occ_hours)
257
+ udi_upper_results = udi_upper_array2d(
258
+ array_filter, total_occ=self.total_occ, max_t=max_t)
259
+ else:
260
+ da_results = cda_results = udi_results = udi_lower_results = \
261
+ udi_upper_results = np.zeros(grid_info['count'])
262
+ da.append(da_results)
263
+ cda.append(cda_results)
264
+ udi.append(udi_results)
265
+ udi_lower.append(udi_lower_results)
266
+ udi_upper.append(udi_upper_results)
267
+
268
+ return da, cda, udi, udi_lower, udi_upper, grids_info
269
+
270
+ def annual_metrics_to_folder(
271
+ self, target_folder: str, threshold: float = 300,
272
+ min_t: float = 100, max_t: float = 3000, states: DynamicSchedule = None,
273
+ grids_filter: str = '*'):
274
+ """Calculate and write multiple annual daylight metrics to a folder.
275
+
276
+ This method will calculate the following metrics:
277
+ * Daylight autonomy
278
+ * Continuous daylight autonomy
279
+ * Useful daylight illuminance
280
+ * Lower than useful daylight illuminance
281
+ * Higher than useful daylight illuminance
282
+
283
+ Args:
284
+ target_folder: Folder path to write annual metrics in. Usually this
285
+ folder is called 'metrics'.
286
+ threshold: Threshold value for daylight autonomy. Defaults to 300.
287
+ min_t: Minimum threshold for useful daylight illuminance. Defaults to 100.
288
+ max_t: Maximum threshold for useful daylight illuminance. Defaults to 3000.
289
+ states: A dictionary of states. Defaults to None.
290
+ grids_filter: The name of a grid or a pattern to filter the grids.
291
+ Defaults to '*'.
292
+ """
293
+ folder = Path(target_folder)
294
+ folder.mkdir(parents=True, exist_ok=True)
295
+
296
+ da, cda, udi, udi_lower, udi_upper, grids_info = self.annual_metrics(
297
+ threshold=threshold, min_t=min_t, max_t=max_t, states=states,
298
+ grids_filter=grids_filter)
299
+
300
+ pattern = {
301
+ 'da': da, 'cda': cda, 'udi_lower': udi_lower, 'udi': udi,
302
+ 'udi_upper': udi_upper
303
+ }
304
+ for metric, data in pattern.items():
305
+ metric_folder = folder.joinpath(metric)
306
+ extension = metric.split('_')[0]
307
+ for count, grid_info in enumerate(grids_info):
308
+ d = data[count]
309
+ full_id = grid_info['full_id']
310
+ output_file = metric_folder.joinpath(f'{full_id}.{extension}')
311
+ output_file.parent.mkdir(parents=True, exist_ok=True)
312
+ np.savetxt(output_file, d, fmt='%.2f')
313
+
314
+ for metric in pattern.keys():
315
+ info_file = folder.joinpath(metric, 'grids_info.json')
316
+ info_file.write_text(json.dumps(grids_info))
317
+
318
+ metric_info_dict = _annual_daylight_vis_metadata()
319
+ for metric, data in metric_info_dict.items():
320
+ vis_metadata_file = folder.joinpath(metric, 'vis_metadata.json')
321
+ vis_metadata_file.write_text(json.dumps(data, indent=4))
322
+
323
+ def spatial_daylight_autonomy(
324
+ self, threshold: float = 300, target_time: float = 50,
325
+ states: DynamicSchedule = None, grids_filter: str = '*'
326
+ ) -> type_hints.spatial_daylight_autonomy:
327
+ """Calculate spatial daylight autonomy.
328
+
329
+ Note: This component will only output a LEED compliant sDA if you've
330
+ run the simulation with blinds and blinds schedules as per the
331
+ IES-LM-83-12. Use the states option to calculate a LEED compliant sDA.
332
+
333
+ Args:
334
+ threshold: Threshold value for daylight autonomy. Defaults to 300.
335
+ target_time: A minimum threshold of occupied time (eg. 50% of the
336
+ time), above which a given sensor passes and contributes to the
337
+ spatial daylight autonomy. Defaults to 50.
338
+ states: A dictionary of states. Defaults to None.
339
+ grids_filter: The name of a grid or a pattern to filter the grids.
340
+ Defaults to '*'.
341
+
342
+ Returns:
343
+ Tuple: A tuple with the spatial daylight autonomy and grid
344
+ information.
345
+ """
346
+ da, grids_info = self.daylight_autonomy(
347
+ threshold=threshold, states=states, grids_filter=grids_filter)
348
+
349
+ sda = []
350
+ for array in da:
351
+ sda.append((array >= target_time).mean())
352
+
353
+ return sda, grids_info
354
+
355
+ def annual_sunlight_exposure(
356
+ self, direct_threshold: float = 1000, occ_hours: int = 250,
357
+ states: DynamicSchedule = None, grids_filter: str = '*'
358
+ ) -> type_hints.annual_sunlight_exposure:
359
+ """Calculate annual sunlight exposure.
360
+
361
+ Args:
362
+ direct_threshold: The threshold that determines if a sensor is
363
+ overlit. Defaults to 1000.
364
+ occ_hours: The number of occupied hours that cannot receive more
365
+ than the direct_threshold. Defaults to 250.
366
+ states: A dictionary of states. Defaults to None.
367
+ grids_filter: The name of a grid or a pattern to filter the grids.
368
+ Defaults to '*'.
369
+
370
+ Returns:
371
+ Tuple: A tuple with the annual sunlight exposure, the number of
372
+ hours that exceeds the direct threshold for each sensor, and
373
+ grid information.
374
+ """
375
+ grids_info = self._filter_grids(grids_filter=grids_filter)
376
+
377
+ ase = []
378
+ hours_above = []
379
+ for grid_info in grids_info:
380
+ array = self._array_from_states(
381
+ grid_info, states=states, res_type='direct')
382
+ if np.any(array):
383
+ array_filter = filter_array2d(array, mask=self.occ_mask)
384
+ results, h_above = ase_array2d(
385
+ array_filter, occ_hours=occ_hours,
386
+ direct_threshold=direct_threshold)
387
+ else:
388
+ results = np.float64(0)
389
+ h_above = np.zeros(grid_info['count'])
390
+ ase.append(results)
391
+ hours_above.append(h_above)
392
+
393
+ return ase, hours_above, grids_info
394
+
395
+ def annual_sunlight_exposure_to_folder(
396
+ self, target_folder: str, direct_threshold: float = 1000,
397
+ occ_hours: int = 250, states: DynamicSchedule = None,
398
+ grids_filter: str = '*'):
399
+ """Calculate and write annual sunlight exposure to a folder.
400
+
401
+ Args:
402
+ direct_threshold: The threshold that determines if a sensor is
403
+ overlit. Defaults to 1000.
404
+ occ_hours: The number of occupied hours that cannot receive more
405
+ than the direct_threshold. Defaults to 250.
406
+ states: A dictionary of states. Defaults to None.
407
+ grids_filter: The name of a grid or a pattern to filter the grids.
408
+ Defaults to '*'.
409
+ """
410
+ folder = Path(target_folder)
411
+ folder.mkdir(parents=True, exist_ok=True)
412
+
413
+ ase, hours_above, grids_info = self.annual_sunlight_exposure(
414
+ direct_threshold=direct_threshold, occ_hours=occ_hours,
415
+ states=states, grids_filter=grids_filter
416
+ )
417
+
418
+ pattern = {'ase': ase, 'hours_above': hours_above}
419
+ for metric, data in pattern.items():
420
+ metric_folder = folder.joinpath(metric)
421
+ if metric == 'hours_above':
422
+ extension = 'res'
423
+ else:
424
+ extension = 'ase'
425
+ for count, grid_info in enumerate(grids_info):
426
+ d = data[count]
427
+ full_id = grid_info['full_id']
428
+ output_file = metric_folder.joinpath(f'{full_id}.{extension}')
429
+ output_file.parent.mkdir(parents=True, exist_ok=True)
430
+ if metric == 'hours_above':
431
+ np.savetxt(output_file, d, fmt='%i')
432
+ elif metric == 'ase':
433
+ output_file.write_text('%.2f' % d)
434
+
435
+ for metric in pattern.keys():
436
+ info_file = folder.joinpath(metric, 'grids_info.json')
437
+ info_file.write_text(json.dumps(grids_info))
438
+
439
+ def daylight_control_schedules(
440
+ self, states: DynamicSchedule = None, grids_filter: str = '*',
441
+ base_schedule: list = None, ill_setpoint: float = 300,
442
+ min_power_in: float = 0.3, min_light_out: float = 0.2,
443
+ off_at_min: bool = False
444
+ ) -> Tuple[List[np.ndarray], List[dict]]:
445
+ """Generate electric lighting schedules from annual daylight results.
446
+
447
+ Such controls will dim the lights according to whether the illuminance values
448
+ at the sensor locations are at a target illuminance setpoint. The results can be
449
+ used to account for daylight controls in energy simulations.
450
+
451
+ This function will generate one schedule per sensor grid in the simulation. Each
452
+ grid should have sensors at the locations in space where daylight dimming sensors
453
+ are located. Grids with one, two, or more sensors can be used to model setups
454
+ where fractions of each room are controlled by different sensors. If the sensor
455
+ grids are distributed over the entire floor of the rooms, the resulting schedules
456
+ will be idealized, where light dimming has been optimized to supply the minimum
457
+ illuminance setpoint everywhere in the room.
458
+
459
+ Args:
460
+ states: A dictionary of states. Defaults to None.
461
+ grids_filter: The name of a grid or a pattern to filter the grids.
462
+ Defaults to '*'.
463
+ base_schedule: A list of 8760 fractional values for the lighting schedule
464
+ representing the usage of lights without any daylight controls. The
465
+ values of this schedule will be multiplied by the hourly dimming
466
+ fraction to yield the output lighting schedules. If None, a schedule
467
+ from 9AM to 5PM on weekdays will be used. (Default: None).
468
+ ill_setpoint: A number for the illuminance setpoint in lux beyond which
469
+ electric lights are dimmed if there is sufficient daylight.
470
+ Some common setpoints are listed below. (Default: 300 lux).
471
+
472
+ * 50 lux - Corridors and hallways.
473
+ * 150 lux - Computer work spaces (screens provide illumination).
474
+ * 300 lux - Paper work spaces (reading from surfaces that need illumination).
475
+ * 500 lux - Retail spaces or museums illuminating merchandise/artifacts.
476
+ * 1000 lux - Operating rooms and workshops where light is needed for safety.
477
+
478
+ min_power_in: A number between 0 and 1 for the the lowest power the lighting
479
+ system can dim down to, expressed as a fraction of maximum
480
+ input power. (Default: 0.3).
481
+ min_light_out: A number between 0 and 1 the lowest lighting output the lighting
482
+ system can dim down to, expressed as a fraction of maximum light
483
+ output. Note that setting this to 1 means lights aren't dimmed at
484
+ all until the illuminance setpoint is reached. This can be used to
485
+ approximate manual light-switching behavior when used in conjunction
486
+ with the off_at_min input below. (Default: 0.2).
487
+ off_at_min: Boolean to note whether lights should switch off completely when
488
+ they get to the minimum power input. (Default: False).
489
+
490
+ Returns:
491
+ A tuple with two values.
492
+
493
+ - schedules: A list of lists where each sub-list represents an electric
494
+ lighting dimming schedule for a sensor grid.
495
+
496
+ - grids_info: A list of grid information.
497
+ """
498
+ # process the base schedule input into a list of values
499
+ if base_schedule is None:
500
+ base_schedule = occupancy_schedule_8_to_6(timestep=self.timestep)
501
+ base_schedule = np.array(base_schedule)
502
+
503
+ grids_info = self._filter_grids(grids_filter=grids_filter)
504
+ sun_up_hours = [int(h) for h in self.sun_up_hours]
505
+
506
+ dim_fracts = []
507
+ for grid_info in grids_info:
508
+ array = self._array_from_states(
509
+ grid_info, states=states, res_type='total')
510
+ if np.any(array):
511
+ fract_list = array_to_dimming_fraction(
512
+ array, sun_up_hours, ill_setpoint, min_power_in,
513
+ min_light_out, off_at_min
514
+ )
515
+ else:
516
+ fract_list = np.ones(8760)
517
+ dim_fracts.append(fract_list)
518
+
519
+ schedules = []
520
+ for grid_info, dim_fract in zip(grids_info, dim_fracts):
521
+ sch_vals = base_schedule * dim_fract
522
+ schedules.append(sch_vals)
523
+
524
+ return schedules, grids_info
525
+
526
+ def daylight_control_schedules_to_folder(
527
+ self, target_folder: str, states: DynamicSchedule = None,
528
+ grids_filter: str = '*', base_schedule: list = None,
529
+ ill_setpoint: float = 300, min_power_in: float = 0.3,
530
+ min_light_out: float = 0.2, off_at_min: bool = False):
531
+ """Generate electric lighting schedules from annual daylight results and
532
+ write the schedules to a folder.
533
+
534
+ Such controls will dim the lights according to whether the illuminance values
535
+ at the sensor locations are at a target illuminance setpoint. The results can be
536
+ used to account for daylight controls in energy simulations.
537
+
538
+ This function will generate one schedule per sensor grid in the simulation. Each
539
+ grid should have sensors at the locations in space where daylight dimming sensors
540
+ are located. Grids with one, two, or more sensors can be used to model setups
541
+ where fractions of each room are controlled by different sensors. If the sensor
542
+ grids are distributed over the entire floor of the rooms, the resulting schedules
543
+ will be idealized, where light dimming has been optimized to supply the minimum
544
+ illuminance setpoint everywhere in the room.
545
+
546
+ Args:
547
+ states: A dictionary of states. Defaults to None.
548
+ grids_filter: The name of a grid or a pattern to filter the grids.
549
+ Defaults to '*'.
550
+ base_schedule: A list of 8760 fractional values for the lighting schedule
551
+ representing the usage of lights without any daylight controls. The
552
+ values of this schedule will be multiplied by the hourly dimming
553
+ fraction to yield the output lighting schedules. If None, a schedule
554
+ from 9AM to 5PM on weekdays will be used. (Default: None).
555
+ ill_setpoint: A number for the illuminance setpoint in lux beyond which
556
+ electric lights are dimmed if there is sufficient daylight.
557
+ Some common setpoints are listed below. (Default: 300 lux).
558
+
559
+ * 50 lux - Corridors and hallways.
560
+ * 150 lux - Computer work spaces (screens provide illumination).
561
+ * 300 lux - Paper work spaces (reading from surfaces that need illumination).
562
+ * 500 lux - Retail spaces or museums illuminating merchandise/artifacts.
563
+ * 1000 lux - Operating rooms and workshops where light is needed for safety.
564
+
565
+ min_power_in: A number between 0 and 1 for the the lowest power the lighting
566
+ system can dim down to, expressed as a fraction of maximum
567
+ input power. (Default: 0.3).
568
+ min_light_out: A number between 0 and 1 the lowest lighting output the lighting
569
+ system can dim down to, expressed as a fraction of maximum light
570
+ output. Note that setting this to 1 means lights aren't dimmed at
571
+ all until the illuminance setpoint is reached. This can be used to
572
+ approximate manual light-switching behavior when used in conjunction
573
+ with the off_at_min input below. (Default: 0.2).
574
+ off_at_min: Boolean to note whether lights should switch off completely when
575
+ they get to the minimum power input. (Default: False).
576
+ """
577
+ folder = Path(target_folder)
578
+ folder.mkdir(parents=True, exist_ok=True)
579
+
580
+ schedules, grids_info = self.daylight_control_schedules(
581
+ states=states, grids_filter=grids_filter,
582
+ base_schedule=base_schedule, ill_setpoint=ill_setpoint,
583
+ min_power_in=min_power_in, min_light_out=min_light_out,
584
+ off_at_min=off_at_min)
585
+
586
+ schedule_folder = folder.joinpath('control_schedules')
587
+
588
+ for count, grid_info in enumerate(grids_info):
589
+ d = schedules[count]
590
+ full_id = grid_info['full_id']
591
+ output_file = schedule_folder.joinpath(f'{full_id}.txt')
592
+ output_file.parent.mkdir(parents=True, exist_ok=True)
593
+ np.savetxt(output_file, d, fmt='%.2f')
594
+
595
+ info_file = schedule_folder.joinpath('grids_info.json')
596
+ info_file.write_text(json.dumps(grids_info))
597
+
598
+ def annual_uniformity_ratio(
599
+ self, threshold: float = 0.5, states: DynamicSchedule = None,
600
+ grids_filter: str = '*') -> type_hints.annual_uniformity_ratio:
601
+ """Calculate annual uniformity ratio.
602
+
603
+ Args:
604
+ threshold: A threshold for the uniformity ratio. Defaults to 0.5.
605
+ states: A dictionary of states. Defaults to None.
606
+ grids_filter: The name of a grid or a pattern to filter the grids.
607
+ Defaults to '*'.
608
+
609
+ Returns:
610
+ Tuple: A tuple with the annual uniformity ratio, annual
611
+ data collections, and grid information.
612
+ """
613
+ grids_info = self._filter_grids(grids_filter=grids_filter)
614
+ analysis_period = AnalysisPeriod(timestep=self.timestep)
615
+
616
+ data_collections = []
617
+ annual_uniformity_ratio = []
618
+ for grid_info in grids_info:
619
+ array = self._array_from_states(grid_info, states=states, res_type='total')
620
+ if np.any(array):
621
+ su_min_array = array.min(axis=0)
622
+ su_mean_array = array.mean(axis=0)
623
+ su_uniformity_ratio = su_min_array / su_mean_array
624
+
625
+ array_filter = filter_array2d(array, mask=self.occ_mask)
626
+ min_array = array_filter.min(axis=0)
627
+ mean_array = array_filter.mean(axis=0)
628
+ uniformity_ratio = min_array / mean_array
629
+ annual_uniformity_ratio.append(
630
+ np.float64(
631
+ (uniformity_ratio >= threshold).sum() / self.total_occ * 100
632
+ )
633
+ )
634
+ else:
635
+ su_uniformity_ratio = np.zeros(len(self.sun_up_hours))
636
+ annual_uniformity_ratio.append(np.float64(0))
637
+
638
+ annual_array = \
639
+ self.values_to_annual(
640
+ self.sun_up_hours, su_uniformity_ratio, self.timestep)
641
+ header = Header(Fraction(), '%', analysis_period)
642
+ header.metadata['sensor grid'] = grid_info['full_id']
643
+ data_collections.append(
644
+ HourlyContinuousCollection(header, annual_array.tolist()))
645
+
646
+ return annual_uniformity_ratio, data_collections, grids_info
647
+
648
+ def annual_uniformity_ratio_to_folder(
649
+ self, target_folder: str, threshold: float = 0.5,
650
+ states: DynamicSchedule = None, grids_filter: str = '*'
651
+ ):
652
+ """Calculate annual uniformity ratio and write it to a folder.
653
+
654
+ Args:
655
+ target_folder: Folder path to write annual uniformity ratio in.
656
+ threshold: A threshold for the uniformity ratio. Defaults to 0.5.
657
+ states: A dictionary of states. Defaults to None.
658
+ grids_filter: The name of a grid or a pattern to filter the grids.
659
+ Defaults to '*'.
660
+
661
+ Returns:
662
+ Tuple: A tuple with the daylight autonomy and grid information.
663
+ """
664
+ folder = Path(target_folder)
665
+ folder.mkdir(parents=True, exist_ok=True)
666
+
667
+ annual_uniformity_ratio, data_collections, grids_info = \
668
+ self.annual_uniformity_ratio(threshold=threshold, states=states,
669
+ grids_filter=grids_filter)
670
+
671
+ datacollection_folder = folder.joinpath('datacollections')
672
+ uniformity_ratio_folder = folder.joinpath('uniformity_ratio')
673
+
674
+ for aur, data_collection, grid_info in \
675
+ zip(annual_uniformity_ratio, data_collections, grids_info):
676
+ grid_id = grid_info['full_id']
677
+ data_dict = data_collection.to_dict()
678
+ data_file = datacollection_folder.joinpath(f'{grid_id}.json')
679
+ data_file.parent.mkdir(parents=True, exist_ok=True)
680
+ data_file.write_text(json.dumps(data_dict))
681
+
682
+ aur_file = uniformity_ratio_folder.joinpath(f'{grid_id}.ur')
683
+ aur_file.parent.mkdir(parents=True, exist_ok=True)
684
+ aur_file.write_text(str(round(aur, 2)))
685
+
686
+ info_file = uniformity_ratio_folder.joinpath('grids_info.json')
687
+ info_file.write_text(json.dumps(grids_info))
688
+
689
+ def dynamic_schedule_from_sensor_maximum(
690
+ self, sensor_index: dict, grids_filter: str = '*',
691
+ maximum: float = 3000, res_type: str = 'total') -> DynamicSchedule:
692
+ """Calculate a DynamicSchedule from a sensor and a maximum allowed
693
+ illuminance.
694
+
695
+ Args:
696
+ sensor_index: A dictionary with grids as keys and a list of sensor
697
+ indices as values. Defaults to None.
698
+ grids_filter: The name of a grid or a pattern to filter the grids.
699
+ Defaults to '*'.
700
+ maximum: A float value of the maximum illuminance allowed for the
701
+ sensor.
702
+ res_type: Type of results to load. Defaults to 'total'.
703
+
704
+ Returns:
705
+ DynamicSchedule object.
706
+ """
707
+ grids_info = self._filter_grids(grids_filter=grids_filter)
708
+
709
+ aperture_group_schedules = []
710
+ for grid_info in grids_info:
711
+ control_sensor = sensor_index.get(grid_info['full_id'], None)
712
+ if control_sensor is None:
713
+ continue
714
+ assert len(control_sensor) == 1, ('Expected one control sensor for '
715
+ f'grid {grid_info["name"]}. Received {len(control_sensor)} '
716
+ 'control sensors.')
717
+ control_sensor_index = control_sensor[0]
718
+
719
+ combinations = self._get_state_combinations(grid_info)
720
+
721
+ array_list_combinations = []
722
+ for combination in combinations:
723
+ combination_arrays = []
724
+ for light_path, state_index in combination.items():
725
+ array = self._get_array(
726
+ grid_info, light_path, state=state_index, res_type=res_type)
727
+ sensor_array = array[control_sensor_index,:]
728
+ combination_arrays.append(sensor_array)
729
+ combination_array = sum(combination_arrays)
730
+ array_list_combinations.append(combination_array)
731
+ array_combinations = np.array(array_list_combinations)
732
+ array_combinations[array_combinations > maximum] = -np.inf
733
+ max_indices = array_combinations.argmax(axis=0)
734
+ combinations = [combinations[idx] for idx in max_indices]
735
+
736
+ states_schedule = defaultdict(list)
737
+ for combination in combinations:
738
+ for light_path, state_index in combination.items():
739
+ if light_path != '__static_apertures__':
740
+ states_schedule[light_path].append(state_index)
741
+
742
+ # map states to 8760 values
743
+ for light_path, state_indices in states_schedule.items():
744
+ mapped_states = self.values_to_annual(
745
+ self.sun_up_hours, state_indices, self.timestep)
746
+ mapped_states = mapped_states.astype(int)
747
+ aperture_group_schedules.append(
748
+ ApertureGroupSchedule(light_path, mapped_states.tolist()))
749
+
750
+ dyn_sch = DynamicSchedule.from_group_schedules(aperture_group_schedules)
751
+
752
+ return dyn_sch