From a20b5a91f75fad548308184a4c8ba351b12017d3 Mon Sep 17 00:00:00 2001 From: Mikkel Pedersen Date: Wed, 28 Aug 2024 18:37:10 +0200 Subject: [PATCH] fix(thermal_map): Add support for NumPy results --- .../icon/HB Read Environment Matrix.png | Bin 1308 -> 1308 bytes .../icon/HB Read Thermal Matrix.png | Bin 1474 -> 1474 bytes .../json/HB_Read_Environment_Matrix.json | 4 +- .../json/HB_Read_Thermal_Matrix.json | 4 +- .../src/HB Read Environment Matrix.py | 88 +++--------------- .../src/HB Read Thermal Matrix.py | 70 ++++++++++---- .../HB Read Environment Matrix.ghuser | Bin 7332 -> 6596 bytes .../HB Read Thermal Matrix.ghuser | Bin 5991 -> 6460 bytes 8 files changed, 69 insertions(+), 97 deletions(-) diff --git a/honeybee_grasshopper_energy/icon/HB Read Environment Matrix.png b/honeybee_grasshopper_energy/icon/HB Read Environment Matrix.png index a24fd4fea447d30d1ef350e4ef2ea71f81f93024..5eacc2cda68be8069e2740b6046fb43756644099 100644 GIT binary patch delta 764 zcmVgcKiq*(*@lV>LBzU3xZCmA?%WEYt*wn#5FV+3 z{ti?5;O`ePbIEdKeANI&6h?1R=IjraX;s(g8Iy|w9DmM$wug$hU%8Ao-<=C#(-~-+ zeuN^EeTHNrK9{Reqqjgk9HF=99x|$fNZ)=Lp33WR@4kkV^53wm=}!zih$0f92Ta9p zUcZWIa~DD^I|H?85L#^zftmA)49P?k!XM4n@Bh|w(AFG)eq{rDt|F7ZgY>mWvHGIk z>&M&-wtr!I!3;QyN|9B01sZ(|+4LP`xLj3+WFkI+$;-@}@v*9AZS-@FS|vSwo4;_* zC!)o?cj5dwEf-Fo!cV7L(c0RIlPxWHyP)uz)ncvAbf?uSuS_WR=T2UcDcCD%Ualee z?Bq|%)zU<9yU20XGB0lD?T&Klc8)}!5kgEUseh@dDrdE7EX%%RH=BLbJ%-wIX0us| zPZ~j>X43~2i$$V2irsE^P-FD5?$cvWL4z}RhIt?n5^@T{B^Qy9l$4ZCLk>wKAvbZI zH;)-JHI3)HWRAN|{YkWLEKOcKX^r&mIFeupFoTMhT9yx(dls{~kP0=?-ENL28PNGB=-D+HpZ|zCIXT(MBqh0NPd)mW`d-un uR;!h#-!3}TZJUmB8$sNRe^t?VS!AMQY?Y(vF`AYxr1-0gU5cW#By*49Q(5FR-J z{T-(A!QU@p=91;e_^JVlD2!gB%-J6-)2gn~9+Qg#9DjO1%R|N6uUy8P@6Ls==?t_@ zKSGhoK0`7QpUc&#(OaM%j?i0l4;j@#q;J0rPvv#EcV9zF`EOX(^d|-$L=lP522=5y z*RNvQ+=UR!&OmJ%gjO3wVCK9cLoyMC@JF-t`@i)Zv^58yU)jK(tH`A9AbssotiGt% z`Y|_yZGV_vFayq_Qe;(Lfkxj#Hhl*fE?1QynTStd@-p*ge5|Tj8~vQ4R!L9a<}aM{ ziD)tJT{wSE%Z1aY@YCs5w6?b5WJ?R)E-1WawOFe&-D$PTD-(+Sxsz993ie8xmupBq zJNc7xwKP%OE^=J8%!}K3yQ7@Cog>j_gb-6oYJX~~%2}-%%d#)o&1N5UkD>OQ*=$zg zlSUAz+4OQRT&s)YOmkvB)qLaluh|UgpVZfo03-=&xX3xVT`3CFFJMKS(|mXeY}oP3rA4f8ge-*7*T-UC^ZdkG;oWx7*Y zk_H)@ZDawSZ^B*xMCnr0Zq$D@3sYf4E--~*{ uYPIt8+eL@^t3O00004A2HpSw delta 22 ecmX@aeTaL42RGkg1_r*vjK}j=q-+e%X9WOQya(F= diff --git a/honeybee_grasshopper_energy/json/HB_Read_Environment_Matrix.json b/honeybee_grasshopper_energy/json/HB_Read_Environment_Matrix.json index 1f2f3a8f..1a0bf5d9 100644 --- a/honeybee_grasshopper_energy/json/HB_Read_Environment_Matrix.json +++ b/honeybee_grasshopper_energy/json/HB_Read_Environment_Matrix.json @@ -1,5 +1,5 @@ { - "version": "1.8.0", + "version": "1.8.1", "nickname": "EnvMtx", "outputs": [ [ @@ -36,7 +36,7 @@ } ], "subcategory": "7 :: Thermal Map", - "code": "\nimport subprocess\nimport os\nimport shutil\nimport json\n\ntry:\n from ladybug.datatype.temperature import AirTemperature, \\\n MeanRadiantTemperature, RadiantTemperature\n from ladybug.datatype.temperaturedelta import RadiantTemperatureDelta\n from ladybug.datatype.fraction import RelativeHumidity\n from ladybug.header import Header\n from ladybug.datacollection import HourlyContinuousCollection, \\\n HourlyDiscontinuousCollection\n from ladybug.futil import csv_to_num_matrix\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug:\\n\\t{}'.format(e))\n\ntry:\n from honeybee.config import folders\nexcept ImportError as e:\n raise ImportError('\\nFailed to import honeybee:\\n\\t{}'.format(e))\n\ntry:\n from ladybug_{{cad}}.{{plugin}} import all_required_inputs, objectify_output\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug_{{cad}}:\\n\\t{}'.format(e))\n\nENV_CONDS_MAP = {\n '0': 'mrt',\n 'mrt': 'mrt',\n 'mean radiant temperature': 'mrt',\n '1': 'air_temperature',\n 'air temperature': 'air_temperature',\n '2': 'longwave_mrt',\n 'longwave mrt': 'longwave_mrt',\n '3': 'shortwave_mrt',\n 'shortwave mrt': 'shortwave_mrt',\n 'shortwave mrt delta': 'shortwave_mrt',\n '4': 'rel_humidity',\n 'relative humidity': 'rel_humidity'\n}\n\n\ndef load_matrix(comf_result):\n \"\"\"Load a matrix of data into an object that can be output in {{Plugin}}.\n\n Args:\n comf_result: Path to a folder with CSV data to be loaded into {{Plugin}}.\n \"\"\"\n # parse the result_info.json into a data collection header\n with open(os.path.join(comf_result, 'results_info.json')) as json_file:\n data_header = Header.from_dict(json.load(json_file))\n a_per = data_header.analysis_period\n continuous = True if a_per.st_hour == 0 and a_per.end_hour == 23 else False\n if not continuous:\n dates = a_per.datetimes\n\n # parse the grids_info.json with the correct order of the grid files\n with open(os.path.join(comf_result, 'grids_info.json')) as json_file:\n grid_list = json.load(json_file)\n\n # loop through the grid CSV files, parse their results, and build data collections\n comf_matrix = []\n for grid in grid_list:\n grid_name = grid['full_id'] if 'full_id' in grid else 'id'\n metadata = {'grid': grid_name}\n grid_file = os.path.join(comf_result, '{}.csv'.format(grid_name))\n data_matrix = csv_to_num_matrix(grid_file)\n grid_data = []\n for i, row in enumerate(data_matrix):\n header = data_header.duplicate()\n header.metadata = metadata.copy()\n header.metadata['sensor_index'] = i\n data = HourlyContinuousCollection(header, row) if continuous else \\\n HourlyDiscontinuousCollection(header, row, dates)\n grid_data.append(data)\n comf_matrix.append(grid_data)\n\n # wrap the maptrix into an object so that it does not slow the {{Plugin}} UI\n comf_mtx = objectify_output(\n '{} Matrix'.format(data_header.data_type.name), comf_matrix)\n return comf_mtx\n\n\ndef create_result_header(env_conds, sub_path):\n \"\"\"Create a DataCollection Header for a given metric.\"\"\"\n with open(os.path.join(env_conds, 'results_info.json')) as json_file:\n base_head = Header.from_dict(json.load(json_file))\n if sub_path == 'mrt':\n return Header(MeanRadiantTemperature(), 'C', base_head.analysis_period)\n elif sub_path == 'air_temperature':\n return Header(AirTemperature(), 'C', base_head.analysis_period)\n elif sub_path == 'longwave_mrt':\n return Header(RadiantTemperature(), 'C', base_head.analysis_period)\n elif sub_path == 'shortwave_mrt':\n return Header(RadiantTemperatureDelta(), 'dC', base_head.analysis_period)\n elif sub_path == 'rel_humidity':\n return Header(RelativeHumidity(), '%', base_head.analysis_period)\n\n\ndef sum_matrices(mtxs_1, mtxs_2, dest_dir):\n \"\"\"Sum together matrices of two folders.\"\"\"\n if not os.path.isdir(dest_dir):\n os.makedirs(dest_dir)\n for mtx_file in os.listdir(mtxs_1):\n if mtx_file.endswith('.csv'):\n mtx_file1 = os.path.join(mtxs_1, mtx_file)\n mtx_file2 = os.path.join(mtxs_2, mtx_file)\n matrix_1 = csv_to_num_matrix(mtx_file1)\n matrix_2 = csv_to_num_matrix(mtx_file2)\n data = [[d1 + d2 for d1, d2 in zip(r1, r2)]\n for r1, r2 in zip(matrix_1, matrix_2)]\n csv_path = os.path.join(dest_dir, mtx_file)\n with open(csv_path, 'w') as csv_file:\n for dat in data:\n str_data = (str(v) for v in dat)\n csv_file.write(','.join(str_data) + '\\n')\n elif mtx_file == 'grids_info.json':\n shutil.copyfile(\n os.path.join(mtxs_1, mtx_file),\n os.path.join(dest_dir, mtx_file)\n )\n\n\nif all_required_inputs(ghenv.Component) and _load:\n # get the folders and that correspond with the requested metric\n _metric_ = _metric_ if _metric_ is not None else 'mrt'\n try:\n sub_path = ENV_CONDS_MAP[_metric_.lower()]\n except KeyError:\n raise ValueError(\n 'Input metric \"{}\" is not recognized. Choose from: {}'.format(\n _metric_, '\\n'.join(ENV_CONDS_MAP.keys()))\n )\n source_folder = os.path.join(_env_conds, sub_path)\n dest_folder = os.path.join(_env_conds, 'final', sub_path)\n\n # if the results have already been processed, then load them up\n if os.path.isdir(dest_folder):\n comf_mtx = load_matrix(dest_folder)\n else: # otherwise, process them into a load-able format\n # make sure the requested metric is valid for the study\n if sub_path == 'mrt':\n source_folders = [os.path.join(_env_conds, 'longwave_mrt'),\n os.path.join(_env_conds, 'shortwave_mrt')]\n dest_folders = [os.path.join(_env_conds, 'final', 'longwave_mrt'),\n os.path.join(_env_conds, 'final', 'shortwave_mrt')]\n else:\n assert os.path.isdir(source_folder), \\\n 'Metric \"{}\" does not exist for this comfort study.'.format(sub_path)\n source_folders, dest_folders = [source_folder], [dest_folder]\n # restructure the results to align with the sensor grids\n dist_info = os.path.join(_env_conds, '_redist_info.json')\n for src_f, dst_f in zip(source_folders, dest_folders):\n if not os.path.isdir(dst_f):\n os.makedirs(dst_f)\n cmds = [folders.python_exe_path, '-m', 'honeybee_radiance', 'grid',\n 'merge-folder', src_f, dst_f, 'csv',\n '--dist-info', dist_info]\n shell = True if os.name == 'nt' else False\n custom_env = os.environ.copy()\n custom_env['PYTHONHOME'] = ''\n process = subprocess.Popen(\n cmds, stdout=subprocess.PIPE, shell=shell, env=custom_env)\n stdout = process.communicate()\n grid_info_src = os.path.join(_env_conds, 'grids_info.json')\n grid_info_dst = os.path.join(dst_f, 'grids_info.json')\n shutil.copyfile(grid_info_src, grid_info_dst)\n data_header = create_result_header(_env_conds, os.path.split(dst_f)[-1])\n result_info_path = os.path.join(dst_f, 'results_info.json')\n with open(result_info_path, 'w') as fp:\n json.dump(data_header.to_dict(), fp, indent=4)\n # if MRT was requested, sum together the longwave and shortwave\n if sub_path == 'mrt':\n sum_matrices(dest_folders[0], dest_folders[1], dest_folder)\n data_header = create_result_header(_env_conds, sub_path)\n result_info_path = os.path.join(dest_folder, 'results_info.json')\n with open(result_info_path, 'w') as fp:\n json.dump(data_header.to_dict(), fp, indent=4)\n # load the resulting matrix into {{Plugin}}\n comf_mtx = load_matrix(dest_folder)\n", + "code": "\nimport subprocess\nimport os\nimport shutil\nimport json\n\ntry:\n from ladybug.datatype.temperature import AirTemperature, \\\n MeanRadiantTemperature, RadiantTemperature\n from ladybug.datatype.temperaturedelta import RadiantTemperatureDelta\n from ladybug.datatype.fraction import RelativeHumidity\n from ladybug.header import Header\n from ladybug.datacollection import HourlyContinuousCollection, \\\n HourlyDiscontinuousCollection\n from ladybug.futil import csv_to_num_matrix\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug:\\n\\t{}'.format(e))\n\ntry:\n from honeybee.config import folders\nexcept ImportError as e:\n raise ImportError('\\nFailed to import honeybee:\\n\\t{}'.format(e))\n\ntry:\n from ladybug_{{cad}}.{{plugin}} import all_required_inputs, objectify_output\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug_{{cad}}:\\n\\t{}'.format(e))\n\nENV_CONDS_MAP = {\n '0': 'mrt',\n 'mrt': 'mrt',\n 'mean radiant temperature': 'mrt',\n '1': 'air_temperature',\n 'air temperature': 'air_temperature',\n '2': 'longwave_mrt',\n 'longwave mrt': 'longwave_mrt',\n '3': 'shortwave_mrt',\n 'shortwave mrt': 'shortwave_mrt',\n 'shortwave mrt delta': 'shortwave_mrt',\n '4': 'rel_humidity',\n 'relative humidity': 'rel_humidity'\n}\n\n\ndef load_matrix(comf_result):\n \"\"\"Load a matrix of data into an object that can be output in {{Plugin}}.\n\n Args:\n comf_result: Path to a folder with CSV data to be loaded into {{Plugin}}.\n \"\"\"\n # parse the result_info.json into a data collection header\n with open(os.path.join(comf_result, 'results_info.json')) as json_file:\n data_header = Header.from_dict(json.load(json_file))\n a_per = data_header.analysis_period\n continuous = True if a_per.st_hour == 0 and a_per.end_hour == 23 else False\n if not continuous:\n dates = a_per.datetimes\n\n # parse the grids_info.json with the correct order of the grid files\n with open(os.path.join(comf_result, 'grids_info.json')) as json_file:\n grid_list = json.load(json_file)\n\n # loop through the grid CSV files, parse their results, and build data collections\n comf_matrix = []\n for grid in grid_list:\n grid_name = grid['full_id'] if 'full_id' in grid else 'id'\n metadata = {'grid': grid_name}\n grid_file = os.path.join(comf_result, '{}.csv'.format(grid_name))\n data_matrix = csv_to_num_matrix(grid_file)\n grid_data = []\n for i, row in enumerate(data_matrix):\n header = data_header.duplicate()\n header.metadata = metadata.copy()\n header.metadata['sensor_index'] = i\n data = HourlyContinuousCollection(header, row) if continuous else \\\n HourlyDiscontinuousCollection(header, row, dates)\n grid_data.append(data)\n comf_matrix.append(grid_data)\n\n # wrap the maptrix into an object so that it does not slow the {{Plugin}} UI\n comf_mtx = objectify_output(\n '{} Matrix'.format(data_header.data_type.name), comf_matrix)\n return comf_mtx\n\n\nif all_required_inputs(ghenv.Component) and _load:\n # get the folders and that correspond with the requested metric\n _metric_ = _metric_ if _metric_ is not None else 'mrt'\n try:\n sub_path = ENV_CONDS_MAP[_metric_.lower()]\n except KeyError:\n raise ValueError(\n 'Input metric \"{}\" is not recognized. Choose from: {}'.format(\n _metric_, '\\n'.join(ENV_CONDS_MAP.keys()))\n )\n source_folder = os.path.join(_env_conds, sub_path)\n dest_folder = os.path.join(_env_conds, 'final', sub_path)\n\n # if the results have already been processed, then load them up\n if os.path.isdir(dest_folder):\n comf_mtx = load_matrix(dest_folder)\n else: # otherwise, process them into a load-able format\n # make sure the requested metric is valid for the study\n if sub_path != 'mrt':\n assert os.path.isdir(source_folder), \\\n 'Metric \"{}\" does not exist for this comfort study.'.format(sub_path)\n cmds = [folders.python_exe_path, '-m', 'ladybug_comfort', 'map',\n 'restructure-env-conditions', _env_conds, dest_folder, sub_path]\n shell = True if os.name == 'nt' else False\n custom_env = os.environ.copy()\n custom_env['PYTHONHOME'] = ''\n process = subprocess.Popen(\n cmds, stdout=subprocess.PIPE, shell=shell, env=custom_env)\n stdout = process.communicate()\n # load the resulting matrix into {{Plugin}}\n comf_mtx = load_matrix(dest_folder)\n", "category": "HB-Energy", "name": "HB Read Environment Matrix", "description": "Read the detailed environmental conditions of a thermal mapping analysis from\nthe env_conds output by a thermal mapping component.\n_\nEnvironemntal conditions include raw inputs to the thermal comfort model, such as\nair temperature, MRT, longwave MRT, and shortwave MRT delta.\n-" diff --git a/honeybee_grasshopper_energy/json/HB_Read_Thermal_Matrix.json b/honeybee_grasshopper_energy/json/HB_Read_Thermal_Matrix.json index 0e2ff517..6d36f42e 100644 --- a/honeybee_grasshopper_energy/json/HB_Read_Thermal_Matrix.json +++ b/honeybee_grasshopper_energy/json/HB_Read_Thermal_Matrix.json @@ -1,5 +1,5 @@ { - "version": "1.8.0", + "version": "1.8.1", "nickname": "ThermalMtx", "outputs": [ [ @@ -29,7 +29,7 @@ } ], "subcategory": "7 :: Thermal Map", - "code": "\nimport os\nimport json\n\ntry:\n from ladybug.header import Header\n from ladybug.datacollection import HourlyContinuousCollection, \\\n HourlyDiscontinuousCollection\n from ladybug.futil import csv_to_num_matrix\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug:\\n\\t{}'.format(e))\n\ntry:\n from ladybug_{{cad}}.{{plugin}} import all_required_inputs, objectify_output\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug_{{cad}}:\\n\\t{}'.format(e))\n\n\nif all_required_inputs(ghenv.Component) and _load:\n # parse the result_info.json into a data collection header\n with open(os.path.join(_comf_result, 'results_info.json')) as json_file:\n data_header = Header.from_dict(json.load(json_file))\n a_per = data_header.analysis_period\n continuous = True if a_per.st_hour == 0 and a_per.end_hour == 23 else False\n if not continuous:\n dates = a_per.datetimes\n\n # parse the grids_info.json with the correct order of the grid files\n with open(os.path.join(_comf_result, 'grids_info.json')) as json_file:\n grid_list = json.load(json_file)\n\n # loop through the grid CSV files, parse their results, and build data collections\n comf_matrix = []\n for grid in grid_list:\n grid_name = grid['full_id'] if 'full_id' in grid else 'id'\n metadata = {'grid': grid_name}\n grid_file = os.path.join(_comf_result, '{}.csv'.format(grid_name))\n data_matrix = csv_to_num_matrix(grid_file)\n grid_data = []\n for i, row in enumerate(data_matrix):\n header = data_header.duplicate()\n header.metadata = metadata.copy()\n header.metadata['sensor_index'] = i\n data = HourlyContinuousCollection(header, row) if continuous else \\\n HourlyDiscontinuousCollection(header, row, dates)\n grid_data.append(data)\n comf_matrix.append(grid_data)\n\n # wrap the maptrix into an object so that it does not slow the {{Plugin}} UI\n comf_mtx = objectify_output(\n '{} Matrix'.format(data_header.data_type.name), comf_matrix)\n", + "code": "\nimport os\nimport json\nimport subprocess\n\ntry:\n from honeybee.config import folders\nexcept ImportError as e:\n raise ImportError('\\nFailed to import honeybee:\\n\\t{}'.format(e))\n\ntry:\n from ladybug.header import Header\n from ladybug.datacollection import HourlyContinuousCollection, \\\n HourlyDiscontinuousCollection\n from ladybug.futil import csv_to_num_matrix\n from ladybug.datautil import collections_from_csv\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug:\\n\\t{}'.format(e))\n\ntry:\n from ladybug_{{cad}}.{{plugin}} import all_required_inputs, objectify_output\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug_{{cad}}:\\n\\t{}'.format(e))\n\n\nif all_required_inputs(ghenv.Component) and _load:\n # parse the result_info.json into a data collection header\n with open(os.path.join(_comf_result, 'results_info.json')) as json_file:\n data_header = Header.from_dict(json.load(json_file))\n a_per = data_header.analysis_period\n continuous = True if a_per.st_hour == 0 and a_per.end_hour == 23 else False\n if not continuous:\n dates = a_per.datetimes\n\n # parse the grids_info.json with the correct order of the grid files\n with open(os.path.join(_comf_result, 'grids_info.json')) as json_file:\n grid_list = json.load(json_file)\n\n # check file extension\n grid_file = os.path.join(_comf_result, '{}.csv'.format(grid_list[0]['full_id']))\n extension = 'csv'\n if not os.path.exists(grid_file):\n extension = 'npy'\n\n comf_matrix = []\n if extension == 'csv':\n # loop through the grid CSV files, parse their results, and build data collections\n for grid in grid_list:\n grid_name = grid['full_id'] if 'full_id' in grid else 'id'\n metadata = {'grid': grid_name}\n grid_file = os.path.join(_comf_result, '{}.csv'.format(grid_name))\n data_matrix = csv_to_num_matrix(grid_file)\n grid_data = []\n for i, row in enumerate(data_matrix):\n header = data_header.duplicate()\n header.metadata = metadata.copy()\n header.metadata['sensor_index'] = i\n data = HourlyContinuousCollection(header, row) if continuous else \\\n HourlyDiscontinuousCollection(header, row, dates)\n grid_data.append(data)\n comf_matrix.append(grid_data)\n else:\n csv_files = []\n csv_exists = []\n # collect csv files and check if they already exists\n for grid in grid_list:\n grid_name = grid['full_id'] if 'full_id' in grid else 'id'\n grid_file = os.path.join(_comf_result, 'datacollections', '{}.csv'.format(grid_name))\n csv_files.append(grid_file)\n csv_exists.append(os.path.exists(grid_file))\n # run command if csv files do not exist\n if not all(csv_exists):\n cmds = [folders.python_exe_path, '-m', 'honeybee_radiance_postprocess',\n 'data-collection', 'folder-to-datacollections', _comf_result,\n os.path.join(_comf_result, 'results_info.json')]\n use_shell = True if os.name == 'nt' else False\n custom_env = os.environ.copy()\n custom_env['PYTHONHOME'] = ''\n process = subprocess.Popen(\n cmds, cwd=_comf_result, shell=use_shell, env=custom_env,\n stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n stdout = process.communicate() # wait for the process to finish\n for grid_file in csv_files:\n grid_data = collections_from_csv(grid_file)\n comf_matrix.append(grid_data)\n\n # wrap the maptrix into an object so that it does not slow the {{Plugin}} UI\n comf_mtx = objectify_output(\n '{} Matrix'.format(data_header.data_type.name), comf_matrix)\n", "category": "HB-Energy", "name": "HB Read Thermal Matrix", "description": "Read the detailed results of a thermal mapping analysis from a folder of CSV\nfiles output by a thermal mapping component.\n_\nDetailed results include temperature amd thermal condition results. It also\nincludes metrics that give a sense of how hot or cold condition are like\npmv, utci category, or adaptive comfort degrees from neutral temperature.\n-" diff --git a/honeybee_grasshopper_energy/src/HB Read Environment Matrix.py b/honeybee_grasshopper_energy/src/HB Read Environment Matrix.py index f8c7728f..603f3959 100644 --- a/honeybee_grasshopper_energy/src/HB Read Environment Matrix.py +++ b/honeybee_grasshopper_energy/src/HB Read Environment Matrix.py @@ -43,7 +43,7 @@ ghenv.Component.Name = 'HB Read Environment Matrix' ghenv.Component.NickName = 'EnvMtx' -ghenv.Component.Message = '1.8.0' +ghenv.Component.Message = '1.8.1' ghenv.Component.Category = 'HB-Energy' ghenv.Component.SubCategory = '7 :: Thermal Map' ghenv.Component.AdditionalHelpFromDocStrings = '0' @@ -132,46 +132,6 @@ def load_matrix(comf_result): return comf_mtx -def create_result_header(env_conds, sub_path): - """Create a DataCollection Header for a given metric.""" - with open(os.path.join(env_conds, 'results_info.json')) as json_file: - base_head = Header.from_dict(json.load(json_file)) - if sub_path == 'mrt': - return Header(MeanRadiantTemperature(), 'C', base_head.analysis_period) - elif sub_path == 'air_temperature': - return Header(AirTemperature(), 'C', base_head.analysis_period) - elif sub_path == 'longwave_mrt': - return Header(RadiantTemperature(), 'C', base_head.analysis_period) - elif sub_path == 'shortwave_mrt': - return Header(RadiantTemperatureDelta(), 'dC', base_head.analysis_period) - elif sub_path == 'rel_humidity': - return Header(RelativeHumidity(), '%', base_head.analysis_period) - - -def sum_matrices(mtxs_1, mtxs_2, dest_dir): - """Sum together matrices of two folders.""" - if not os.path.isdir(dest_dir): - os.makedirs(dest_dir) - for mtx_file in os.listdir(mtxs_1): - if mtx_file.endswith('.csv'): - mtx_file1 = os.path.join(mtxs_1, mtx_file) - mtx_file2 = os.path.join(mtxs_2, mtx_file) - matrix_1 = csv_to_num_matrix(mtx_file1) - matrix_2 = csv_to_num_matrix(mtx_file2) - data = [[d1 + d2 for d1, d2 in zip(r1, r2)] - for r1, r2 in zip(matrix_1, matrix_2)] - csv_path = os.path.join(dest_dir, mtx_file) - with open(csv_path, 'w') as csv_file: - for dat in data: - str_data = (str(v) for v in dat) - csv_file.write(','.join(str_data) + '\n') - elif mtx_file == 'grids_info.json': - shutil.copyfile( - os.path.join(mtxs_1, mtx_file), - os.path.join(dest_dir, mtx_file) - ) - - if all_required_inputs(ghenv.Component) and _load: # get the folders and that correspond with the requested metric _metric_ = _metric_ if _metric_ is not None else 'mrt' @@ -190,42 +150,16 @@ def sum_matrices(mtxs_1, mtxs_2, dest_dir): comf_mtx = load_matrix(dest_folder) else: # otherwise, process them into a load-able format # make sure the requested metric is valid for the study - if sub_path == 'mrt': - source_folders = [os.path.join(_env_conds, 'longwave_mrt'), - os.path.join(_env_conds, 'shortwave_mrt')] - dest_folders = [os.path.join(_env_conds, 'final', 'longwave_mrt'), - os.path.join(_env_conds, 'final', 'shortwave_mrt')] - else: + if sub_path != 'mrt': assert os.path.isdir(source_folder), \ - 'Metric "{}" does not exist for this comfort study.'.format(sub_path) - source_folders, dest_folders = [source_folder], [dest_folder] - # restructure the results to align with the sensor grids - dist_info = os.path.join(_env_conds, '_redist_info.json') - for src_f, dst_f in zip(source_folders, dest_folders): - if not os.path.isdir(dst_f): - os.makedirs(dst_f) - cmds = [folders.python_exe_path, '-m', 'honeybee_radiance', 'grid', - 'merge-folder', src_f, dst_f, 'csv', - '--dist-info', dist_info] - shell = True if os.name == 'nt' else False - custom_env = os.environ.copy() - custom_env['PYTHONHOME'] = '' - process = subprocess.Popen( - cmds, stdout=subprocess.PIPE, shell=shell, env=custom_env) - stdout = process.communicate() - grid_info_src = os.path.join(_env_conds, 'grids_info.json') - grid_info_dst = os.path.join(dst_f, 'grids_info.json') - shutil.copyfile(grid_info_src, grid_info_dst) - data_header = create_result_header(_env_conds, os.path.split(dst_f)[-1]) - result_info_path = os.path.join(dst_f, 'results_info.json') - with open(result_info_path, 'w') as fp: - json.dump(data_header.to_dict(), fp, indent=4) - # if MRT was requested, sum together the longwave and shortwave - if sub_path == 'mrt': - sum_matrices(dest_folders[0], dest_folders[1], dest_folder) - data_header = create_result_header(_env_conds, sub_path) - result_info_path = os.path.join(dest_folder, 'results_info.json') - with open(result_info_path, 'w') as fp: - json.dump(data_header.to_dict(), fp, indent=4) + 'Metric "{}" does not exist for this comfort study.'.format(sub_path) + cmds = [folders.python_exe_path, '-m', 'ladybug_comfort', 'map', + 'restructure-env-conditions', _env_conds, dest_folder, sub_path] + shell = True if os.name == 'nt' else False + custom_env = os.environ.copy() + custom_env['PYTHONHOME'] = '' + process = subprocess.Popen( + cmds, stdout=subprocess.PIPE, shell=shell, env=custom_env) + stdout = process.communicate() # load the resulting matrix into Grasshopper comf_mtx = load_matrix(dest_folder) diff --git a/honeybee_grasshopper_energy/src/HB Read Thermal Matrix.py b/honeybee_grasshopper_energy/src/HB Read Thermal Matrix.py index 59fd2419..cc98069f 100644 --- a/honeybee_grasshopper_energy/src/HB Read Thermal Matrix.py +++ b/honeybee_grasshopper_energy/src/HB Read Thermal Matrix.py @@ -37,19 +37,26 @@ ghenv.Component.Name = 'HB Read Thermal Matrix' ghenv.Component.NickName = 'ThermalMtx' -ghenv.Component.Message = '1.8.0' +ghenv.Component.Message = '1.8.1' ghenv.Component.Category = 'HB-Energy' ghenv.Component.SubCategory = '7 :: Thermal Map' ghenv.Component.AdditionalHelpFromDocStrings = '2' import os import json +import subprocess + +try: + from honeybee.config import folders +except ImportError as e: + raise ImportError('\nFailed to import honeybee:\n\t{}'.format(e)) try: from ladybug.header import Header from ladybug.datacollection import HourlyContinuousCollection, \ HourlyDiscontinuousCollection from ladybug.futil import csv_to_num_matrix + from ladybug.datautil import collections_from_csv except ImportError as e: raise ImportError('\nFailed to import ladybug:\n\t{}'.format(e)) @@ -72,22 +79,53 @@ with open(os.path.join(_comf_result, 'grids_info.json')) as json_file: grid_list = json.load(json_file) - # loop through the grid CSV files, parse their results, and build data collections + # check file extension + grid_file = os.path.join(_comf_result, '{}.csv'.format(grid_list[0]['full_id'])) + extension = 'csv' + if not os.path.exists(grid_file): + extension = 'npy' + comf_matrix = [] - for grid in grid_list: - grid_name = grid['full_id'] if 'full_id' in grid else 'id' - metadata = {'grid': grid_name} - grid_file = os.path.join(_comf_result, '{}.csv'.format(grid_name)) - data_matrix = csv_to_num_matrix(grid_file) - grid_data = [] - for i, row in enumerate(data_matrix): - header = data_header.duplicate() - header.metadata = metadata.copy() - header.metadata['sensor_index'] = i - data = HourlyContinuousCollection(header, row) if continuous else \ - HourlyDiscontinuousCollection(header, row, dates) - grid_data.append(data) - comf_matrix.append(grid_data) + if extension == 'csv': + # loop through the grid CSV files, parse their results, and build data collections + for grid in grid_list: + grid_name = grid['full_id'] if 'full_id' in grid else 'id' + metadata = {'grid': grid_name} + grid_file = os.path.join(_comf_result, '{}.csv'.format(grid_name)) + data_matrix = csv_to_num_matrix(grid_file) + grid_data = [] + for i, row in enumerate(data_matrix): + header = data_header.duplicate() + header.metadata = metadata.copy() + header.metadata['sensor_index'] = i + data = HourlyContinuousCollection(header, row) if continuous else \ + HourlyDiscontinuousCollection(header, row, dates) + grid_data.append(data) + comf_matrix.append(grid_data) + else: + csv_files = [] + csv_exists = [] + # collect csv files and check if they already exists + for grid in grid_list: + grid_name = grid['full_id'] if 'full_id' in grid else 'id' + grid_file = os.path.join(_comf_result, 'datacollections', '{}.csv'.format(grid_name)) + csv_files.append(grid_file) + csv_exists.append(os.path.exists(grid_file)) + # run command if csv files do not exist + if not all(csv_exists): + cmds = [folders.python_exe_path, '-m', 'honeybee_radiance_postprocess', + 'data-collection', 'folder-to-datacollections', _comf_result, + os.path.join(_comf_result, 'results_info.json')] + use_shell = True if os.name == 'nt' else False + custom_env = os.environ.copy() + custom_env['PYTHONHOME'] = '' + process = subprocess.Popen( + cmds, cwd=_comf_result, shell=use_shell, env=custom_env, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout = process.communicate() # wait for the process to finish + for grid_file in csv_files: + grid_data = collections_from_csv(grid_file) + comf_matrix.append(grid_data) # wrap the maptrix into an object so that it does not slow the Grasshopper UI comf_mtx = objectify_output( diff --git a/honeybee_grasshopper_energy/user_objects/HB Read Environment Matrix.ghuser b/honeybee_grasshopper_energy/user_objects/HB Read Environment Matrix.ghuser index fdabb918f24a16e006a08552cc9d0a51b77b5706..89b182ca83a2a3650f11fa0836e9c8ae5ad35462 100644 GIT binary patch literal 6596 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 CE$?dp literal 7332 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 K2YfnUkN*SEbZI^S diff --git a/honeybee_grasshopper_energy/user_objects/HB Read Thermal Matrix.ghuser b/honeybee_grasshopper_energy/user_objects/HB Read Thermal Matrix.ghuser index c2270069beb31f56f4af4e8e306bb243e850e4a5..d1f9745be73a707f631c7cc46fe91d9b47a85c1f 100644 GIT binary patch literal 6460 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 W*m|Be|Mw3Ife=7c^glk~K>rIu8?wUy literal 5991 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 V(Dlv#{TWX#4v>=ikFR~ie*uyE;U@q9