diff --git a/ladybug_comfort/cli/_helper.py b/ladybug_comfort/cli/_helper.py index c12ce462..92506c29 100644 --- a/ladybug_comfort/cli/_helper.py +++ b/ladybug_comfort/cli/_helper.py @@ -4,6 +4,7 @@ """ import os import json +import numpy as np from ladybug.analysisperiod import AnalysisPeriod from ladybug.header import Header @@ -162,7 +163,8 @@ def _data_to_ill(data, ill_path): ill_file.write(' '.join(str_data) + '\n') -def thermal_map_csv(folder, temperature, condition, condition_intensity): +def thermal_map_csv(folder, temperature, condition, condition_intensity, + plain_text=True): """Write out the thermal mapping CSV files associated with every comfort map.""" preparedir(folder, remove_content=False) result_file_dict = { @@ -170,7 +172,15 @@ def thermal_map_csv(folder, temperature, condition, condition_intensity): 'condition': os.path.join(folder, 'condition.csv'), 'condition_intensity': os.path.join(folder, 'condition_intensity.csv') } - _data_to_csv(temperature, result_file_dict['temperature']) - _data_to_csv(condition, result_file_dict['condition']) - _data_to_csv(condition_intensity, result_file_dict['condition_intensity']) + if plain_text: + _data_to_csv(temperature, result_file_dict['temperature']) + _data_to_csv(condition, result_file_dict['condition']) + _data_to_csv(condition_intensity, result_file_dict['condition_intensity']) + else: + with open(result_file_dict['temperature'], 'wb') as fp: + np.save(fp, temperature) + with open(result_file_dict['condition'], 'wb') as fp: + np.save(fp, np.array(condition)) + with open(result_file_dict['condition_intensity'], 'wb') as fp: + np.save(fp, np.array(condition_intensity)) return result_file_dict diff --git a/ladybug_comfort/cli/map.py b/ladybug_comfort/cli/map.py index a51b097c..7ad579c0 100644 --- a/ladybug_comfort/cli/map.py +++ b/ladybug_comfort/cli/map.py @@ -5,6 +5,7 @@ import json import os import shutil +import numpy as np from ladybug.epw import EPW from ladybug.legend import LegendParameters @@ -337,9 +338,14 @@ def adaptive(result_sql, enclosure_info, epw_file, @click.option('--log-file', '-log', help='Optional log file to output the paths to the ' 'generated CSV files. By default this will be printed out to stdout', type=click.File('w'), default='-', show_default=True) +@click.option('--plain-text/--binary', ' /-b', help='Flag to note whether the ' + 'output should be formatted as a plain text CSV or whether it ' + 'should be formatted as a binary numpy array.', + default=True, show_default=True) def utci(result_sql, enclosure_info, epw_file, total_irradiance, direct_irradiance, ref_irradiance, sun_up_hours, - wind_speed, run_period, comfort_par, solarcal_par, folder, log_file): + wind_speed, run_period, comfort_par, solarcal_par, folder, log_file, + plain_text): """Get CSV files with maps of UTCI comfort from EnergyPlus and Radiance results. \b @@ -386,7 +392,7 @@ def utci(result_sql, enclosure_info, epw_file, if folder is None: folder = os.path.join(os.path.dirname(result_sql), 'thermal_map') result_file_dict = thermal_map_csv( - folder, temperature, condition, condition_intensity) + folder, temperature, condition, condition_intensity, plain_text) log_file.write(json.dumps(result_file_dict)) except Exception as e: _logger.exception('Failed to run UTCI model comfort map.\n{}'.format(e)) @@ -517,10 +523,14 @@ def irradiance_contrib( @click.option('--output-file', '-f', help='Optional file to output the CSV matrix ' 'of MRT deltas. By default this will be printed out to stdout', type=click.File('w'), default='-', show_default=True) +@click.option('--plain-text/--binary', ' /-b', help='Flag to note whether the ' + 'output should be formatted as a plain text CSV or whether it ' + 'should be formatted as a binary numpy array.', + default=True, show_default=True) def shortwave_mrt( epw_file, indirect_irradiance, direct_irradiance, ref_irradiance, sun_up_hours, contributions, transmittance_contribs, trans_schedule_json, - run_period, solarcal_par, is_indirect, output_file): + run_period, solarcal_par, is_indirect, output_file, plain_text): """Get CSV files with maps of shortwave MRT Deltas from Radiance results. \b @@ -568,12 +578,19 @@ def shortwave_mrt( solarcal_par=solarcal_par, indirect_is_total=is_total) # write out the final results to CSV files - if len(d_mrt_temps) == 0: # no sun-up hours; just create a blank file - output_file.write('') + if plain_text: + if len(d_mrt_temps) == 0: + output_file.write('') + else: + for mrt_d in d_mrt_temps: + output_file.write(','.join(str(v) for v in mrt_d)) + output_file.write('\n') else: - for mrt_d in d_mrt_temps: - output_file.write(','.join(str(v) for v in mrt_d)) - output_file.write('\n') + if len(d_mrt_temps) == 0: # no sun-up hours; just create a blank file + output_file.write('') + else: + with open(output_file.name, 'wb') as fp: + np.save(fp, d_mrt_temps) except Exception as e: _logger.exception('Failed to run Shortwave MRT Delta map.\n{}'.format(e)) sys.exit(1) @@ -598,8 +615,12 @@ def shortwave_mrt( @click.option('--output-file', '-f', help='Optional file to output the CSV matrix ' 'of longwave MRT. By default this will be printed out to stdout', type=click.File('w'), default='-', show_default=True) +@click.option('--plain-text/--binary', ' /-b', help='Flag to note whether the ' + 'output should be formatted as a plain text CSV or whether it ' + 'should be formatted as a binary numpy array.', + default=True, show_default=True) def longwave_mrt(result_sql, view_factors, modifiers, enclosure_info, epw_file, - run_period, output_file): + run_period, output_file, plain_text): """Get CSV files with maps of longwave MRT from Radiance and EnergyPlus results. \b @@ -624,9 +645,13 @@ def longwave_mrt(result_sql, view_factors, modifiers, enclosure_info, epw_file, enclosure_info, modifiers, result_sql, view_factors, epw_file, run_period) # write out the final results to CSV files - for mrt_d in mrt_temps: - output_file.write(','.join(str(v) for v in mrt_d)) - output_file.write('\n') + if plain_text: + for mrt_d in mrt_temps: + output_file.write(','.join(str(v) for v in mrt_d)) + output_file.write('\n') + else: + with open(output_file.name, 'wb') as fp: + np.save(fp, mrt_temps) except Exception as e: _logger.exception('Failed to run Longwave MRT map.\n{}'.format(e)) sys.exit(1) diff --git a/ladybug_comfort/cli/mtx.py b/ladybug_comfort/cli/mtx.py index 8f0a8b07..79e6ca19 100644 --- a/ladybug_comfort/cli/mtx.py +++ b/ladybug_comfort/cli/mtx.py @@ -4,6 +4,7 @@ import logging import json import os +import numpy as np from ladybug_comfort.pmv import predicted_mean_vote, predicted_mean_vote_no_set from ladybug_comfort.adaptive import adaptive_comfort_ashrae55, \ @@ -339,9 +340,14 @@ def adaptive_mtx( @click.option('--log-file', '-log', help='Optional log file to output the paths to the ' 'generated CSV files. By default this will be printed out to stdout', type=click.File('w'), default='-', show_default=True) +@click.option('--plain-text/--binary', ' /-b', help='Flag to note whether the ' + 'output should be formatted as a plain text CSV or whether it ' + 'should be formatted as a binary numpy array.', + default=True, show_default=True) def utci_mtx( temperature_mtx, rel_humidity_mtx, rad_temperature_mtx, rad_delta_mtx, - air_speed_mtx, wind_speed_json, wind_speed, comfort_par, folder, log_file + air_speed_mtx, wind_speed_json, wind_speed, comfort_par, folder, log_file, + plain_text ): """Get CSV files with matrices of UTCI comfort from matrices of UTCI inputs. @@ -354,12 +360,29 @@ def utci_mtx( """ try: # load up the matrices of values - air_temp = csv_to_num_matrix(temperature_mtx) - rel_h = csv_to_num_matrix(rel_humidity_mtx) - rad_temp = csv_to_num_matrix(rad_temperature_mtx) \ - if rad_temperature_mtx is not None else air_temp + air_temp = np.genfromtxt(temperature_mtx, delimiter=',').tolist() + rel_h = np.genfromtxt(rel_humidity_mtx, delimiter=',').tolist() + if rad_temperature_mtx is not None: + with open(rad_temperature_mtx, 'rb') as inf: + first_char = inf.read(1) + second_char = inf.read(1) + is_text = True if first_char.isdigit() or second_char.isdigit() else False + if is_text: + rad_temp = np.genfromtxt( + rad_temperature_mtx, delimiter=',', encoding='utf-8').tolist() + else: + rad_temp = np.load(rad_temperature_mtx).tolist() + else: + rad_temp = air_temp if rad_delta_mtx is not None and not os.path.getsize(rad_delta_mtx) == 0: - d_rad_temp = csv_to_num_matrix(rad_delta_mtx) + with open(rad_delta_mtx, 'rb') as inf: + first_char = inf.read(1) + second_char = inf.read(1) + is_text = True if first_char.isdigit() or second_char.isdigit() else False + if is_text: + d_rad_temp = np.genfromtxt(rad_delta_mtx, delimiter=',', encoding='utf-8') + else: + d_rad_temp = np.load(rad_delta_mtx).tolist() rad_temp = tuple(tuple(t + dt for t, dt in zip(t_pt, dt_pt)) for t_pt, dt_pt in zip(rad_temp, d_rad_temp)) mtx_len = len(air_temp[0]) @@ -399,7 +422,8 @@ def utci_mtx( # write out the final results to CSV files if folder is None: folder = os.path.join(os.path.dirname(temperature_mtx), 'thermal_mtx') - result_file_dict = thermal_map_csv(folder, temper, cond, cond_intensity) + result_file_dict = thermal_map_csv(folder, temper, cond, cond_intensity, + plain_text=plain_text) log_file.write(json.dumps(result_file_dict)) except Exception as e: _logger.exception('Failed to run UTCI matrix.\n{}'.format(e)) diff --git a/ladybug_comfort/map/_helper.py b/ladybug_comfort/map/_helper.py new file mode 100644 index 00000000..4ec5884c --- /dev/null +++ b/ladybug_comfort/map/_helper.py @@ -0,0 +1,97 @@ +"""A collection of helper functions for the map sub-package.""" +import numpy as np + + +def binary_mtx_dimension(filepath): + """Return binary Radiance matrix dimensions if exist. + + This function returns NROWS, NCOLS, NCOMP and number of header lines including the + white line after last header line. + + Args: + filepath: Full path to Radiance file. + + Returns: + nrows, ncols, ncomp, line_count + """ + try: + inf = open(filepath, 'rb', encoding='utf-8') + except Exception: + inf = open(filepath, 'rb') + try: + first_line = next(inf).rstrip().decode('utf-8') + if first_line[:10] != '#?RADIANCE': + error_message = ( + f'File with Radiance header must start with #?RADIANCE not ' + f'{first_line}.' + ) + raise ValueError(error_message) + + header_lines = [first_line] + nrows = ncols = ncomp = None + for line in inf: + line = line.rstrip().decode('utf-8') + header_lines.append(line) + if line[:6] == 'NROWS=': + nrows = int(line.split('=')[-1]) + if line[:6] == 'NCOLS=': + ncols = int(line.split('=')[-1]) + if line[:6] == 'NCOMP=': + ncomp = int(line.split('=')[-1]) + if line[:7] == 'FORMAT=': + break + + if not nrows or not ncols: + error_message = ( + f'NROWS or NCOLS was not found in the Radiance header. NROWS ' + f'is {nrows} and NCOLS is {ncols}. The header must have both ' + f'elements.' + ) + raise ValueError(error_message) + return nrows, ncols, ncomp, len(header_lines) + 1 + finally: + inf.close() + + +def binary_to_array(binary_file, nrows=None, ncols=None, ncomp=None, line_count=0): + """Read a Radiance binary file as a NumPy array. + + Args: + binary_file: Path to binary Radiance file. + nrows: Number of rows in the Radiance file. + ncols: Number of columns in the Radiance file. + ncomp: Number of components of each element in the Radiance file. + line_count: Number of lines to skip in the input file. Usually used to + skip the header. + + Returns: + A NumPy array. + """ + with open(binary_file, 'rb') as reader: + if (nrows or ncols or ncomp) is None: + # get nrows, ncols and header line count + nrows, ncols, ncomp, line_count = binary_mtx_dimension(binary_file) + # skip first n lines from reader + for i in range(line_count): + reader.readline() + + array = np.fromfile(reader, dtype=np.float32) + if ncomp != 1: + array = array.reshape(nrows, ncols, ncomp) + else: + array = array.reshape(nrows, ncols) + + return array + + +def load_matrix(matrix_file, delimiter=','): + with open(matrix_file, 'rb') as inf: + first_char = inf.read(1) + second_char = inf.read(1) + is_text = True if first_char.isdigit() or second_char.isdigit() else False + if is_text: + array = np.genfromtxt(matrix_file, delimiter=delimiter, encoding='utf-8') + else: + array = np.load(matrix_file) + + return array diff --git a/ladybug_comfort/map/mrt.py b/ladybug_comfort/map/mrt.py index a318b9bd..0662fb1d 100644 --- a/ladybug_comfort/map/mrt.py +++ b/ladybug_comfort/map/mrt.py @@ -4,6 +4,7 @@ import os import json +import numpy as np from ladybug.epw import EPW from ladybug.sql import SQLiteResult @@ -16,6 +17,7 @@ from ..solarcal import sharp_from_solar_and_body_azimuth from ..collection.solarcal import _HorizontalSolarCalMap, _HorizontalRefSolarCalMap from ..parameter.solarcal import SolarCalParameter +from ._helper import binary_to_array, load_matrix def shortwave_mrt_map( @@ -243,19 +245,42 @@ def longwave_mrt_map( out_data = tuple(zip(*out_data)) # load the view factors and perform the matrix multiplication with temperature - with open(view_factors) as csv_data_file: - vf_data = tuple( - tuple(float(val) for val in row.split(',')) for row in csv_data_file) - mrt_data = [] - for sen_enc, view_facs in zip(enclosure_dict['sensor_indices'], vf_data): - if sen_enc == -1: # outdoor sensor - temp_data = out_data - else: # indoor sensor - temp_data = in_data[sen_enc] - sensor_vals = [] - for t_step in temp_data: - sensor_vals.append(sum(vf * t for vf, t in zip(view_facs, t_step))) - mrt_data.append(sensor_vals) + vf_data = load_matrix(view_factors) + + sensor_indices = np.array(enclosure_dict['sensor_indices']) + + # create masks for outdoor and indoor sensors + outdoor_mask = sensor_indices == -1 + indoor_mask = ~outdoor_mask + + # process outdoor sensors if any + if np.any(outdoor_mask): + out_temp_data = np.array(out_data) + out_view_facs = vf_data[outdoor_mask] + outdoor_results = np.dot(out_temp_data, out_view_facs.T).T + num_time_steps = outdoor_results.shape[1] + + # process indoor sensors if any + if np.any(indoor_mask): + indoor_indices = sensor_indices[indoor_mask] + in_temp_data = np.array([in_data[i] for i in indoor_indices]) + in_view_facs = vf_data[indoor_mask] + indoor_results = np.einsum('ijk,ik->ij', in_temp_data, in_view_facs) + num_time_steps = indoor_results.shape[1] + + # initialize mrt_data with the correct shape + mrt_data = np.empty((len(sensor_indices), num_time_steps)) + + # place results in the correct order + if np.any(outdoor_mask): + mrt_data[outdoor_mask, :] = outdoor_results + + if np.any(indoor_mask): + mrt_data[indoor_mask, :] = indoor_results + + # convert to list format + mrt_data = mrt_data.tolist() + return mrt_data @@ -274,9 +299,20 @@ def _ill_file_to_data(ill_file, sun_indices, timestep=1, leap_yr=False): a_period = AnalysisPeriod(timestep=timestep, is_leap_year=leap_yr) header = Header(Irradiance(), 'W/m2', a_period) irr_data = [] - with open(ill_file) as results: - for pt_res in results: - ill_values = [float(v) for v in pt_res.split()] + with open(ill_file, 'rb') as inf: + first_char = inf.read(1) + second_char = inf.read(1) + is_text = True if first_char.isdigit() or second_char.isdigit() else False + if is_text: + with open(ill_file) as results: + for pt_res in results: + ill_values = [float(v) for v in pt_res.split()] + pt_irr_data = _ill_values_to_data( + ill_values, sun_indices, header, timestep, leap_yr) + irr_data.append(pt_irr_data) + else: + results = binary_to_array(ill_file) + for ill_values in results: pt_irr_data = _ill_values_to_data( ill_values, sun_indices, header, timestep, leap_yr) irr_data.append(pt_irr_data) diff --git a/ladybug_comfort/map/tcp.py b/ladybug_comfort/map/tcp.py index cf6999f4..84968433 100644 --- a/ladybug_comfort/map/tcp.py +++ b/ladybug_comfort/map/tcp.py @@ -3,6 +3,8 @@ from __future__ import division import json +from ._helper import load_matrix + def tcp_model_schedules( condition_csv, enclosure_info_json, occ_schedule_json, outdoor_occ_csv=None): @@ -30,12 +32,10 @@ def tcp_model_schedules( * csp_list - List of Cold Sensation Percent (CSP) values for each sensor. """ # parse all of the input files + cond_mtx = load_matrix(condition_csv) + with open(enclosure_info_json) as json_file: enclosure_dict = json.load(json_file) - cond_mtx = [] - with open(condition_csv) as csv_data_file: - for row in csv_data_file: - cond_mtx.append([int(val) for val in row.split(',')]) with open(occ_schedule_json) as json_file: occ_dict = json.load(json_file) @@ -116,10 +116,7 @@ def tcp_total(condition_csv, schedule=None): * csp_list - List of Cold Sensation Percent (CSP) values for each sensor. """ # parse the csv of results - cond_mtx = [] - with open(condition_csv) as csv_data_file: - for row in csv_data_file: - cond_mtx.append([int(val) for val in row.split(',')]) + cond_mtx = load_matrix(condition_csv) # create the occupancy schedule time_count = len(cond_mtx[0])