diff --git a/lbt_recipes/annual_daylight/flow/dependencies/dependencies/__init__.py b/lbt_recipes/annual_daylight/flow/dependencies/dependencies/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/lbt_recipes/annual_daylight/flow/dependencies/dependencies/__init__.py @@ -0,0 +1 @@ + diff --git a/lbt_recipes/annual_daylight/flow/dependencies/annual_daylight_ray_tracing.py b/lbt_recipes/annual_daylight/flow/dependencies/dependencies/annual_daylight_ray_tracing.py similarity index 83% rename from lbt_recipes/annual_daylight/flow/dependencies/annual_daylight_ray_tracing.py rename to lbt_recipes/annual_daylight/flow/dependencies/dependencies/annual_daylight_ray_tracing.py index 97e56245..0f1dd11a 100644 --- a/lbt_recipes/annual_daylight/flow/dependencies/annual_daylight_ray_tracing.py +++ b/lbt_recipes/annual_daylight/flow/dependencies/dependencies/annual_daylight_ray_tracing.py @@ -22,6 +22,7 @@ _default_inputs = { 'bsdfs': None, 'grid_name': None, 'octree_file': None, + 'octree_file_direct': None, 'octree_file_with_suns': None, 'params_folder': '__params', 'radiance_parameters': '-ab 2 -ad 5000 -lw 2e-05', @@ -81,7 +82,7 @@ def sensor_grid(self): @property def scene_file(self): - value = pathlib.Path(self._input_params['octree_file']) + value = pathlib.Path(self._input_params['octree_file_direct']) return value.as_posix() if value.is_absolute() \ else pathlib.Path(self.initiation_folder, value).resolve().as_posix() @@ -238,8 +239,8 @@ def output_artifacts(self): }] -class OutputMatrixMath(QueenbeeTask): - """Remove direct sky from total sky and add direct sun.""" +class DirectSunlightToNpy(QueenbeeTask): + """Convert binary Radiance matrix file to NumPy file.""" # DAG Input parameters _input_params = luigi.DictParameter() @@ -253,9 +254,67 @@ def name(self): def conversion(self): return '47.4 119.9 11.6' - header = luigi.Parameter(default='remove') + @property + def matrix_file(self): + value = pathlib.Path(self.input()['DirectSunlight']['result_file'].path) + return value.as_posix() if value.is_absolute() \ + else pathlib.Path(self.initiation_folder, value).resolve().as_posix() - output_format = luigi.Parameter(default='a') + @property + def execution_folder(self): + return pathlib.Path(self._input_params['simulation_folder']).as_posix() + + @property + def initiation_folder(self): + return pathlib.Path(self._input_params['simulation_folder']).as_posix() + + @property + def params_folder(self): + return pathlib.Path(self.execution_folder, self._input_params['params_folder']).resolve().as_posix() + + def command(self): + return 'honeybee-radiance-postprocess translate binary-to-npy "{matrix_file}" --conversion "{conversion}" --name output'.format(matrix_file=self.matrix_file, conversion=self.conversion) + + def requires(self): + return {'DirectSunlight': DirectSunlight(_input_params=self._input_params)} + + def output(self): + return { + 'output_file': luigi.LocalTarget( + pathlib.Path(self.execution_folder, '../final/direct/{name}.ill'.format(name=self.name)).resolve().as_posix() + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'matrix_file', 'to': 'input.ill', 'from': self.matrix_file, 'optional': False}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'output-file', 'from': 'output.npy', + 'to': pathlib.Path(self.execution_folder, '../final/direct/{name}.ill'.format(name=self.name)).resolve().as_posix(), + 'optional': False, + 'type': 'file' + }] + + +class OutputMatrixMath(QueenbeeTask): + """Multiply a matrix with conversation numbers.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def name(self): + return self._input_params['grid_name'] + + @property + def conversion(self): + return '47.4 119.9 11.6' @property def direct_sky_matrix(self): @@ -288,7 +347,7 @@ def params_folder(self): return pathlib.Path(self.execution_folder, self._input_params['params_folder']).resolve().as_posix() def command(self): - return 'honeybee-radiance mtxop operate-three sky.ill sky_dir.ill sun.ill --operator-one "-" --operator-two "+" --{header}-header --conversion "{conversion}" --output-mtx final.ill --output-format {output_format}'.format(header=self.header, conversion=self.conversion, output_format=self.output_format) + return 'honeybee-radiance-postprocess mtxop operate-three "{total_sky_matrix}" "{direct_sky_matrix}" "{sunlight_matrix}" --operator-one - --operator-two + --conversion "{conversion}" --name output'.format(total_sky_matrix=self.total_sky_matrix, direct_sky_matrix=self.direct_sky_matrix, sunlight_matrix=self.sunlight_matrix, conversion=self.conversion) def requires(self): return {'DirectSunlight': DirectSunlight(_input_params=self._input_params), 'TotalSky': TotalSky(_input_params=self._input_params), 'DirectSky': DirectSky(_input_params=self._input_params)} @@ -296,7 +355,7 @@ def requires(self): def output(self): return { 'results_file': luigi.LocalTarget( - pathlib.Path(self.execution_folder, '../final/{name}.ill'.format(name=self.name)).resolve().as_posix() + pathlib.Path(self.execution_folder, '../final/total/{name}.ill'.format(name=self.name)).resolve().as_posix() ) } @@ -311,8 +370,8 @@ def input_artifacts(self): def output_artifacts(self): return [ { - 'name': 'results-file', 'from': 'final.ill', - 'to': pathlib.Path(self.execution_folder, '../final/{name}.ill'.format(name=self.name)).resolve().as_posix(), + 'name': 'results-file', 'from': 'output.npy', + 'to': pathlib.Path(self.execution_folder, '../final/total/{name}.ill'.format(name=self.name)).resolve().as_posix(), 'optional': False, 'type': 'file' }] @@ -422,7 +481,7 @@ def output_artifacts(self): }] -class _AnnualDaylightRayTracing_ae044755Orchestrator(luigi.WrapperTask): +class _AnnualDaylightRayTracing_8529a1a1Orchestrator(luigi.WrapperTask): """Runs all the tasks in this module.""" # user input for this module _input_params = luigi.DictParameter() @@ -434,4 +493,4 @@ def input_values(self): return params def requires(self): - yield [OutputMatrixMath(_input_params=self.input_values)] + yield [DirectSunlightToNpy(_input_params=self.input_values), OutputMatrixMath(_input_params=self.input_values)] diff --git a/lbt_recipes/annual_daylight/flow/dependencies/two_phase.py b/lbt_recipes/annual_daylight/flow/dependencies/two_phase.py new file mode 100644 index 00000000..1409dd7c --- /dev/null +++ b/lbt_recipes/annual_daylight/flow/dependencies/two_phase.py @@ -0,0 +1,395 @@ +""" +This file is auto-generated from a Queenbee recipe. It is unlikely that +you should be editing this file directly. Instead try to edit the recipe +itself and regenerate the code. + +Contact the recipe maintainers with additional questions. + mostapha: mostapha@ladybug.tools + ladybug-tools: info@ladybug.tools + +This file is licensed under "PolyForm Shield License 1.0.0". +See https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt for more information. +""" + + +import luigi +import os +import pathlib +from queenbee_local import QueenbeeTask +from queenbee_local import load_input_param as qb_load_input_param +from .dependencies.annual_daylight_ray_tracing import _AnnualDaylightRayTracing_8529a1a1Orchestrator as AnnualDaylightRayTracing_8529a1a1Workerbee + + +_default_inputs = { 'bsdf_folder': None, + 'direct_sky': None, + 'identifier': '__static__', + 'light_path': '__static__', + 'octree_file': None, + 'octree_file_direct': None, + 'octree_file_with_suns': None, + 'params_folder': '__params', + 'radiance_parameters': '-ab 2 -ad 5000 -lw 2e-05', + 'results_folder': 'results', + 'sensor_grids_folder': None, + 'sensor_grids_info': [], + 'simulation_folder': '.', + 'sky_dome': None, + 'sun_modifiers': None, + 'total_sky': None} + + +class RestructureDirectSunlightResults(QueenbeeTask): + """Restructure files in a distributed folder.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def identifier(self): + return self._input_params['identifier'] + + @property + def light_path(self): + return self._input_params['light_path'] + + @property + def extension(self): + return 'ill' + + @property + def results_folder(self): + return self._input_params['results_folder'] + + @property + def input_folder(self): + value = pathlib.Path('initial_results/final/direct') + return value.as_posix() if value.is_absolute() \ + else pathlib.Path(self.initiation_folder, value).resolve().as_posix() + + @property + def dist_info(self): + try: + pathlib.Path(self._input_params['sensor_grids_folder'], '_redist_info.json') + except TypeError: + # optional artifact + return None + value = pathlib.Path(self._input_params['sensor_grids_folder'], '_redist_info.json') + return value.as_posix() if value.is_absolute() \ + else pathlib.Path(self.initiation_folder, value).resolve().as_posix() + + @property + def execution_folder(self): + return pathlib.Path(self._input_params['simulation_folder']).as_posix() + + @property + def initiation_folder(self): + return pathlib.Path(self._input_params['simulation_folder']).as_posix() + + @property + def params_folder(self): + return pathlib.Path(self.execution_folder, self._input_params['params_folder']).resolve().as_posix() + + def command(self): + return 'honeybee-radiance-postprocess grid merge-folder ./input_folder ./output_folder {extension} --dist-info dist_info.json'.format(extension=self.extension) + + def requires(self): + return {'TwoPhaseRaytracing': TwoPhaseRaytracing(_input_params=self._input_params)} + + def output(self): + return { + 'output_folder': luigi.LocalTarget( + pathlib.Path(self.execution_folder, '{results_folder}/{light_path}/{identifier}/direct'.format(results_folder=self.results_folder, light_path=self.light_path, identifier=self.identifier)).resolve().as_posix() + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'input_folder', 'to': 'input_folder', 'from': self.input_folder, 'optional': False}, + {'name': 'dist_info', 'to': 'dist_info.json', 'from': self.dist_info, 'optional': True}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'output-folder', 'from': 'output_folder', + 'to': pathlib.Path(self.execution_folder, '{results_folder}/{light_path}/{identifier}/direct'.format(results_folder=self.results_folder, light_path=self.light_path, identifier=self.identifier)).resolve().as_posix(), + 'optional': False, + 'type': 'folder' + }] + + +class RestructureTotalResults(QueenbeeTask): + """Restructure files in a distributed folder.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def identifier(self): + return self._input_params['identifier'] + + @property + def light_path(self): + return self._input_params['light_path'] + + @property + def extension(self): + return 'ill' + + @property + def results_folder(self): + return self._input_params['results_folder'] + + @property + def input_folder(self): + value = pathlib.Path('initial_results/final/total') + return value.as_posix() if value.is_absolute() \ + else pathlib.Path(self.initiation_folder, value).resolve().as_posix() + + @property + def dist_info(self): + try: + pathlib.Path(self._input_params['sensor_grids_folder'], '_redist_info.json') + except TypeError: + # optional artifact + return None + value = pathlib.Path(self._input_params['sensor_grids_folder'], '_redist_info.json') + return value.as_posix() if value.is_absolute() \ + else pathlib.Path(self.initiation_folder, value).resolve().as_posix() + + @property + def execution_folder(self): + return pathlib.Path(self._input_params['simulation_folder']).as_posix() + + @property + def initiation_folder(self): + return pathlib.Path(self._input_params['simulation_folder']).as_posix() + + @property + def params_folder(self): + return pathlib.Path(self.execution_folder, self._input_params['params_folder']).resolve().as_posix() + + def command(self): + return 'honeybee-radiance-postprocess grid merge-folder ./input_folder ./output_folder {extension} --dist-info dist_info.json'.format(extension=self.extension) + + def requires(self): + return {'TwoPhaseRaytracing': TwoPhaseRaytracing(_input_params=self._input_params)} + + def output(self): + return { + 'output_folder': luigi.LocalTarget( + pathlib.Path(self.execution_folder, '{results_folder}/{light_path}/{identifier}/total'.format(results_folder=self.results_folder, light_path=self.light_path, identifier=self.identifier)).resolve().as_posix() + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'input_folder', 'to': 'input_folder', 'from': self.input_folder, 'optional': False}, + {'name': 'dist_info', 'to': 'dist_info.json', 'from': self.dist_info, 'optional': True}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'output-folder', 'from': 'output_folder', + 'to': pathlib.Path(self.execution_folder, '{results_folder}/{light_path}/{identifier}/total'.format(results_folder=self.results_folder, light_path=self.light_path, identifier=self.identifier)).resolve().as_posix(), + 'optional': False, + 'type': 'folder' + }] + + +class TwoPhaseRaytracingLoop(luigi.Task): + """No description is provided.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def radiance_parameters(self): + return self._input_params['radiance_parameters'] + + @property + def grid_name(self): + return self.item['full_id'] + + @property + def sensor_count(self): + return self.item['count'] + + @property + def octree_file(self): + value = pathlib.Path(self._input_params['octree_file']) + return value.as_posix() if value.is_absolute() \ + else pathlib.Path(self.initiation_folder, value).resolve().as_posix() + + @property + def octree_file_direct(self): + value = pathlib.Path(self._input_params['octree_file_direct']) + return value.as_posix() if value.is_absolute() \ + else pathlib.Path(self.initiation_folder, value).resolve().as_posix() + + @property + def octree_file_with_suns(self): + value = pathlib.Path(self._input_params['octree_file_with_suns']) + return value.as_posix() if value.is_absolute() \ + else pathlib.Path(self.initiation_folder, value).resolve().as_posix() + + @property + def sensor_grid(self): + value = pathlib.Path(self._input_params['sensor_grids_folder'], '{item_full_id}.pts'.format(item_full_id=self.item['full_id'])) + return value.as_posix() if value.is_absolute() \ + else pathlib.Path(self.initiation_folder, value).resolve().as_posix() + + @property + def sky_matrix(self): + value = pathlib.Path(self._input_params['total_sky']) + return value.as_posix() if value.is_absolute() \ + else pathlib.Path(self.initiation_folder, value).resolve().as_posix() + + @property + def sky_matrix_direct(self): + value = pathlib.Path(self._input_params['direct_sky']) + return value.as_posix() if value.is_absolute() \ + else pathlib.Path(self.initiation_folder, value).resolve().as_posix() + + @property + def sky_dome(self): + value = pathlib.Path(self._input_params['sky_dome']) + return value.as_posix() if value.is_absolute() \ + else pathlib.Path(self.initiation_folder, value).resolve().as_posix() + + @property + def sun_modifiers(self): + value = pathlib.Path(self._input_params['sun_modifiers']) + return value.as_posix() if value.is_absolute() \ + else pathlib.Path(self.initiation_folder, value).resolve().as_posix() + + @property + def bsdfs(self): + try: + pathlib.Path(self._input_params['bsdf_folder']) + except TypeError: + # optional artifact + return None + value = pathlib.Path(self._input_params['bsdf_folder']) + return value.as_posix() if value.is_absolute() \ + else pathlib.Path(self.initiation_folder, value).resolve().as_posix() + + # get item for loop + try: + item = luigi.DictParameter() + except Exception: + item = luigi.Parameter() + + @property + def execution_folder(self): + return pathlib.Path(self._input_params['simulation_folder'], 'initial_results/{item_full_id}'.format(item_full_id=self.item['full_id'])).resolve().as_posix() + + @property + def initiation_folder(self): + return pathlib.Path(self._input_params['simulation_folder']).as_posix() + + @property + def params_folder(self): + return pathlib.Path(self.execution_folder, self._input_params['params_folder']).resolve().as_posix() + + @property + def map_dag_inputs(self): + """Map task inputs to DAG inputs.""" + inputs = { + 'simulation_folder': self.execution_folder, + 'radiance_parameters': self.radiance_parameters, + 'octree_file': self.octree_file, + 'octree_file_direct': self.octree_file_direct, + 'octree_file_with_suns': self.octree_file_with_suns, + 'grid_name': self.grid_name, + 'sensor_grid': self.sensor_grid, + 'sensor_count': self.sensor_count, + 'sky_matrix': self.sky_matrix, + 'sky_matrix_direct': self.sky_matrix_direct, + 'sky_dome': self.sky_dome, + 'sun_modifiers': self.sun_modifiers, + 'bsdfs': self.bsdfs + } + try: + inputs['__debug__'] = self._input_params['__debug__'] + except KeyError: + # not debug mode + pass + + return inputs + + def run(self): + yield [AnnualDaylightRayTracing_8529a1a1Workerbee(_input_params=self.map_dag_inputs)] + done_file = pathlib.Path(self.execution_folder, 'two_phase_raytracing.done') + done_file.parent.mkdir(parents=True, exist_ok=True) + done_file.write_text('done!') + + def output(self): + return { + 'is_done': luigi.LocalTarget(pathlib.Path(self.execution_folder, 'two_phase_raytracing.done').resolve().as_posix()) + } + + +class TwoPhaseRaytracing(luigi.Task): + """No description is provided.""" + # global parameters + _input_params = luigi.DictParameter() + @property + def sensor_grids_info(self): + value = pathlib.Path(self._input_params['sensor_grids_info']) + return value.as_posix() if value.is_absolute() \ + else pathlib.Path(self.initiation_folder, value).resolve().as_posix() + + @property + def items(self): + try: + # assume the input is a file + return qb_load_input_param(self.sensor_grids_info) + except: + # it is a parameter + return self._input_params['sensor_grids_info'] + + def run(self): + yield [TwoPhaseRaytracingLoop(item=item, _input_params=self._input_params) for item in self.items] + done_file = pathlib.Path(self.execution_folder, 'two_phase_raytracing.done') + done_file.parent.mkdir(parents=True, exist_ok=True) + done_file.write_text('done!') + + @property + def initiation_folder(self): + return pathlib.Path(self._input_params['simulation_folder']).as_posix() + + @property + def execution_folder(self): + return pathlib.Path(self._input_params['simulation_folder']).as_posix() + + @property + def params_folder(self): + return pathlib.Path(self.execution_folder, self._input_params['params_folder']).resolve().as_posix() + + + def output(self): + return { + 'is_done': luigi.LocalTarget(pathlib.Path(self.execution_folder, 'two_phase_raytracing.done').resolve().as_posix()) + } + + +class _TwoPhase_8529a1a1Orchestrator(luigi.WrapperTask): + """Runs all the tasks in this module.""" + # user input for this module + _input_params = luigi.DictParameter() + + @property + def input_values(self): + params = dict(_default_inputs) + params.update(dict(self._input_params)) + return params + + def requires(self): + yield [RestructureDirectSunlightResults(_input_params=self.input_values), RestructureTotalResults(_input_params=self.input_values)] diff --git a/lbt_recipes/annual_daylight/flow/main.py b/lbt_recipes/annual_daylight/flow/main.py index 87b4f2f7..f84c8b20 100644 --- a/lbt_recipes/annual_daylight/flow/main.py +++ b/lbt_recipes/annual_daylight/flow/main.py @@ -17,7 +17,7 @@ import pathlib from queenbee_local import QueenbeeTask from queenbee_local import load_input_param as qb_load_input_param -from .dependencies.annual_daylight_ray_tracing import _AnnualDaylightRayTracing_ae044755Orchestrator as AnnualDaylightRayTracing_ae044755Workerbee +from .dependencies.two_phase import _TwoPhase_8529a1a1Orchestrator as TwoPhase_8529a1a1Workerbee _default_inputs = { 'cpu_count': 50, @@ -33,46 +33,124 @@ 'wea': None} -class AnnualDaylightRaytracingLoop(luigi.Task): +class CalculateAnnualMetrics(QueenbeeTask): + """Calculate annual daylight metrics for annual daylight simulation.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def thresholds(self): + return self._input_params['thresholds'] + + @property + def folder(self): + value = pathlib.Path('results') + return value.as_posix() if value.is_absolute() \ + else pathlib.Path(self.initiation_folder, value).resolve().as_posix() + + @property + def schedule(self): + try: + pathlib.Path(self._input_params['schedule']) + except TypeError: + # optional artifact + return None + value = pathlib.Path(self._input_params['schedule']) + return value.as_posix() if value.is_absolute() \ + else pathlib.Path(self.initiation_folder, value).resolve().as_posix() + + @property + def execution_folder(self): + return pathlib.Path(self._input_params['simulation_folder']).as_posix() + + @property + def initiation_folder(self): + return pathlib.Path(self._input_params['simulation_folder']).as_posix() + + @property + def params_folder(self): + return pathlib.Path(self.execution_folder, self._input_params['params_folder']).resolve().as_posix() + + def command(self): + return 'honeybee-radiance-postprocess post-process annual-daylight raw_results --schedule schedule.txt {thresholds} --sub-folder metrics'.format(thresholds=self.thresholds) + + def requires(self): + return {'CalculateTwoPhaseMatrix': CalculateTwoPhaseMatrix(_input_params=self._input_params)} + + def output(self): + return { + 'annual_metrics': luigi.LocalTarget( + pathlib.Path(self.execution_folder, 'metrics').resolve().as_posix() + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'folder', 'to': 'raw_results', 'from': self.folder, 'optional': False}, + {'name': 'schedule', 'to': 'schedule.txt', 'from': self.schedule, 'optional': True}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'annual-metrics', 'from': 'metrics', + 'to': pathlib.Path(self.execution_folder, 'metrics').resolve().as_posix(), + 'optional': False, + 'type': 'folder' + }] + + +class CalculateTwoPhaseMatrixLoop(luigi.Task): """No description is provided.""" # DAG Input parameters _input_params = luigi.DictParameter() # Task inputs + @property + def identifier(self): + return self.item['identifier'] + + @property + def light_path(self): + return self.item['light_path'] + @property def radiance_parameters(self): return self._input_params['radiance_parameters'] @property - def grid_name(self): - return self.item['full_id'] + def sensor_grids_info(self): + return self.item['sensor_grids_info'] @property - def sensor_count(self): - return self.item['count'] + def results_folder(self): + return '../../../results' @property - def octree_file_with_suns(self): - value = pathlib.Path(self.input()['CreateOctreeWithSuns']['scene_file'].path) + def sensor_grids_folder(self): + value = pathlib.Path(self.input()['PrepareMultiphase']['grid_folder'].path, self.item['sensor_grids_folder']) return value.as_posix() if value.is_absolute() \ else pathlib.Path(self.initiation_folder, value).resolve().as_posix() @property def octree_file(self): - value = pathlib.Path(self.input()['CreateOctree']['scene_file'].path) + value = pathlib.Path(self.input()['PrepareMultiphase']['scene_folder'].path, self.item['octree']) return value.as_posix() if value.is_absolute() \ else pathlib.Path(self.initiation_folder, value).resolve().as_posix() @property - def sensor_grid(self): - value = pathlib.Path(self.input()['SplitGridFolder']['output_folder'].path, '{item_full_id}.pts'.format(item_full_id=self.item['full_id'])) + def octree_file_direct(self): + value = pathlib.Path(self.input()['PrepareMultiphase']['scene_folder'].path, self.item['octree_direct']) return value.as_posix() if value.is_absolute() \ else pathlib.Path(self.initiation_folder, value).resolve().as_posix() @property - def sky_matrix(self): - value = pathlib.Path(self.input()['CreateTotalSky']['sky_matrix'].path) + def octree_file_with_suns(self): + value = pathlib.Path(self.input()['PrepareMultiphase']['scene_folder'].path, self.item['octree_direct_sun']) return value.as_posix() if value.is_absolute() \ else pathlib.Path(self.initiation_folder, value).resolve().as_posix() @@ -83,14 +161,14 @@ def sky_dome(self): else pathlib.Path(self.initiation_folder, value).resolve().as_posix() @property - def sky_matrix_direct(self): - value = pathlib.Path(self.input()['CreateDirectSky']['sky_matrix'].path) + def total_sky(self): + value = pathlib.Path(self.input()['CreateTotalSky']['sky_matrix'].path) return value.as_posix() if value.is_absolute() \ else pathlib.Path(self.initiation_folder, value).resolve().as_posix() @property - def sunpath(self): - value = pathlib.Path(self.input()['GenerateSunpath']['sunpath'].path) + def direct_sky(self): + value = pathlib.Path(self.input()['CreateDirectSky']['sky_matrix'].path) return value.as_posix() if value.is_absolute() \ else pathlib.Path(self.initiation_folder, value).resolve().as_posix() @@ -101,7 +179,7 @@ def sun_modifiers(self): else pathlib.Path(self.initiation_folder, value).resolve().as_posix() @property - def bsdfs(self): + def bsdf_folder(self): try: pathlib.Path(self.input()['CreateRadFolder']['bsdf_folder'].path) except TypeError: @@ -119,7 +197,7 @@ def bsdfs(self): @property def execution_folder(self): - return pathlib.Path(self._input_params['simulation_folder'], 'initial_results/{item_full_id}'.format(item_full_id=self.item['full_id'])).resolve().as_posix() + return pathlib.Path(self._input_params['simulation_folder'], 'calcs/2_phase/{item_identifier}'.format(item_identifier=self.item['identifier'])).resolve().as_posix() @property def initiation_folder(self): @@ -134,18 +212,20 @@ def map_dag_inputs(self): """Map task inputs to DAG inputs.""" inputs = { 'simulation_folder': self.execution_folder, + 'identifier': self.identifier, + 'light_path': self.light_path, 'radiance_parameters': self.radiance_parameters, - 'octree_file_with_suns': self.octree_file_with_suns, + 'sensor_grids_info': self.sensor_grids_info, + 'sensor_grids_folder': self.sensor_grids_folder, 'octree_file': self.octree_file, - 'grid_name': self.grid_name, - 'sensor_grid': self.sensor_grid, - 'sensor_count': self.sensor_count, - 'sky_matrix': self.sky_matrix, + 'octree_file_direct': self.octree_file_direct, + 'octree_file_with_suns': self.octree_file_with_suns, 'sky_dome': self.sky_dome, - 'sky_matrix_direct': self.sky_matrix_direct, - 'sunpath': self.sunpath, + 'total_sky': self.total_sky, + 'direct_sky': self.direct_sky, 'sun_modifiers': self.sun_modifiers, - 'bsdfs': self.bsdfs + 'bsdf_folder': self.bsdf_folder, + 'results_folder': self.results_folder } try: inputs['__debug__'] = self._input_params['__debug__'] @@ -156,27 +236,27 @@ def map_dag_inputs(self): return inputs def run(self): - yield [AnnualDaylightRayTracing_ae044755Workerbee(_input_params=self.map_dag_inputs)] - done_file = pathlib.Path(self.execution_folder, 'annual_daylight_raytracing.done') + yield [TwoPhase_8529a1a1Workerbee(_input_params=self.map_dag_inputs)] + done_file = pathlib.Path(self.execution_folder, 'calculate_two_phase_matrix.done') done_file.parent.mkdir(parents=True, exist_ok=True) done_file.write_text('done!') def requires(self): - return {'CreateSkyDome': CreateSkyDome(_input_params=self._input_params), 'CreateOctreeWithSuns': CreateOctreeWithSuns(_input_params=self._input_params), 'CreateOctree': CreateOctree(_input_params=self._input_params), 'GenerateSunpath': GenerateSunpath(_input_params=self._input_params), 'CreateTotalSky': CreateTotalSky(_input_params=self._input_params), 'CreateDirectSky': CreateDirectSky(_input_params=self._input_params), 'CreateRadFolder': CreateRadFolder(_input_params=self._input_params), 'SplitGridFolder': SplitGridFolder(_input_params=self._input_params)} + return {'CreateRadFolder': CreateRadFolder(_input_params=self._input_params), 'PrepareMultiphase': PrepareMultiphase(_input_params=self._input_params), 'CreateTotalSky': CreateTotalSky(_input_params=self._input_params), 'CreateDirectSky': CreateDirectSky(_input_params=self._input_params), 'CreateSkyDome': CreateSkyDome(_input_params=self._input_params), 'GenerateSunpath': GenerateSunpath(_input_params=self._input_params)} def output(self): return { - 'is_done': luigi.LocalTarget(pathlib.Path(self.execution_folder, 'annual_daylight_raytracing.done').resolve().as_posix()) + 'is_done': luigi.LocalTarget(pathlib.Path(self.execution_folder, 'calculate_two_phase_matrix.done').resolve().as_posix()) } -class AnnualDaylightRaytracing(luigi.Task): +class CalculateTwoPhaseMatrix(luigi.Task): """No description is provided.""" # global parameters _input_params = luigi.DictParameter() @property - def sensor_grids(self): - value = pathlib.Path(self.input()['SplitGridFolder']['sensor_grids'].path) + def two_phase_info(self): + value = pathlib.Path(self.input()['PrepareMultiphase']['two_phase_info'].path) return value.as_posix() if value.is_absolute() \ else pathlib.Path(self.initiation_folder, value).resolve().as_posix() @@ -184,14 +264,14 @@ def sensor_grids(self): def items(self): try: # assume the input is a file - return qb_load_input_param(self.sensor_grids) + return qb_load_input_param(self.two_phase_info) except: # it is a parameter - return self.input()['SplitGridFolder']['sensor_grids'].path + return self.input()['PrepareMultiphase']['two_phase_info'].path def run(self): - yield [AnnualDaylightRaytracingLoop(item=item, _input_params=self._input_params) for item in self.items] - done_file = pathlib.Path(self.execution_folder, 'annual_daylight_raytracing.done') + yield [CalculateTwoPhaseMatrixLoop(item=item, _input_params=self._input_params) for item in self.items] + done_file = pathlib.Path(self.execution_folder, 'calculate_two_phase_matrix.done') done_file.parent.mkdir(parents=True, exist_ok=True) done_file.write_text('done!') @@ -208,84 +288,14 @@ def params_folder(self): return pathlib.Path(self.execution_folder, self._input_params['params_folder']).resolve().as_posix() def requires(self): - return {'CreateSkyDome': CreateSkyDome(_input_params=self._input_params), 'CreateOctreeWithSuns': CreateOctreeWithSuns(_input_params=self._input_params), 'CreateOctree': CreateOctree(_input_params=self._input_params), 'GenerateSunpath': GenerateSunpath(_input_params=self._input_params), 'CreateTotalSky': CreateTotalSky(_input_params=self._input_params), 'CreateDirectSky': CreateDirectSky(_input_params=self._input_params), 'CreateRadFolder': CreateRadFolder(_input_params=self._input_params), 'SplitGridFolder': SplitGridFolder(_input_params=self._input_params)} + return {'CreateRadFolder': CreateRadFolder(_input_params=self._input_params), 'PrepareMultiphase': PrepareMultiphase(_input_params=self._input_params), 'CreateTotalSky': CreateTotalSky(_input_params=self._input_params), 'CreateDirectSky': CreateDirectSky(_input_params=self._input_params), 'CreateSkyDome': CreateSkyDome(_input_params=self._input_params), 'GenerateSunpath': GenerateSunpath(_input_params=self._input_params)} def output(self): return { - 'is_done': luigi.LocalTarget(pathlib.Path(self.execution_folder, 'annual_daylight_raytracing.done').resolve().as_posix()) + 'is_done': luigi.LocalTarget(pathlib.Path(self.execution_folder, 'calculate_two_phase_matrix.done').resolve().as_posix()) } -class CalculateAnnualMetrics(QueenbeeTask): - """Calculate annual daylight metrics for annual daylight simulation.""" - - # DAG Input parameters - _input_params = luigi.DictParameter() - - # Task inputs - @property - def thresholds(self): - return self._input_params['thresholds'] - - @property - def folder(self): - value = pathlib.Path('results') - return value.as_posix() if value.is_absolute() \ - else pathlib.Path(self.initiation_folder, value).resolve().as_posix() - - @property - def schedule(self): - try: - pathlib.Path(self._input_params['schedule']) - except TypeError: - # optional artifact - return None - value = pathlib.Path(self._input_params['schedule']) - return value.as_posix() if value.is_absolute() \ - else pathlib.Path(self.initiation_folder, value).resolve().as_posix() - - @property - def execution_folder(self): - return pathlib.Path(self._input_params['simulation_folder']).as_posix() - - @property - def initiation_folder(self): - return pathlib.Path(self._input_params['simulation_folder']).as_posix() - - @property - def params_folder(self): - return pathlib.Path(self.execution_folder, self._input_params['params_folder']).resolve().as_posix() - - def command(self): - return 'honeybee-radiance post-process annual-daylight raw_results --schedule schedule.txt {thresholds} --sub_folder ../metrics'.format(thresholds=self.thresholds) - - def requires(self): - return {'ParseSunUpHours': ParseSunUpHours(_input_params=self._input_params), 'AnnualDaylightRaytracing': AnnualDaylightRaytracing(_input_params=self._input_params), 'RestructureResults': RestructureResults(_input_params=self._input_params)} - - def output(self): - return { - 'annual_metrics': luigi.LocalTarget( - pathlib.Path(self.execution_folder, 'metrics').resolve().as_posix() - ) - } - - @property - def input_artifacts(self): - return [ - {'name': 'folder', 'to': 'raw_results', 'from': self.folder, 'optional': False}, - {'name': 'schedule', 'to': 'schedule.txt', 'from': self.schedule, 'optional': True}] - - @property - def output_artifacts(self): - return [ - { - 'name': 'annual-metrics', 'from': 'metrics', - 'to': pathlib.Path(self.execution_folder, 'metrics').resolve().as_posix(), - 'optional': False, - 'type': 'folder' - }] - - class CreateDirectSky(QueenbeeTask): """Generate a sun-up sky matrix.""" @@ -357,129 +367,6 @@ def output_artifacts(self): }] -class CreateOctree(QueenbeeTask): - """Generate an octree from a Radiance folder.""" - - # DAG Input parameters - _input_params = luigi.DictParameter() - - # Task inputs - black_out = luigi.Parameter(default='default') - - include_aperture = luigi.Parameter(default='include') - - @property - def model(self): - value = pathlib.Path(self.input()['CreateRadFolder']['model_folder'].path) - return value.as_posix() if value.is_absolute() \ - else pathlib.Path(self.initiation_folder, value).resolve().as_posix() - - @property - def execution_folder(self): - return pathlib.Path(self._input_params['simulation_folder']).as_posix() - - @property - def initiation_folder(self): - return pathlib.Path(self._input_params['simulation_folder']).as_posix() - - @property - def params_folder(self): - return pathlib.Path(self.execution_folder, self._input_params['params_folder']).resolve().as_posix() - - def command(self): - return 'honeybee-radiance octree from-folder model --output scene.oct --{include_aperture}-aperture --{black_out}'.format(include_aperture=self.include_aperture, black_out=self.black_out) - - def requires(self): - return {'CreateRadFolder': CreateRadFolder(_input_params=self._input_params)} - - def output(self): - return { - 'scene_file': luigi.LocalTarget( - pathlib.Path(self.execution_folder, 'resources/scene.oct').resolve().as_posix() - ) - } - - @property - def input_artifacts(self): - return [ - {'name': 'model', 'to': 'model', 'from': self.model, 'optional': False}] - - @property - def output_artifacts(self): - return [ - { - 'name': 'scene-file', 'from': 'scene.oct', - 'to': pathlib.Path(self.execution_folder, 'resources/scene.oct').resolve().as_posix(), - 'optional': False, - 'type': 'file' - }] - - -class CreateOctreeWithSuns(QueenbeeTask): - """Generate an octree from a Radiance folder and a sky!""" - - # DAG Input parameters - _input_params = luigi.DictParameter() - - # Task inputs - black_out = luigi.Parameter(default='default') - - include_aperture = luigi.Parameter(default='include') - - @property - def model(self): - value = pathlib.Path(self.input()['CreateRadFolder']['model_folder'].path) - return value.as_posix() if value.is_absolute() \ - else pathlib.Path(self.initiation_folder, value).resolve().as_posix() - - @property - def sky(self): - value = pathlib.Path(self.input()['GenerateSunpath']['sunpath'].path) - return value.as_posix() if value.is_absolute() \ - else pathlib.Path(self.initiation_folder, value).resolve().as_posix() - - @property - def execution_folder(self): - return pathlib.Path(self._input_params['simulation_folder']).as_posix() - - @property - def initiation_folder(self): - return pathlib.Path(self._input_params['simulation_folder']).as_posix() - - @property - def params_folder(self): - return pathlib.Path(self.execution_folder, self._input_params['params_folder']).resolve().as_posix() - - def command(self): - return 'honeybee-radiance octree from-folder model --output scene.oct --{include_aperture}-aperture --{black_out} --add-before sky.sky'.format(include_aperture=self.include_aperture, black_out=self.black_out) - - def requires(self): - return {'GenerateSunpath': GenerateSunpath(_input_params=self._input_params), 'CreateRadFolder': CreateRadFolder(_input_params=self._input_params)} - - def output(self): - return { - 'scene_file': luigi.LocalTarget( - pathlib.Path(self.execution_folder, 'resources/scene_with_suns.oct').resolve().as_posix() - ) - } - - @property - def input_artifacts(self): - return [ - {'name': 'model', 'to': 'model', 'from': self.model, 'optional': False}, - {'name': 'sky', 'to': 'sky.sky', 'from': self.sky, 'optional': False}] - - @property - def output_artifacts(self): - return [ - { - 'name': 'scene-file', 'from': 'scene.oct', - 'to': pathlib.Path(self.execution_folder, 'resources/scene_with_suns.oct').resolve().as_posix(), - 'optional': False, - 'type': 'file' - }] - - class CreateRadFolder(QueenbeeTask): """Create a Radiance folder from a HBJSON input file.""" @@ -800,76 +687,20 @@ def output_artifacts(self): }] -class RestructureResults(QueenbeeTask): - """Restructure files in a distributed folder.""" +class PrepareMultiphase(QueenbeeTask): + """Generate several octree from a Radiance folder as well as evenly distributed + grids. + + Use this function to create octrees and grids for multi-phase simulations.""" # DAG Input parameters _input_params = luigi.DictParameter() # Task inputs @property - def extension(self): - return 'ill' + def phase(self): + return '2' - @property - def input_folder(self): - value = pathlib.Path('initial_results/final') - return value.as_posix() if value.is_absolute() \ - else pathlib.Path(self.initiation_folder, value).resolve().as_posix() - - @property - def execution_folder(self): - return pathlib.Path(self._input_params['simulation_folder']).as_posix() - - @property - def initiation_folder(self): - return pathlib.Path(self._input_params['simulation_folder']).as_posix() - - @property - def params_folder(self): - return pathlib.Path(self.execution_folder, self._input_params['params_folder']).resolve().as_posix() - - def command(self): - return 'honeybee-radiance grid merge-folder ./input_folder ./output_folder {extension} --dist-info dist_info.json'.format(extension=self.extension) - - def requires(self): - return {'AnnualDaylightRaytracing': AnnualDaylightRaytracing(_input_params=self._input_params)} - - def output(self): - return { - 'output_folder': luigi.LocalTarget( - pathlib.Path(self.execution_folder, 'results').resolve().as_posix() - ) - } - - @property - def input_artifacts(self): - return [ - {'name': 'input_folder', 'to': 'input_folder', 'from': self.input_folder, 'optional': False}] - - @property - def output_artifacts(self): - return [ - { - 'name': 'output-folder', 'from': 'output_folder', - 'to': pathlib.Path(self.execution_folder, 'results').resolve().as_posix(), - 'optional': False, - 'type': 'folder' - }] - - -class SplitGridFolder(QueenbeeTask): - """Create new sensor grids folder with evenly distributed sensors. - - This function creates a new folder with evenly distributed sensor grids. The folder - will include a ``_redist_info.json`` file which has the information to recreate the - original input files from this folder and the results generated based on the grids - in this folder.""" - - # DAG Input parameters - _input_params = luigi.DictParameter() - - # Task inputs @property def cpu_count(self): return self._input_params['cpu_count'] @@ -883,8 +714,23 @@ def min_sensor_count(self): return self._input_params['min_sensor_count'] @property - def input_folder(self): - value = pathlib.Path(self.input()['CreateRadFolder']['model_folder'].path, 'grid') + def static(self): + return 'include' + + @property + def model(self): + value = pathlib.Path(self.input()['CreateRadFolder']['model_folder'].path) + return value.as_posix() if value.is_absolute() \ + else pathlib.Path(self.initiation_folder, value).resolve().as_posix() + + @property + def sunpath(self): + try: + pathlib.Path(self.input()['GenerateSunpath']['sunpath'].path) + except TypeError: + # optional artifact + return None + value = pathlib.Path(self.input()['GenerateSunpath']['sunpath'].path) return value.as_posix() if value.is_absolute() \ else pathlib.Path(self.initiation_folder, value).resolve().as_posix() @@ -901,56 +747,74 @@ def params_folder(self): return pathlib.Path(self.execution_folder, self._input_params['params_folder']).resolve().as_posix() def command(self): - return 'honeybee-radiance grid split-folder ./input_folder ./output_folder {cpu_count} --grid-divisor {cpus_per_grid} --min-sensor-count {min_sensor_count}'.format(cpu_count=self.cpu_count, cpus_per_grid=self.cpus_per_grid, min_sensor_count=self.min_sensor_count) + return 'honeybee-radiance multi-phase prepare-multiphase model {cpu_count} --grid-divisor {cpus_per_grid} --min-sensor-count {min_sensor_count} --sun-path sun.path --phase {phase} --octree-folder octree --grid-folder grid --{static}-static'.format(cpu_count=self.cpu_count, cpus_per_grid=self.cpus_per_grid, min_sensor_count=self.min_sensor_count, phase=self.phase, static=self.static) def requires(self): - return {'CreateRadFolder': CreateRadFolder(_input_params=self._input_params)} + return {'CreateRadFolder': CreateRadFolder(_input_params=self._input_params), 'GenerateSunpath': GenerateSunpath(_input_params=self._input_params)} def output(self): return { - 'output_folder': luigi.LocalTarget( - pathlib.Path(self.execution_folder, 'resources/grid').resolve().as_posix() + 'scene_folder': luigi.LocalTarget( + pathlib.Path(self.execution_folder, 'resources/dynamic/octree').resolve().as_posix() ), - 'dist_info': luigi.LocalTarget( - pathlib.Path(self.execution_folder, 'initial_results/final/_redist_info.json').resolve().as_posix() + 'grid_folder': luigi.LocalTarget( + pathlib.Path(self.execution_folder, 'resources/dynamic/grid').resolve().as_posix() ), - 'sensor_grids': luigi.LocalTarget( + + 'grid_states_file': luigi.LocalTarget( + pathlib.Path(self.execution_folder, 'results/grid_states.json').resolve().as_posix() + ), + 'scene_info': luigi.LocalTarget( + pathlib.Path( + self.params_folder, + 'multi_phase.json').resolve().as_posix() + ), + 'two_phase_info': luigi.LocalTarget( pathlib.Path( self.params_folder, - 'output_folder/_info.json').resolve().as_posix() + 'two_phase.json').resolve().as_posix() ) } @property def input_artifacts(self): return [ - {'name': 'input_folder', 'to': 'input_folder', 'from': self.input_folder, 'optional': False}] + {'name': 'model', 'to': 'model', 'from': self.model, 'optional': False}, + {'name': 'sunpath', 'to': 'sun.path', 'from': self.sunpath, 'optional': True}] @property def output_artifacts(self): return [ { - 'name': 'output-folder', 'from': 'output_folder', - 'to': pathlib.Path(self.execution_folder, 'resources/grid').resolve().as_posix(), - 'optional': False, + 'name': 'scene-folder', 'from': 'octree', + 'to': pathlib.Path(self.execution_folder, 'resources/dynamic/octree').resolve().as_posix(), + 'optional': True, 'type': 'folder' }, { - 'name': 'dist-info', 'from': 'output_folder/_redist_info.json', - 'to': pathlib.Path(self.execution_folder, 'initial_results/final/_redist_info.json').resolve().as_posix(), - 'optional': False, + 'name': 'grid-folder', 'from': 'grid', + 'to': pathlib.Path(self.execution_folder, 'resources/dynamic/grid').resolve().as_posix(), + 'optional': True, + 'type': 'folder' + }, + + { + 'name': 'grid-states-file', 'from': 'grid_states.json', + 'to': pathlib.Path(self.execution_folder, 'results/grid_states.json').resolve().as_posix(), + 'optional': True, 'type': 'file' }] @property def output_parameters(self): - return [{'name': 'sensor-grids', 'from': 'output_folder/_info.json', 'to': pathlib.Path(self.params_folder, 'output_folder/_info.json').resolve().as_posix()}] + return [{'name': 'scene-info', 'from': 'multi_phase.json', 'to': pathlib.Path(self.params_folder, 'multi_phase.json').resolve().as_posix()}, + {'name': 'two-phase-info', 'from': 'two_phase.json', 'to': pathlib.Path(self.params_folder, 'two_phase.json').resolve().as_posix()}] -class _Main_ae044755Orchestrator(luigi.WrapperTask): +class _Main_8529a1a1Orchestrator(luigi.WrapperTask): """Runs all the tasks in this module.""" # user input for this module _input_params = luigi.DictParameter() @@ -962,4 +826,4 @@ def input_values(self): return params def requires(self): - yield [CalculateAnnualMetrics(_input_params=self.input_values)] + yield [CalculateAnnualMetrics(_input_params=self.input_values), ParseSunUpHours(_input_params=self.input_values)] diff --git a/lbt_recipes/annual_daylight/package.json b/lbt_recipes/annual_daylight/package.json index 71be3bf0..fae775a7 100644 --- a/lbt_recipes/annual_daylight/package.json +++ b/lbt_recipes/annual_daylight/package.json @@ -6,7 +6,7 @@ "type": "MetaData", "annotations": {}, "name": "annual-daylight", - "tag": "0.8.21", + "tag": "0.8.22", "app_version": null, "keywords": [ "honeybee", @@ -496,27 +496,7 @@ "annotations": {}, "path": "results" }, - "alias": [ - { - "type": "DAGGenericOutputAlias", - "annotations": {}, - "name": "results", - "description": "Raw result files (.ill) that contain illuminance matrices.", - "platform": [ - "grasshopper" - ], - "handler": [ - { - "type": "IOAliasHandler", - "annotations": {}, - "language": "python", - "module": "pollination_handlers.outputs.daylight", - "function": "sort_ill_from_folder", - "index": 0 - } - ] - } - ], + "alias": [], "required": true }, { diff --git a/lbt_recipes/annual_daylight/run.py b/lbt_recipes/annual_daylight/run.py index 8d171826..78196e9d 100644 --- a/lbt_recipes/annual_daylight/run.py +++ b/lbt_recipes/annual_daylight/run.py @@ -39,7 +39,7 @@ class LetAnnualDaylightFly(luigi.WrapperTask): _input_params = luigi.DictParameter() def requires(self): - yield [annual_daylight_workerbee._Main_ae044755Orchestrator(_input_params=self._input_params)] + yield [annual_daylight_workerbee._Main_8529a1a1Orchestrator(_input_params=self._input_params)] def start(project_folder, user_values, workers): diff --git a/tests/recipes_test.py b/tests/recipes_test.py index 94197f06..33b0c793 100644 --- a/tests/recipes_test.py +++ b/tests/recipes_test.py @@ -16,7 +16,7 @@ env_args.append('{}="{}"'.format(k, v)) -def run_daylight_recipe(recipe_name, extension): +def run_daylight_recipe(recipe_name, extension, is_dynamic=False): project_folder = './tests/assets/project folder' recipe_folder = './lbt_recipes/{}'.format(recipe_name.replace('-', '_')) inputs = './tests/assets/radiance_grid_inputs.json' @@ -32,8 +32,14 @@ def run_daylight_recipe(recipe_name, extension): ) assert result.exit_code == 0 results_folder = os.path.join(sim_folder, 'results') - assert os.path.isfile(os.path.join(results_folder, f'TestRoom_1.{extension}')) - assert os.path.isfile(os.path.join(results_folder, f'TestRoom_2.{extension}')) + if is_dynamic: + results_folder = os.path.join( + results_folder, '__static_apertures__', 'default', 'total') + assert os.path.isfile(os.path.join(results_folder, f'TestRoom_1.{extension}')) + assert os.path.isfile(os.path.join(results_folder, f'TestRoom_2.{extension}')) + else: + assert os.path.isfile(os.path.join(results_folder, f'TestRoom_1.{extension}')) + assert os.path.isfile(os.path.join(results_folder, f'TestRoom_2.{extension}')) nukedir(sim_folder, True) @@ -42,7 +48,7 @@ def test_daylight_factor(): def test_annual_daylight(): - run_daylight_recipe('annual-daylight', 'ill') + run_daylight_recipe('annual-daylight', 'npy', True) def run_comfort_map_recipe(recipe_name):