diff --git a/lbt_recipes/annual_radiation/flow/__init__.py b/lbt_recipes/annual_radiation/flow/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/lbt_recipes/annual_radiation/flow/__init__.py @@ -0,0 +1 @@ + diff --git a/lbt_recipes/annual_radiation/flow/dependencies/__init__.py b/lbt_recipes/annual_radiation/flow/dependencies/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/lbt_recipes/annual_radiation/flow/dependencies/__init__.py @@ -0,0 +1 @@ + diff --git a/lbt_recipes/annual_radiation/flow/dependencies/annual_radiation_ray_tracing.py b/lbt_recipes/annual_radiation/flow/dependencies/annual_radiation_ray_tracing.py new file mode 100644 index 00000000..13d1aa89 --- /dev/null +++ b/lbt_recipes/annual_radiation/flow/dependencies/annual_radiation_ray_tracing.py @@ -0,0 +1,613 @@ +import luigi +import os +from queenbee_local import QueenbeeTask + + +_default_inputs = { 'grid_name': None, + 'octree_file': None, + 'octree_file_with_suns': None, + 'params_folder': '__params', + 'radiance_parameters': '-ab 2', + 'sensor_count': 200, + 'sensor_grid': None, + 'simulation_folder': '.', + 'sky_dome': None, + 'sky_matrix_indirect': None, + 'sun_modifiers': None} + + +class DirectSunlightLoop(QueenbeeTask): + """Calculate daylight contribution for a grid of sensors from a series of modifiers + using rcontrib command.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def radiance_parameters(self): + return self._input_params['radiance_parameters'] + + @property + def fixed_radiance_parameters(self): + return '-aa 0.0 -I -ab 0 -dc 1.0 -dt 0.0 -dj 0.0 -dr 0' + + @property + def sensor_count(self): + return self.item['count'] + + @property + def conversion(self): + return '0.265 0.670 0.065' + + @property + def output_format(self): + return 'a' + + calculate_values = luigi.Parameter(default='value') + + @property + def modifiers(self): + value = self._input_params['sun_modifiers'].replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def sensor_grid(self): + value = os.path.join(self.input()['SplitGrid']['output_folder'].path, self.item['path']).replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def scene_file(self): + value = self._input_params['octree_file_with_suns'].replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + # get item for loop + try: + item = luigi.DictParameter() + except Exception: + item = luigi.Parameter() + + @property + def execution_folder(self): + return os.path.join(self._input_params['simulation_folder'], '01_direct').replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def command(self): + return 'honeybee-radiance dc scontrib scene.oct grid.pts suns.mod --{calculate_values} --sensor-count {sensor_count} --rad-params "{radiance_parameters}" --rad-params-locked "{fixed_radiance_parameters}" --conversion "{conversion}" --output-format {output_format} --output results.ill'.format(calculate_values=self.calculate_values, sensor_count=self.sensor_count, radiance_parameters=self.radiance_parameters, fixed_radiance_parameters=self.fixed_radiance_parameters, conversion=self.conversion, output_format=self.output_format) + + def requires(self): + return {'SplitGrid': SplitGrid(_input_params=self._input_params)} + + def output(self): + return { + 'result_file': luigi.LocalTarget( + os.path.join(self.execution_folder, '{item_name}.ill'.format(item_name=self.item['name'])) + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'modifiers', 'to': 'suns.mod', 'from': self.modifiers, 'optional': False}, + {'name': 'sensor_grid', 'to': 'grid.pts', 'from': self.sensor_grid, 'optional': False}, + {'name': 'scene_file', 'to': 'scene.oct', 'from': self.scene_file, 'optional': False}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'result-file', 'from': 'results.ill', + 'to': os.path.join(self.execution_folder, '{item_name}.ill'.format(item_name=self.item['name'])) + }] + + +class DirectSunlight(luigi.Task): + """Calculate daylight contribution for a grid of sensors from a series of modifiers + using rcontrib command.""" + # global parameters + _input_params = luigi.DictParameter() + @property + def grids_list(self): + value = self.input()['SplitGrid']['grids_list'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def items(self): + try: + # assume the input is a file + return QueenbeeTask.load_input_param(self.grids_list) + except: + # it is a parameter + return self.input()['SplitGrid']['grids_list'].path + + def run(self): + yield [DirectSunlightLoop(item=item, _input_params=self._input_params) for item in self.items] + with open(os.path.join(self.execution_folder, 'direct_sunlight.done'), 'w') as out_file: + out_file.write('done!\n') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def requires(self): + return {'SplitGrid': SplitGrid(_input_params=self._input_params)} + + def output(self): + return { + 'is_done': luigi.LocalTarget(os.path.join(self.execution_folder, 'direct_sunlight.done')) + } + + +class IndirectSkyLoop(QueenbeeTask): + """Calculate daylight coefficient for a grid of sensors from a sky matrix.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def radiance_parameters(self): + return self._input_params['radiance_parameters'] + + @property + def fixed_radiance_parameters(self): + return '-aa 0.0 -I -c 1' + + @property + def sensor_count(self): + return self.item['count'] + + @property + def conversion(self): + return '0.265 0.670 0.065' + + output_format = luigi.Parameter(default='f') + + @property + def sky_matrix(self): + value = self._input_params['sky_matrix_indirect'].replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def sky_dome(self): + value = self._input_params['sky_dome'].replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def sensor_grid(self): + value = os.path.join(self.input()['SplitGrid']['output_folder'].path, self.item['path']).replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def scene_file(self): + value = self._input_params['octree_file'].replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + # get item for loop + try: + item = luigi.DictParameter() + except Exception: + item = luigi.Parameter() + + @property + def execution_folder(self): + return os.path.join(self._input_params['simulation_folder'], '02_indirect').replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def command(self): + return 'honeybee-radiance dc scoeff scene.oct grid.pts sky.dome sky.mtx --sensor-count {sensor_count} --output results.ill --rad-params "{radiance_parameters}" --rad-params-locked "{fixed_radiance_parameters}" --conversion "{conversion}" --output-format {output_format}'.format(sensor_count=self.sensor_count, radiance_parameters=self.radiance_parameters, fixed_radiance_parameters=self.fixed_radiance_parameters, conversion=self.conversion, output_format=self.output_format) + + def requires(self): + return {'SplitGrid': SplitGrid(_input_params=self._input_params)} + + def output(self): + return { + 'result_file': luigi.LocalTarget( + os.path.join(self.execution_folder, '{item_name}.ill'.format(item_name=self.item['name'])) + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'sky_matrix', 'to': 'sky.mtx', 'from': self.sky_matrix, 'optional': False}, + {'name': 'sky_dome', 'to': 'sky.dome', 'from': self.sky_dome, 'optional': False}, + {'name': 'sensor_grid', 'to': 'grid.pts', 'from': self.sensor_grid, 'optional': False}, + {'name': 'scene_file', 'to': 'scene.oct', 'from': self.scene_file, 'optional': False}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'result-file', 'from': 'results.ill', + 'to': os.path.join(self.execution_folder, '{item_name}.ill'.format(item_name=self.item['name'])) + }] + + +class IndirectSky(luigi.Task): + """Calculate daylight coefficient for a grid of sensors from a sky matrix.""" + # global parameters + _input_params = luigi.DictParameter() + @property + def grids_list(self): + value = self.input()['SplitGrid']['grids_list'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def items(self): + try: + # assume the input is a file + return QueenbeeTask.load_input_param(self.grids_list) + except: + # it is a parameter + return self.input()['SplitGrid']['grids_list'].path + + def run(self): + yield [IndirectSkyLoop(item=item, _input_params=self._input_params) for item in self.items] + with open(os.path.join(self.execution_folder, 'indirect_sky.done'), 'w') as out_file: + out_file.write('done!\n') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def requires(self): + return {'SplitGrid': SplitGrid(_input_params=self._input_params)} + + def output(self): + return { + 'is_done': luigi.LocalTarget(os.path.join(self.execution_folder, 'indirect_sky.done')) + } + + +class MergeDirectResults(QueenbeeTask): + """Merge several files with similar starting name into one.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def name(self): + return self._input_params['grid_name'] + + @property + def extension(self): + return '.ill' + + @property + def folder(self): + value = '01_direct'.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def command(self): + return 'honeybee-radiance grid merge input_folder grid {extension} --name {name}'.format(extension=self.extension, name=self.name) + + def requires(self): + return {'OutputMatrixMath': OutputMatrixMath(_input_params=self._input_params)} + + def output(self): + return { + 'result_file': luigi.LocalTarget( + os.path.join(self.execution_folder, '../../results/direct/{name}.ill'.format(name=self.name)) + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'folder', 'to': 'input_folder', 'from': self.folder, 'optional': False}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'result-file', 'from': '{name}{extension}'.format(name=self.name, extension=self.extension), + 'to': os.path.join(self.execution_folder, '../../results/direct/{name}.ill'.format(name=self.name)) + }] + + +class MergeTotalResults(QueenbeeTask): + """Merge several files with similar starting name into one.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def name(self): + return self._input_params['grid_name'] + + @property + def extension(self): + return '.ill' + + @property + def folder(self): + value = '03_total'.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def command(self): + return 'honeybee-radiance grid merge input_folder grid {extension} --name {name}'.format(extension=self.extension, name=self.name) + + def requires(self): + return {'OutputMatrixMath': OutputMatrixMath(_input_params=self._input_params)} + + def output(self): + return { + 'result_file': luigi.LocalTarget( + os.path.join(self.execution_folder, '../../results/total/{name}.ill'.format(name=self.name)) + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'folder', 'to': 'input_folder', 'from': self.folder, 'optional': False}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'result-file', 'from': '{name}{extension}'.format(name=self.name, extension=self.extension), + 'to': os.path.join(self.execution_folder, '../../results/total/{name}.ill'.format(name=self.name)) + }] + + +class OutputMatrixMathLoop(QueenbeeTask): + """Add indirect sky to direct sunlight.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + conversion = luigi.Parameter(default=' ') + + header = luigi.Parameter(default='remove') + + output_format = luigi.Parameter(default='a') + + @property + def indirect_sky_matrix(self): + value = '02_indirect/{item_name}.ill'.format(item_name=self.item['name']).replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def sunlight_matrix(self): + value = '01_direct/{item_name}.ill'.format(item_name=self.item['name']).replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + # get item for loop + try: + item = luigi.DictParameter() + except Exception: + item = luigi.Parameter() + + @property + def execution_folder(self): + return os.path.join(self._input_params['simulation_folder'], '03_total').replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def command(self): + return 'honeybee-radiance mtxop operate-two sky.ill sun.ill --operator + --{header}-header --conversion "{conversion}" --output-mtx final.ill --output-format {output_format}'.format(header=self.header, conversion=self.conversion, output_format=self.output_format) + + def requires(self): + return {'SplitGrid': SplitGrid(_input_params=self._input_params), 'DirectSunlight': DirectSunlight(_input_params=self._input_params), 'IndirectSky': IndirectSky(_input_params=self._input_params)} + + def output(self): + return { + 'results_file': luigi.LocalTarget( + os.path.join(self.execution_folder, '{item_name}.ill'.format(item_name=self.item['name'])) + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'indirect_sky_matrix', 'to': 'sky.ill', 'from': self.indirect_sky_matrix, 'optional': False}, + {'name': 'sunlight_matrix', 'to': 'sun.ill', 'from': self.sunlight_matrix, 'optional': False}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'results-file', 'from': 'final.ill', + 'to': os.path.join(self.execution_folder, '{item_name}.ill'.format(item_name=self.item['name'])) + }] + + +class OutputMatrixMath(luigi.Task): + """Add indirect sky to direct sunlight.""" + # global parameters + _input_params = luigi.DictParameter() + @property + def grids_list(self): + value = self.input()['SplitGrid']['grids_list'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def items(self): + try: + # assume the input is a file + return QueenbeeTask.load_input_param(self.grids_list) + except: + # it is a parameter + return self.input()['SplitGrid']['grids_list'].path + + def run(self): + yield [OutputMatrixMathLoop(item=item, _input_params=self._input_params) for item in self.items] + with open(os.path.join(self.execution_folder, 'output_matrix_math.done'), 'w') as out_file: + out_file.write('done!\n') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def requires(self): + return {'SplitGrid': SplitGrid(_input_params=self._input_params), 'DirectSunlight': DirectSunlight(_input_params=self._input_params), 'IndirectSky': IndirectSky(_input_params=self._input_params)} + + def output(self): + return { + 'is_done': luigi.LocalTarget(os.path.join(self.execution_folder, 'output_matrix_math.done')) + } + + +class SplitGrid(QueenbeeTask): + """Split a single sensor grid file into multiple smaller grids.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def sensor_count(self): + return self._input_params['sensor_count'] + + @property + def input_grid(self): + value = self._input_params['sensor_grid'].replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def command(self): + return 'honeybee-radiance grid split grid.pts {sensor_count} --folder output --log-file output/grids_info.json'.format(sensor_count=self.sensor_count) + + def output(self): + return { + + 'output_folder': luigi.LocalTarget( + os.path.join(self.execution_folder, '00_sub_grids') + ), + 'grids_list': luigi.LocalTarget( + os.path.join( + self.params_folder, + 'output/grids_info.json') + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'input_grid', 'to': 'grid.pts', 'from': self.input_grid, 'optional': False}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'output-folder', 'from': 'output', + 'to': os.path.join(self.execution_folder, '00_sub_grids') + }] + + @property + def output_parameters(self): + return [{'name': 'grids-list', 'from': 'output/grids_info.json', 'to': os.path.join(self.params_folder, 'output/grids_info.json')}] + + +class _AnnualRadiationRayTracing_9da8e0c2Orchestrator(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): + return [MergeDirectResults(_input_params=self.input_values), MergeTotalResults(_input_params=self.input_values)] diff --git a/lbt_recipes/annual_radiation/flow/main.py b/lbt_recipes/annual_radiation/flow/main.py new file mode 100644 index 00000000..1d8241a7 --- /dev/null +++ b/lbt_recipes/annual_radiation/flow/main.py @@ -0,0 +1,717 @@ +import luigi +import os +from queenbee_local import QueenbeeTask +from .dependencies.annual_radiation_ray_tracing import _AnnualRadiationRayTracing_9da8e0c2Orchestrator as AnnualRadiationRayTracing_9da8e0c2Workerbee + + +_default_inputs = { 'model': None, + 'north': 0.0, + 'params_folder': '__params', + 'radiance_parameters': '-ab 2 -ad 5000 -lw 2e-05', + 'sensor_count': 200, + 'sensor_grid': '*', + 'simulation_folder': '.', + 'wea': None} + + +class AnnualRadiationRaytracingLoop(luigi.Task): + """No description is provided.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def sensor_count(self): + return self._input_params['sensor_count'] + + @property + def radiance_parameters(self): + return self._input_params['radiance_parameters'] + + @property + def grid_name(self): + return self.item['full_id'] + + @property + def octree_file_with_suns(self): + value = self.input()['CreateOctreeWithSuns']['scene_file'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def octree_file(self): + value = self.input()['CreateOctree']['scene_file'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def sensor_grid(self): + value = os.path.join(self.input()['CreateRadFolder']['model_folder'].path, 'grid/{item_full_id}.pts'.format(item_full_id=self.item['full_id'])).replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def sky_dome(self): + value = self.input()['CreateSkyDome']['sky_dome'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def sky_matrix_indirect(self): + value = self.input()['CreateIndirectSky']['sky_matrix'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def sunpath(self): + value = self.input()['GenerateSunpath']['sunpath'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def sun_modifiers(self): + value = self.input()['GenerateSunpath']['sun_modifiers'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + # get item for loop + try: + item = luigi.DictParameter() + except Exception: + item = luigi.Parameter() + + @property + def execution_folder(self): + return os.path.join(self._input_params['simulation_folder'], 'initial_results/{item_name}'.format(item_name=self.item['name'])).replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + @property + def map_dag_inputs(self): + """Map task inputs to DAG inputs.""" + inputs = { + 'simulation_folder': self.execution_folder, + 'sensor_count': self.sensor_count, + 'radiance_parameters': self.radiance_parameters, + 'octree_file_with_suns': self.octree_file_with_suns, + 'octree_file': self.octree_file, + 'grid_name': self.grid_name, + 'sensor_grid': self.sensor_grid, + 'sky_dome': self.sky_dome, + 'sky_matrix_indirect': self.sky_matrix_indirect, + 'sunpath': self.sunpath, + 'sun_modifiers': self.sun_modifiers + } + try: + inputs['__debug__'] = self._input_params['__debug__'] + except KeyError: + # not debug mode + pass + + return inputs + + def run(self): + yield [AnnualRadiationRayTracing_9da8e0c2Workerbee(_input_params=self.map_dag_inputs)] + with open(os.path.join(self.execution_folder, 'annual_radiation_raytracing.done'), 'w') as out_file: + out_file.write('done!\n') + + 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), 'CreateIndirectSky': CreateIndirectSky(_input_params=self._input_params), 'CreateRadFolder': CreateRadFolder(_input_params=self._input_params)} + + def output(self): + return { + 'is_done': luigi.LocalTarget(os.path.join(self.execution_folder, 'annual_radiation_raytracing.done')) + } + + +class AnnualRadiationRaytracing(luigi.Task): + """No description is provided.""" + # global parameters + _input_params = luigi.DictParameter() + @property + def sensor_grids(self): + value = self.input()['CreateRadFolder']['sensor_grids'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def items(self): + try: + # assume the input is a file + return QueenbeeTask.load_input_param(self.sensor_grids) + except: + # it is a parameter + return self.input()['CreateRadFolder']['sensor_grids'].path + + def run(self): + yield [AnnualRadiationRaytracingLoop(item=item, _input_params=self._input_params) for item in self.items] + with open(os.path.join(self.execution_folder, 'annual_radiation_raytracing.done'), 'w') as out_file: + out_file.write('done!\n') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + 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), 'CreateIndirectSky': CreateIndirectSky(_input_params=self._input_params), 'CreateRadFolder': CreateRadFolder(_input_params=self._input_params)} + + def output(self): + return { + 'is_done': luigi.LocalTarget(os.path.join(self.execution_folder, 'annual_radiation_raytracing.done')) + } + + +class CopyGridInfo(QueenbeeTask): + """Copy a file or folder to a destination.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def src(self): + value = self.input()['CreateRadFolder']['sensor_grids_file'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def command(self): + return 'echo copying input path...' + + def requires(self): + return {'CreateRadFolder': CreateRadFolder(_input_params=self._input_params)} + + def output(self): + return { + 'dst': luigi.LocalTarget( + os.path.join(self.execution_folder, 'results/total/grids_info.json') + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'src', 'to': 'input_path', 'from': self.src, 'optional': False}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'dst', 'from': 'input_path', + 'to': os.path.join(self.execution_folder, 'results/total/grids_info.json') + }] + + +class CopySunUpHours(QueenbeeTask): + """Copy a file or folder to a destination.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def src(self): + value = self.input()['ParseSunUpHours']['sun_up_hours'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def command(self): + return 'echo copying input path...' + + def requires(self): + return {'ParseSunUpHours': ParseSunUpHours(_input_params=self._input_params)} + + def output(self): + return { + 'dst': luigi.LocalTarget( + os.path.join(self.execution_folder, 'results/direct/sun-up-hours.txt') + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'src', 'to': 'input_path', 'from': self.src, 'optional': False}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'dst', 'from': 'input_path', + 'to': os.path.join(self.execution_folder, 'results/direct/sun-up-hours.txt') + }] + + +class CreateIndirectSky(QueenbeeTask): + """Generate a sun-up sky matrix.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def north(self): + return self._input_params['north'] + + @property + def sky_type(self): + return 'no-sun' + + @property + def output_type(self): + return 'solar' + + @property + def output_format(self): + return 'ASCII' + + @property + def sun_up_hours(self): + return 'sun-up-hours' + + cumulative = luigi.Parameter(default='hourly') + + sky_density = luigi.Parameter(default='1') + + @property + def wea(self): + value = self._input_params['wea'].replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def command(self): + return 'honeybee-radiance sky mtx sky.wea --name sky --north {north} --sky-type {sky_type} --{cumulative} --{sun_up_hours} --{output_type} --output-format {output_format} --sky-density {sky_density}'.format(north=self.north, sky_type=self.sky_type, cumulative=self.cumulative, sun_up_hours=self.sun_up_hours, output_type=self.output_type, output_format=self.output_format, sky_density=self.sky_density) + + def output(self): + return { + 'sky_matrix': luigi.LocalTarget( + os.path.join(self.execution_folder, 'resources/sky_direct.mtx') + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'wea', 'to': 'sky.wea', 'from': self.wea, 'optional': False}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'sky-matrix', 'from': 'sky.mtx', + 'to': os.path.join(self.execution_folder, 'resources/sky_direct.mtx') + }] + + +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 = self.input()['CreateRadFolder']['model_folder'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + 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( + os.path.join(self.execution_folder, 'resources/scene.oct') + ) + } + + @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': os.path.join(self.execution_folder, 'resources/scene.oct') + }] + + +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 = self.input()['CreateRadFolder']['model_folder'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def sky(self): + value = self.input()['GenerateSunpath']['sunpath'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + 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( + os.path.join(self.execution_folder, 'resources/scene_with_suns.oct') + ) + } + + @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': os.path.join(self.execution_folder, 'resources/scene_with_suns.oct') + }] + + +class CreateRadFolder(QueenbeeTask): + """Create a Radiance folder from a HBJSON input file.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def sensor_grid(self): + return self._input_params['sensor_grid'] + + @property + def input_model(self): + value = self._input_params['model'].replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def command(self): + return 'honeybee-radiance translate model-to-rad-folder model.hbjson --grid "{sensor_grid}"'.format(sensor_grid=self.sensor_grid) + + def output(self): + return { + + 'model_folder': luigi.LocalTarget( + os.path.join(self.execution_folder, 'model') + ), + + 'sensor_grids_file': luigi.LocalTarget( + os.path.join(self.execution_folder, 'results/direct/grids_info.json') + ), + 'sensor_grids': luigi.LocalTarget( + os.path.join( + self.params_folder, + 'model/grid/_info.json') + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'input_model', 'to': 'model.hbjson', 'from': self.input_model, 'optional': False}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'model-folder', 'from': 'model', + 'to': os.path.join(self.execution_folder, 'model') + }, + + { + 'name': 'sensor-grids-file', 'from': 'model/grid/_info.json', + 'to': os.path.join(self.execution_folder, 'results/direct/grids_info.json') + }] + + @property + def output_parameters(self): + return [{'name': 'sensor-grids', 'from': 'model/grid/_info.json', 'to': os.path.join(self.params_folder, 'model/grid/_info.json')}] + + +class CreateSkyDome(QueenbeeTask): + """Create a skydome for daylight coefficient studies.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + sky_density = luigi.Parameter(default='1') + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def command(self): + return 'honeybee-radiance sky skydome --name rflux_sky.sky --sky-density {sky_density}'.format(sky_density=self.sky_density) + + def output(self): + return { + 'sky_dome': luigi.LocalTarget( + os.path.join(self.execution_folder, 'resources/sky.dome') + ) + } + + @property + def output_artifacts(self): + return [ + { + 'name': 'sky-dome', 'from': 'rflux_sky.sky', + 'to': os.path.join(self.execution_folder, 'resources/sky.dome') + }] + + +class GenerateSunpath(QueenbeeTask): + """Generate a Radiance sun matrix (AKA sun-path).""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def north(self): + return self._input_params['north'] + + @property + def output_type(self): + return '1' + + @property + def wea(self): + value = self._input_params['wea'].replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def command(self): + return 'gendaymtx -n -D sunpath.mtx -M suns.mod -O{output_type} -r {north} -v sky.wea'.format(output_type=self.output_type, north=self.north) + + def output(self): + return { + 'sunpath': luigi.LocalTarget( + os.path.join(self.execution_folder, 'resources/sunpath.mtx') + ), + + 'sun_modifiers': luigi.LocalTarget( + os.path.join(self.execution_folder, 'resources/suns.mod') + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'wea', 'to': 'sky.wea', 'from': self.wea, 'optional': False}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'sunpath', 'from': 'sunpath.mtx', + 'to': os.path.join(self.execution_folder, 'resources/sunpath.mtx') + }, + + { + 'name': 'sun-modifiers', 'from': 'suns.mod', + 'to': os.path.join(self.execution_folder, 'resources/suns.mod') + }] + + +class ParseSunUpHours(QueenbeeTask): + """Parse sun up hours from sun modifiers file.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def sun_modifiers(self): + value = self.input()['GenerateSunpath']['sun_modifiers'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def command(self): + return 'honeybee-radiance sunpath parse-hours suns.mod --name sun-up-hours.txt' + + def requires(self): + return {'GenerateSunpath': GenerateSunpath(_input_params=self._input_params)} + + def output(self): + return { + 'sun_up_hours': luigi.LocalTarget( + os.path.join(self.execution_folder, 'results/total/sun-up-hours.txt') + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'sun_modifiers', 'to': 'suns.mod', 'from': self.sun_modifiers, 'optional': False}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'sun-up-hours', 'from': 'sun-up-hours.txt', + 'to': os.path.join(self.execution_folder, 'results/total/sun-up-hours.txt') + }] + + +class _Main_9da8e0c2Orchestrator(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): + return [AnnualRadiationRaytracing(_input_params=self.input_values), CopyGridInfo(_input_params=self.input_values), CopySunUpHours(_input_params=self.input_values)] diff --git a/lbt_recipes/annual_radiation/package.json b/lbt_recipes/annual_radiation/package.json new file mode 100644 index 00000000..45ffd286 --- /dev/null +++ b/lbt_recipes/annual_radiation/package.json @@ -0,0 +1,283 @@ +{ + "type": "RecipeInterface", + "annotations": {}, + "api_version": "v1beta1", + "metadata": { + "type": "MetaData", + "annotations": {}, + "name": "annual-radiation", + "tag": "0.1.5", + "app_version": null, + "keywords": [ + "honeybee", + "radiance", + "ladybug-tools", + "daylight", + "annual-radiation" + ], + "maintainers": [ + { + "type": "Maintainer", + "annotations": {}, + "name": "mostapha", + "email": "mostapha@ladybug.tools" + }, + { + "type": "Maintainer", + "annotations": {}, + "name": "ladybug-tools", + "email": "info@ladybug.tools" + } + ], + "home": "https://github.com/pollination/annual-radiation", + "sources": [ + "https://hub.docker.com/r/ladybugtools/honeybee-radiance" + ], + "icon": "https://raw.githubusercontent.com/ladybug-tools/artwork/master/icons_components/honeybee/png/annualradrecipe.png", + "deprecated": null, + "description": "Annual radiation recipe for Pollination.", + "license": { + "type": "License", + "annotations": {}, + "name": "PolyForm Shield License 1.0.0", + "url": "https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt" + } + }, + "source": null, + "inputs": [ + { + "type": "DAGFileInput", + "annotations": {}, + "name": "model", + "description": "A Honeybee model in HBJSON file format.", + "default": null, + "alias": [ + { + "type": "DAGGenericInputAlias", + "annotations": {}, + "name": "model", + "description": "A path to a HBJSON file or a HB model object built with Python or dotnet libraries.", + "platform": [ + "grasshopper" + ], + "handler": [ + { + "type": "IOAliasHandler", + "annotations": {}, + "language": "python", + "module": "pollination_handlers.inputs.model", + "function": "model_to_json" + }, + { + "type": "IOAliasHandler", + "annotations": {}, + "language": "csharp", + "module": "HoneybeeSchema.Handlers", + "function": "HBModelToJSON" + } + ], + "default": null, + "required": true, + "spec": null + }, + { + "type": "DAGLinkedInputAlias", + "annotations": {}, + "name": "model", + "description": "This input links the model to Rhino model.", + "platform": [ + "rhino" + ], + "handler": [ + { + "type": "IOAliasHandler", + "annotations": {}, + "language": "csharp", + "module": "HoneybeeRhino.Handlers", + "function": "RhinoHBModelToJSON" + } + ], + "default": null, + "required": true, + "spec": null + } + ], + "required": true, + "spec": null, + "extensions": [ + "json", + "hbjson" + ] + }, + { + "type": "DAGNumberInput", + "annotations": {}, + "name": "north", + "description": "A number for rotation from north.", + "default": 0.0, + "alias": [ + { + "type": "DAGGenericInputAlias", + "annotations": {}, + "name": "north", + "description": "Either a Vector2D for the north direction or a number between -360 and 360 for the counterclockwise difference between the North and the positive Y-axis in degrees.", + "platform": [ + "grasshopper" + ], + "handler": [ + { + "type": "IOAliasHandler", + "annotations": {}, + "language": "python", + "module": "pollination_handlers.inputs.north", + "function": "north_vector_to_angle" + } + ], + "default": null, + "required": true, + "spec": null + } + ], + "required": false, + "spec": { + "type": "number", + "minimum": -360, + "maximum": 360 + } + }, + { + "type": "DAGStringInput", + "annotations": {}, + "name": "radiance-parameters", + "description": "Radiance parameters for ray tracing.", + "default": "-ab 2 -ad 5000 -lw 2e-05", + "alias": [], + "required": false, + "spec": null + }, + { + "type": "DAGIntegerInput", + "annotations": {}, + "name": "sensor-count", + "description": "The maximum number of grid points per parallel execution.", + "default": 200, + "alias": [], + "required": false, + "spec": { + "type": "integer", + "minimum": 1 + } + }, + { + "type": "DAGStringInput", + "annotations": {}, + "name": "sensor-grid", + "description": "A grid name or a pattern to filter the sensor grids. By default all the grids in HBJSON model will be exported.", + "default": "*", + "alias": [], + "required": false, + "spec": null + }, + { + "type": "DAGFileInput", + "annotations": {}, + "name": "wea", + "description": "Wea file.", + "default": null, + "alias": [ + { + "type": "DAGGenericInputAlias", + "annotations": {}, + "name": "wea", + "description": "Either a Wea python object or the path to a wea or an epw file.", + "platform": [ + "grasshopper" + ], + "handler": [ + { + "type": "IOAliasHandler", + "annotations": {}, + "language": "python", + "module": "pollination_handlers.inputs.wea", + "function": "wea_handler" + } + ], + "default": null, + "required": true, + "spec": null + } + ], + "required": true, + "spec": null, + "extensions": [ + "wea" + ] + } + ], + "outputs": [ + { + "type": "DAGFolderOutput", + "annotations": {}, + "name": "direct-radiation", + "description": null, + "from": { + "type": "FolderReference", + "annotations": {}, + "path": "results/direct" + }, + "alias": [ + { + "type": "DAGGenericOutputAlias", + "annotations": {}, + "name": "annual_daylight", + "description": "Annual daylight result files.", + "platform": [ + "grasshopper" + ], + "handler": [ + { + "type": "IOAliasHandler", + "annotations": {}, + "language": "python", + "module": "pollination_handlers.outputs.daylight", + "function": "sort_ill_from_folder" + } + ] + } + ], + "required": true + }, + { + "type": "DAGFolderOutput", + "annotations": {}, + "name": "total-radiation", + "description": null, + "from": { + "type": "FolderReference", + "annotations": {}, + "path": "results/total" + }, + "alias": [ + { + "type": "DAGGenericOutputAlias", + "annotations": {}, + "name": "annual_daylight", + "description": "Annual daylight result files.", + "platform": [ + "grasshopper" + ], + "handler": [ + { + "type": "IOAliasHandler", + "annotations": {}, + "language": "python", + "module": "pollination_handlers.outputs.daylight", + "function": "sort_ill_from_folder" + } + ] + } + ], + "required": true + } + ] +} \ No newline at end of file diff --git a/lbt_recipes/annual_radiation/run.py b/lbt_recipes/annual_radiation/run.py new file mode 100644 index 00000000..11767636 --- /dev/null +++ b/lbt_recipes/annual_radiation/run.py @@ -0,0 +1,65 @@ +import sys +import luigi +import os +import time +from multiprocessing import freeze_support +from queenbee_local import local_scheduler, _copy_artifacts, update_params, parse_input_args + +import flow.main as annual_radiation_workerbee + + +_recipe_default_inputs = { 'model': None, + 'north': 0.0, + 'radiance_parameters': '-ab 2 -ad 5000 -lw 2e-05', + 'sensor_count': 200, + 'sensor_grid': '*', + 'wea': None} + + +class LetAnnualRadiationFly(luigi.WrapperTask): + # global parameters + _input_params = luigi.DictParameter() + + def requires(self): + yield [annual_radiation_workerbee._Main_9da8e0c2Orchestrator(_input_params=self._input_params)] + + +def start(project_folder, user_values, workers): + freeze_support() + + input_params = update_params(_recipe_default_inputs, user_values) + + if 'simulation_folder' not in input_params or not input_params['simulation_folder']: + if 'simulation_id' not in input_params or not input_params['simulation_id']: + simulation_id = 'annual_radiation_%d' % int(round(time.time(), 2) * 100) + else: + simulation_id = input_params['simulation_id'] + + simulation_folder = os.path.join(project_folder, simulation_id) + input_params['simulation_folder'] = simulation_folder + else: + simulation_folder = input_params['simulation_folder'] + + # copy project folder content to simulation folder + artifacts = ['model', 'wea'] + optional_artifacts = [] + for artifact in artifacts: + value = input_params[artifact] + if value is None: + if artifact in optional_artifacts: + continue + raise ValueError('None value for required artifact input: %s' % artifact) + from_ = os.path.join(project_folder, input_params[artifact]) + to_ = os.path.join(simulation_folder, input_params[artifact]) + _copy_artifacts(from_, to_) + + luigi.build( + [LetAnnualRadiationFly(_input_params=input_params)], + local_scheduler=local_scheduler(), + workers=workers + ) + + +if __name__ == '__main__': + project_folder, user_values, workers = parse_input_args(sys.argv) + start(project_folder, user_values, workers) diff --git a/lbt_recipes/direct_sun_hours/flow/__init__.py b/lbt_recipes/direct_sun_hours/flow/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/lbt_recipes/direct_sun_hours/flow/__init__.py @@ -0,0 +1 @@ + diff --git a/lbt_recipes/direct_sun_hours/flow/dependencies/__init__.py b/lbt_recipes/direct_sun_hours/flow/dependencies/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/lbt_recipes/direct_sun_hours/flow/dependencies/__init__.py @@ -0,0 +1 @@ + diff --git a/lbt_recipes/direct_sun_hours/flow/dependencies/dependencies/__init__.py b/lbt_recipes/direct_sun_hours/flow/dependencies/dependencies/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/lbt_recipes/direct_sun_hours/flow/dependencies/dependencies/__init__.py @@ -0,0 +1 @@ + diff --git a/lbt_recipes/direct_sun_hours/flow/dependencies/dependencies/direct_sun_hours_calculation.py b/lbt_recipes/direct_sun_hours/flow/dependencies/dependencies/direct_sun_hours_calculation.py new file mode 100644 index 00000000..8b07fbe7 --- /dev/null +++ b/lbt_recipes/direct_sun_hours/flow/dependencies/dependencies/direct_sun_hours_calculation.py @@ -0,0 +1,229 @@ +import luigi +import os +from queenbee_local import QueenbeeTask + + +_default_inputs = { 'grid_name': None, + 'octree_file': None, + 'params_folder': '__params', + 'sensor_count': 200, + 'sensor_grid': None, + 'simulation_folder': '.', + 'sun_modifiers': None} + + +class CalculateCumulativeHours(QueenbeeTask): + """Postprocess a Radiance matrix and add all the numbers in each row. + + This function is useful for translating Radiance results to outputs like radiation + to total radiation. Input matrix must be in ASCII format. The header in the input + file will be ignored.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def grid_name(self): + return self._input_params['grid_name'] + + @property + def input_mtx(self): + value = self.input()['ConvertToSunHours']['output_mtx'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def execution_folder(self): + return os.path.join(self._input_params['simulation_folder'], 'cumulative-sun-hours').replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def command(self): + return 'honeybee-radiance post-process sum-row input.mtx --output sum.mtx' + + def requires(self): + return {'ConvertToSunHours': ConvertToSunHours(_input_params=self._input_params)} + + def output(self): + return { + 'output_mtx': luigi.LocalTarget( + os.path.join(self.execution_folder, '{grid_name}.res'.format(grid_name=self.grid_name)) + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'input_mtx', 'to': 'input.mtx', 'from': self.input_mtx, 'optional': False}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'output-mtx', 'from': 'sum.mtx', + 'to': os.path.join(self.execution_folder, '{grid_name}.res'.format(grid_name=self.grid_name)) + }] + + +class ConvertToSunHours(QueenbeeTask): + """Convert a Radiance matrix to a new matrix with 0-1 values.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def grid_name(self): + return self._input_params['grid_name'] + + @property + def input_mtx(self): + value = self.input()['DirectRadiationCalculation']['result_file'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def execution_folder(self): + return os.path.join(self._input_params['simulation_folder'], 'direct-sun-hours').replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def command(self): + return 'honeybee-radiance post-process convert-to-binary input.mtx --output binary.mtx' + + def requires(self): + return {'DirectRadiationCalculation': DirectRadiationCalculation(_input_params=self._input_params)} + + def output(self): + return { + 'output_mtx': luigi.LocalTarget( + os.path.join(self.execution_folder, '{grid_name}.ill'.format(grid_name=self.grid_name)) + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'input_mtx', 'to': 'input.mtx', 'from': self.input_mtx, 'optional': False}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'output-mtx', 'from': 'binary.mtx', + 'to': os.path.join(self.execution_folder, '{grid_name}.ill'.format(grid_name=self.grid_name)) + }] + + +class DirectRadiationCalculation(QueenbeeTask): + """Calculate daylight contribution for a grid of sensors from a series of modifiers + using rcontrib command.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def fixed_radiance_parameters(self): + return '-aa 0.0 -I -faa -ab 0 -dc 1.0 -dt 0.0 -dj 0.0 -dr 0' + + @property + def conversion(self): + return '0.265 0.670 0.065' + + @property + def sensor_count(self): + return self._input_params['sensor_count'] + + @property + def grid_name(self): + return self._input_params['grid_name'] + + calculate_values = luigi.Parameter(default='value') + + output_format = luigi.Parameter(default='a') + + radiance_parameters = luigi.Parameter(default='') + + @property + def modifiers(self): + value = self._input_params['sun_modifiers'].replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def sensor_grid(self): + value = self._input_params['sensor_grid'].replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def scene_file(self): + value = self._input_params['octree_file'].replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def execution_folder(self): + return os.path.join(self._input_params['simulation_folder'], 'direct-radiation').replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def command(self): + return 'honeybee-radiance dc scontrib scene.oct grid.pts suns.mod --{calculate_values} --sensor-count {sensor_count} --rad-params "{radiance_parameters}" --rad-params-locked "{fixed_radiance_parameters}" --conversion "{conversion}" --output-format {output_format} --output results.ill'.format(calculate_values=self.calculate_values, sensor_count=self.sensor_count, radiance_parameters=self.radiance_parameters, fixed_radiance_parameters=self.fixed_radiance_parameters, conversion=self.conversion, output_format=self.output_format) + + def output(self): + return { + 'result_file': luigi.LocalTarget( + os.path.join(self.execution_folder, '{grid_name}.ill'.format(grid_name=self.grid_name)) + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'modifiers', 'to': 'suns.mod', 'from': self.modifiers, 'optional': False}, + {'name': 'sensor_grid', 'to': 'grid.pts', 'from': self.sensor_grid, 'optional': False}, + {'name': 'scene_file', 'to': 'scene.oct', 'from': self.scene_file, 'optional': False}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'result-file', 'from': 'results.ill', + 'to': os.path.join(self.execution_folder, '{grid_name}.ill'.format(grid_name=self.grid_name)) + }] + + +class _DirectSunHoursCalculation_45518ce1Orchestrator(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): + return [CalculateCumulativeHours(_input_params=self.input_values)] diff --git a/lbt_recipes/direct_sun_hours/flow/dependencies/direct_sun_hours_entry_loop.py b/lbt_recipes/direct_sun_hours/flow/dependencies/direct_sun_hours_entry_loop.py new file mode 100644 index 00000000..f0795804 --- /dev/null +++ b/lbt_recipes/direct_sun_hours/flow/dependencies/direct_sun_hours_entry_loop.py @@ -0,0 +1,407 @@ +import luigi +import os +from queenbee_local import QueenbeeTask +from .dependencies.direct_sun_hours_calculation import _DirectSunHoursCalculation_45518ce1Orchestrator as DirectSunHoursCalculation_45518ce1Workerbee + + +_default_inputs = { 'grid_name': None, + 'octree_file': None, + 'params_folder': '__params', + 'sensor_count': 200, + 'sensor_grid': None, + 'simulation_folder': '.', + 'sun_modifiers': None} + + +class DirectSunlightLoop(luigi.Task): + """No description is provided.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def sensor_count(self): + return self.item['count'] + + @property + def grid_name(self): + return self.item['name'] + + @property + def octree_file(self): + value = self._input_params['octree_file'].replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def sun_modifiers(self): + value = self._input_params['sun_modifiers'].replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def sensor_grid(self): + value = os.path.join(self.input()['SplitGrid']['output_folder'].path, self.item['path']).replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def scene_file(self): + value = self._input_params['octree_file'].replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + # get item for loop + try: + item = luigi.DictParameter() + except Exception: + item = luigi.Parameter() + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + @property + def map_dag_inputs(self): + """Map task inputs to DAG inputs.""" + inputs = { + 'simulation_folder': self.execution_folder, + 'octree_file': self.octree_file, + 'sensor_count': self.sensor_count, + 'grid_name': self.grid_name, + 'sun_modifiers': self.sun_modifiers, + 'sensor_grid': self.sensor_grid, + 'scene_file': self.scene_file + } + try: + inputs['__debug__'] = self._input_params['__debug__'] + except KeyError: + # not debug mode + pass + + return inputs + + def run(self): + yield [DirectSunHoursCalculation_45518ce1Workerbee(_input_params=self.map_dag_inputs)] + with open(os.path.join(self.execution_folder, 'direct_sunlight.done'), 'w') as out_file: + out_file.write('done!\n') + + def requires(self): + return {'SplitGrid': SplitGrid(_input_params=self._input_params)} + + def output(self): + return { + 'is_done': luigi.LocalTarget(os.path.join(self.execution_folder, 'direct_sunlight.done')) + } + + +class DirectSunlight(luigi.Task): + """No description is provided.""" + # global parameters + _input_params = luigi.DictParameter() + @property + def grids_list(self): + value = self.input()['SplitGrid']['grids_list'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def items(self): + try: + # assume the input is a file + return QueenbeeTask.load_input_param(self.grids_list) + except: + # it is a parameter + return self.input()['SplitGrid']['grids_list'].path + + def run(self): + yield [DirectSunlightLoop(item=item, _input_params=self._input_params) for item in self.items] + with open(os.path.join(self.execution_folder, 'direct_sunlight.done'), 'w') as out_file: + out_file.write('done!\n') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def requires(self): + return {'SplitGrid': SplitGrid(_input_params=self._input_params)} + + def output(self): + return { + 'is_done': luigi.LocalTarget(os.path.join(self.execution_folder, 'direct_sunlight.done')) + } + + +class MergeCumulativeSunHours(QueenbeeTask): + """Merge several files with similar starting name into one.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def name(self): + return self._input_params['grid_name'] + + @property + def extension(self): + return '.res' + + @property + def folder(self): + value = 'cumulative-sun-hours'.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def command(self): + return 'honeybee-radiance grid merge input_folder grid {extension} --name {name}'.format(extension=self.extension, name=self.name) + + def requires(self): + return {'DirectSunlight': DirectSunlight(_input_params=self._input_params)} + + def output(self): + return { + 'result_file': luigi.LocalTarget( + os.path.join(self.execution_folder, '../../results/cumulative/{name}.res'.format(name=self.name)) + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'folder', 'to': 'input_folder', 'from': self.folder, 'optional': False}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'result-file', 'from': '{name}{extension}'.format(name=self.name, extension=self.extension), + 'to': os.path.join(self.execution_folder, '../../results/cumulative/{name}.res'.format(name=self.name)) + }] + + +class MergeDirectSunHours(QueenbeeTask): + """Merge several files with similar starting name into one.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def name(self): + return self._input_params['grid_name'] + + @property + def extension(self): + return '.ill' + + @property + def folder(self): + value = 'direct-sun-hours'.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def command(self): + return 'honeybee-radiance grid merge input_folder grid {extension} --name {name}'.format(extension=self.extension, name=self.name) + + def requires(self): + return {'DirectSunlight': DirectSunlight(_input_params=self._input_params)} + + def output(self): + return { + 'result_file': luigi.LocalTarget( + os.path.join(self.execution_folder, '../../results/direct_sun_hours/{name}.ill'.format(name=self.name)) + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'folder', 'to': 'input_folder', 'from': self.folder, 'optional': False}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'result-file', 'from': '{name}{extension}'.format(name=self.name, extension=self.extension), + 'to': os.path.join(self.execution_folder, '../../results/direct_sun_hours/{name}.ill'.format(name=self.name)) + }] + + +class MergeRadiationResults(QueenbeeTask): + """Merge several files with similar starting name into one.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def name(self): + return self._input_params['grid_name'] + + @property + def extension(self): + return '.ill' + + @property + def folder(self): + value = 'direct-radiation'.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def command(self): + return 'honeybee-radiance grid merge input_folder grid {extension} --name {name}'.format(extension=self.extension, name=self.name) + + def requires(self): + return {'DirectSunlight': DirectSunlight(_input_params=self._input_params)} + + def output(self): + return { + 'result_file': luigi.LocalTarget( + os.path.join(self.execution_folder, '../../results/direct_radiation/{name}.ill'.format(name=self.name)) + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'folder', 'to': 'input_folder', 'from': self.folder, 'optional': False}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'result-file', 'from': '{name}{extension}'.format(name=self.name, extension=self.extension), + 'to': os.path.join(self.execution_folder, '../../results/direct_radiation/{name}.ill'.format(name=self.name)) + }] + + +class SplitGrid(QueenbeeTask): + """Split a single sensor grid file into multiple smaller grids.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def sensor_count(self): + return self._input_params['sensor_count'] + + @property + def input_grid(self): + value = self._input_params['sensor_grid'].replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def command(self): + return 'honeybee-radiance grid split grid.pts {sensor_count} --folder output --log-file output/grids_info.json'.format(sensor_count=self.sensor_count) + + def output(self): + return { + + 'output_folder': luigi.LocalTarget( + os.path.join(self.execution_folder, 'sub_grids') + ), + 'grids_list': luigi.LocalTarget( + os.path.join( + self.params_folder, + 'output/grids_info.json') + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'input_grid', 'to': 'grid.pts', 'from': self.input_grid, 'optional': False}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'output-folder', 'from': 'output', + 'to': os.path.join(self.execution_folder, 'sub_grids') + }] + + @property + def output_parameters(self): + return [{'name': 'grids-list', 'from': 'output/grids_info.json', 'to': os.path.join(self.params_folder, 'output/grids_info.json')}] + + +class _DirectSunHoursEntryLoop_45518ce1Orchestrator(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): + return [MergeCumulativeSunHours(_input_params=self.input_values), MergeDirectSunHours(_input_params=self.input_values), MergeRadiationResults(_input_params=self.input_values)] diff --git a/lbt_recipes/direct_sun_hours/flow/main.py b/lbt_recipes/direct_sun_hours/flow/main.py new file mode 100644 index 00000000..3de553de --- /dev/null +++ b/lbt_recipes/direct_sun_hours/flow/main.py @@ -0,0 +1,478 @@ +import luigi +import os +from queenbee_local import QueenbeeTask +from .dependencies.direct_sun_hours_entry_loop import _DirectSunHoursEntryLoop_45518ce1Orchestrator as DirectSunHoursEntryLoop_45518ce1Workerbee + + +_default_inputs = { 'model': None, + 'north': 0.0, + 'params_folder': '__params', + 'sensor_count': 200, + 'sensor_grid': '*', + 'simulation_folder': '.', + 'wea': None} + + +class CopyGridInfo(QueenbeeTask): + """Copy a file or folder to multiple destinations.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def src(self): + value = self.input()['CreateRadFolder']['sensor_grids_file'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def command(self): + return 'echo copying input path...' + + def requires(self): + return {'CreateRadFolder': CreateRadFolder(_input_params=self._input_params)} + + def output(self): + return { + 'dst_1': luigi.LocalTarget( + os.path.join(self.execution_folder, 'results/cumulative/grids_info.json') + ), + + 'dst_2': luigi.LocalTarget( + os.path.join(self.execution_folder, 'results/direct_radiation/grids_info.json') + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'src', 'to': 'input_path', 'from': self.src, 'optional': False}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'dst-1', 'from': 'input_path', + 'to': os.path.join(self.execution_folder, 'results/cumulative/grids_info.json') + }, + + { + 'name': 'dst-2', 'from': 'input_path', + 'to': os.path.join(self.execution_folder, 'results/direct_radiation/grids_info.json') + }] + + +class CreateOctree(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 = self.input()['CreateRadFolder']['model_folder'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def sky(self): + value = self.input()['GenerateSunpath']['sunpath'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + 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( + os.path.join(self.execution_folder, 'resources/scene_with_suns.oct') + ) + } + + @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': os.path.join(self.execution_folder, 'resources/scene_with_suns.oct') + }] + + +class CreateRadFolder(QueenbeeTask): + """Create a Radiance folder from a HBJSON input file.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def sensor_grid(self): + return self._input_params['sensor_grid'] + + @property + def input_model(self): + value = self._input_params['model'].replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def command(self): + return 'honeybee-radiance translate model-to-rad-folder model.hbjson --grid "{sensor_grid}"'.format(sensor_grid=self.sensor_grid) + + def output(self): + return { + + 'model_folder': luigi.LocalTarget( + os.path.join(self.execution_folder, 'model') + ), + + 'sensor_grids_file': luigi.LocalTarget( + os.path.join(self.execution_folder, 'results/direct_sun_hours/grids_info.json') + ), + 'sensor_grids': luigi.LocalTarget( + os.path.join( + self.params_folder, + 'model/grid/_info.json') + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'input_model', 'to': 'model.hbjson', 'from': self.input_model, 'optional': False}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'model-folder', 'from': 'model', + 'to': os.path.join(self.execution_folder, 'model') + }, + + { + 'name': 'sensor-grids-file', 'from': 'model/grid/_info.json', + 'to': os.path.join(self.execution_folder, 'results/direct_sun_hours/grids_info.json') + }] + + @property + def output_parameters(self): + return [{'name': 'sensor-grids', 'from': 'model/grid/_info.json', 'to': os.path.join(self.params_folder, 'model/grid/_info.json')}] + + +class DirectSunHoursRaytracingLoop(luigi.Task): + """No description is provided.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def sensor_count(self): + return self._input_params['sensor_count'] + + @property + def grid_name(self): + return self.item['full_id'] + + @property + def octree_file(self): + value = self.input()['CreateOctree']['scene_file'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def sensor_grid(self): + value = os.path.join(self.input()['CreateRadFolder']['model_folder'].path, 'grid/{item_full_id}.pts'.format(item_full_id=self.item['full_id'])).replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def sunpath(self): + value = self.input()['GenerateSunpath']['sunpath'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def sun_modifiers(self): + value = self.input()['GenerateSunpath']['sun_modifiers'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + # get item for loop + try: + item = luigi.DictParameter() + except Exception: + item = luigi.Parameter() + + @property + def execution_folder(self): + return os.path.join(self._input_params['simulation_folder'], 'initial_results/{item_name}'.format(item_name=self.item['name'])).replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + @property + def map_dag_inputs(self): + """Map task inputs to DAG inputs.""" + inputs = { + 'simulation_folder': self.execution_folder, + 'sensor_count': self.sensor_count, + 'octree_file': self.octree_file, + 'grid_name': self.grid_name, + 'sensor_grid': self.sensor_grid, + 'sunpath': self.sunpath, + 'sun_modifiers': self.sun_modifiers + } + try: + inputs['__debug__'] = self._input_params['__debug__'] + except KeyError: + # not debug mode + pass + + return inputs + + def run(self): + yield [DirectSunHoursEntryLoop_45518ce1Workerbee(_input_params=self.map_dag_inputs)] + with open(os.path.join(self.execution_folder, 'direct_sun_hours_raytracing.done'), 'w') as out_file: + out_file.write('done!\n') + + def requires(self): + return {'CreateOctree': CreateOctree(_input_params=self._input_params), 'GenerateSunpath': GenerateSunpath(_input_params=self._input_params), 'CreateRadFolder': CreateRadFolder(_input_params=self._input_params)} + + def output(self): + return { + 'is_done': luigi.LocalTarget(os.path.join(self.execution_folder, 'direct_sun_hours_raytracing.done')) + } + + +class DirectSunHoursRaytracing(luigi.Task): + """No description is provided.""" + # global parameters + _input_params = luigi.DictParameter() + @property + def sensor_grids(self): + value = self.input()['CreateRadFolder']['sensor_grids'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def items(self): + try: + # assume the input is a file + return QueenbeeTask.load_input_param(self.sensor_grids) + except: + # it is a parameter + return self.input()['CreateRadFolder']['sensor_grids'].path + + def run(self): + yield [DirectSunHoursRaytracingLoop(item=item, _input_params=self._input_params) for item in self.items] + with open(os.path.join(self.execution_folder, 'direct_sun_hours_raytracing.done'), 'w') as out_file: + out_file.write('done!\n') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def requires(self): + return {'CreateOctree': CreateOctree(_input_params=self._input_params), 'GenerateSunpath': GenerateSunpath(_input_params=self._input_params), 'CreateRadFolder': CreateRadFolder(_input_params=self._input_params)} + + def output(self): + return { + 'is_done': luigi.LocalTarget(os.path.join(self.execution_folder, 'direct_sun_hours_raytracing.done')) + } + + +class GenerateSunpath(QueenbeeTask): + """Generate a Radiance sun matrix (AKA sun-path).""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def north(self): + return self._input_params['north'] + + @property + def output_type(self): + return '1' + + @property + def wea(self): + value = self._input_params['wea'].replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def command(self): + return 'gendaymtx -n -D sunpath.mtx -M suns.mod -O{output_type} -r {north} -v sky.wea'.format(output_type=self.output_type, north=self.north) + + def output(self): + return { + 'sunpath': luigi.LocalTarget( + os.path.join(self.execution_folder, 'resources/sunpath.mtx') + ), + + 'sun_modifiers': luigi.LocalTarget( + os.path.join(self.execution_folder, 'resources/suns.mod') + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'wea', 'to': 'sky.wea', 'from': self.wea, 'optional': False}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'sunpath', 'from': 'sunpath.mtx', + 'to': os.path.join(self.execution_folder, 'resources/sunpath.mtx') + }, + + { + 'name': 'sun-modifiers', 'from': 'suns.mod', + 'to': os.path.join(self.execution_folder, 'resources/suns.mod') + }] + + +class ParseSunUpHours(QueenbeeTask): + """Parse sun up hours from sun modifiers file.""" + + # DAG Input parameters + _input_params = luigi.DictParameter() + + # Task inputs + @property + def sun_modifiers(self): + value = self.input()['GenerateSunpath']['sun_modifiers'].path.replace('\\', '/') + return value if os.path.isabs(value) \ + else os.path.join(self.initiation_folder, value) + + @property + def execution_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def initiation_folder(self): + return self._input_params['simulation_folder'].replace('\\', '/') + + @property + def params_folder(self): + return os.path.join(self.execution_folder, self._input_params['params_folder']).replace('\\', '/') + + def command(self): + return 'honeybee-radiance sunpath parse-hours suns.mod --name sun-up-hours.txt' + + def requires(self): + return {'GenerateSunpath': GenerateSunpath(_input_params=self._input_params)} + + def output(self): + return { + 'sun_up_hours': luigi.LocalTarget( + os.path.join(self.execution_folder, 'results/direct_sun_hours/sun-up-hours.txt') + ) + } + + @property + def input_artifacts(self): + return [ + {'name': 'sun_modifiers', 'to': 'suns.mod', 'from': self.sun_modifiers, 'optional': False}] + + @property + def output_artifacts(self): + return [ + { + 'name': 'sun-up-hours', 'from': 'sun-up-hours.txt', + 'to': os.path.join(self.execution_folder, 'results/direct_sun_hours/sun-up-hours.txt') + }] + + +class _Main_45518ce1Orchestrator(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): + return [CopyGridInfo(_input_params=self.input_values), DirectSunHoursRaytracing(_input_params=self.input_values), ParseSunUpHours(_input_params=self.input_values)] diff --git a/lbt_recipes/direct_sun_hours/package.json b/lbt_recipes/direct_sun_hours/package.json new file mode 100644 index 00000000..a6ed99c3 --- /dev/null +++ b/lbt_recipes/direct_sun_hours/package.json @@ -0,0 +1,299 @@ +{ + "type": "RecipeInterface", + "annotations": {}, + "api_version": "v1beta1", + "metadata": { + "type": "MetaData", + "annotations": {}, + "name": "direct-sun-hours", + "tag": "0.1.0", + "app_version": null, + "keywords": [ + "honeybee", + "radiance", + "ladybug-tools", + "daylight", + "direct-sun-hours" + ], + "maintainers": [ + { + "type": "Maintainer", + "annotations": {}, + "name": "mostapha", + "email": "mostapha@ladybug.tools" + }, + { + "type": "Maintainer", + "annotations": {}, + "name": "ladybug-tools", + "email": "info@ladybug.tools" + } + ], + "home": "https://github.com/pollination/direct-sun-hours", + "sources": [ + "https://hub.docker.com/r/ladybugtools/honeybee-radiance" + ], + "icon": "https://raw.githubusercontent.com/ladybug-tools/artwork/master/icons_components/honeybee/png/annualrecipe.png", + "deprecated": null, + "description": "Direct sun hours recipe for Pollination.", + "license": { + "type": "License", + "annotations": {}, + "name": "PolyForm Shield License 1.0.0", + "url": "https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt" + } + }, + "source": null, + "inputs": [ + { + "type": "DAGFileInput", + "annotations": {}, + "name": "model", + "description": "A Honeybee model in HBJSON file format.", + "default": null, + "alias": [ + { + "type": "DAGGenericInputAlias", + "annotations": {}, + "name": "model", + "description": "A path to a HBJSON file or a HB model object built with Python or dotnet libraries.", + "platform": [ + "grasshopper" + ], + "handler": [ + { + "type": "IOAliasHandler", + "annotations": {}, + "language": "python", + "module": "pollination_handlers.inputs.model", + "function": "model_to_json" + }, + { + "type": "IOAliasHandler", + "annotations": {}, + "language": "csharp", + "module": "Pollination.Handlers", + "function": "HBModelToJSON" + } + ], + "default": null, + "required": true, + "spec": null + }, + { + "type": "DAGLinkedInputAlias", + "annotations": {}, + "name": "model", + "description": "This input links the model to Rhino model.", + "platform": [ + "rhino" + ], + "handler": [ + { + "type": "IOAliasHandler", + "annotations": {}, + "language": "csharp", + "module": "Pollination.Handlers", + "function": "RhinoHBModelToJSON" + } + ], + "default": null, + "required": true, + "spec": null + } + ], + "required": true, + "spec": null, + "extensions": [ + "json", + "hbjson" + ] + }, + { + "type": "DAGNumberInput", + "annotations": {}, + "name": "north", + "description": "A number for rotation from north.", + "default": 0.0, + "alias": [ + { + "type": "DAGGenericInputAlias", + "annotations": {}, + "name": "north", + "description": "Either a Vector2D for the north direction or a number between -360 and 360 for the counterclockwise difference between the North and the positive Y-axis in degrees.", + "platform": [ + "grasshopper" + ], + "handler": [ + { + "type": "IOAliasHandler", + "annotations": {}, + "language": "python", + "module": "pollination_handlers.inputs.north", + "function": "north_vector_to_angle" + } + ], + "default": null, + "required": true, + "spec": null + } + ], + "required": false, + "spec": { + "type": "number", + "minimum": 0, + "maximum": 360 + } + }, + { + "type": "DAGIntegerInput", + "annotations": {}, + "name": "sensor-count", + "description": "The maximum number of grid points per parallel execution.", + "default": 200, + "alias": [], + "required": false, + "spec": { + "type": "integer", + "minimum": 1 + } + }, + { + "type": "DAGStringInput", + "annotations": {}, + "name": "sensor-grid", + "description": "A grid name or a pattern to filter the sensor grids. By default all the grids in HBJSON model will be exported.", + "default": "*", + "alias": [], + "required": false, + "spec": null + }, + { + "type": "DAGFileInput", + "annotations": {}, + "name": "wea", + "description": "Wea file.", + "default": null, + "alias": [ + { + "type": "DAGGenericInputAlias", + "annotations": {}, + "name": "wea", + "description": "Either a Wea python object or the path to a wea or an epw file.", + "platform": [ + "grasshopper" + ], + "handler": [ + { + "type": "IOAliasHandler", + "annotations": {}, + "language": "python", + "module": "pollination_handlers.inputs.wea", + "function": "wea_handler" + } + ], + "default": null, + "required": true, + "spec": null + } + ], + "required": true, + "spec": null, + "extensions": [ + "wea" + ] + } + ], + "outputs": [ + { + "type": "DAGFolderOutput", + "annotations": {}, + "name": "cumulative-sun-hours", + "description": "Cumulative results for direct sun hours for all the input hours.", + "from": { + "type": "FolderReference", + "annotations": {}, + "path": "results/cumulative" + }, + "alias": [ + { + "type": "DAGGenericOutputAlias", + "annotations": {}, + "name": "direct_sun_hours", + "description": "Hours of direct sun.", + "platform": [ + "grasshopper" + ], + "handler": [ + { + "type": "IOAliasHandler", + "annotations": {}, + "language": "python", + "module": "pollination_handlers.outputs.daylight", + "function": "read_hours_from_folder" + } + ] + } + ], + "required": true + }, + { + "type": "DAGFolderOutput", + "annotations": {}, + "name": "direct-radiation", + "description": "Hourly direct radiation results. These results only includes the direct radiation from sun disk.", + "from": { + "type": "FolderReference", + "annotations": {}, + "path": "results/direct_radiation" + }, + "alias": [], + "required": true + }, + { + "type": "DAGFolderOutput", + "annotations": {}, + "name": "direct-sun-hours", + "description": "Hourly results for direct sun hours.", + "from": { + "type": "FolderReference", + "annotations": {}, + "path": "results/direct_sun_hours" + }, + "alias": [ + { + "type": "DAGGenericOutputAlias", + "annotations": {}, + "name": "annual_daylight", + "description": "Annual daylight result files.", + "platform": [ + "grasshopper" + ], + "handler": [ + { + "type": "IOAliasHandler", + "annotations": {}, + "language": "python", + "module": "pollination_handlers.outputs.daylight", + "function": "sort_ill_from_folder" + } + ] + } + ], + "required": true + }, + { + "type": "DAGFolderOutput", + "annotations": {}, + "name": "results", + "description": "Results folder. There are 3 subfolders under results folder: direct_sun_hours, cumulative and direct_radiation.", + "from": { + "type": "FolderReference", + "annotations": {}, + "path": "results" + }, + "alias": [], + "required": true + } + ] +} \ No newline at end of file diff --git a/lbt_recipes/direct_sun_hours/run.py b/lbt_recipes/direct_sun_hours/run.py new file mode 100644 index 00000000..4fffc220 --- /dev/null +++ b/lbt_recipes/direct_sun_hours/run.py @@ -0,0 +1,64 @@ +import sys +import luigi +import os +import time +from multiprocessing import freeze_support +from queenbee_local import local_scheduler, _copy_artifacts, update_params, parse_input_args + +import flow.main as direct_sun_hours_workerbee + + +_recipe_default_inputs = { 'model': None, + 'north': 0.0, + 'sensor_count': 200, + 'sensor_grid': '*', + 'wea': None} + + +class LetDirectSunHoursFly(luigi.WrapperTask): + # global parameters + _input_params = luigi.DictParameter() + + def requires(self): + yield [direct_sun_hours_workerbee._Main_45518ce1Orchestrator(_input_params=self._input_params)] + + +def start(project_folder, user_values, workers): + freeze_support() + + input_params = update_params(_recipe_default_inputs, user_values) + + if 'simulation_folder' not in input_params or not input_params['simulation_folder']: + if 'simulation_id' not in input_params or not input_params['simulation_id']: + simulation_id = 'direct_sun_hours_%d' % int(round(time.time(), 2) * 100) + else: + simulation_id = input_params['simulation_id'] + + simulation_folder = os.path.join(project_folder, simulation_id) + input_params['simulation_folder'] = simulation_folder + else: + simulation_folder = input_params['simulation_folder'] + + # copy project folder content to simulation folder + artifacts = ['model', 'wea'] + optional_artifacts = [] + for artifact in artifacts: + value = input_params[artifact] + if value is None: + if artifact in optional_artifacts: + continue + raise ValueError('None value for required artifact input: %s' % artifact) + from_ = os.path.join(project_folder, input_params[artifact]) + to_ = os.path.join(simulation_folder, input_params[artifact]) + _copy_artifacts(from_, to_) + + luigi.build( + [LetDirectSunHoursFly(_input_params=input_params)], + local_scheduler=local_scheduler(), + workers=workers + ) + + +if __name__ == '__main__': + project_folder, user_values, workers = parse_input_args(sys.argv) + start(project_folder, user_values, workers)