From afd5af66d30ee57e9f5efb7ff87ef65d5adaec27 Mon Sep 17 00:00:00 2001 From: Mikkel Pedersen <34610298+mikkelkp@users.noreply.github.com> Date: Fri, 26 Jul 2024 16:24:45 +0200 Subject: [PATCH] fix(helper): Add function to find smallest dtype * fix(helper): Add function to find smallest dtype * fix(map): Add binary option in CLI command * refactor(mtx): Use load matrix function --- ladybug_comfort/cli/_helper.py | 97 ++++++++++++++++++++++++++++++++-- ladybug_comfort/cli/map.py | 24 ++++++--- ladybug_comfort/cli/mtx.py | 26 +++------ 3 files changed, 116 insertions(+), 31 deletions(-) diff --git a/ladybug_comfort/cli/_helper.py b/ladybug_comfort/cli/_helper.py index 92506c29..4c12e29e 100644 --- a/ladybug_comfort/cli/_helper.py +++ b/ladybug_comfort/cli/_helper.py @@ -178,9 +178,100 @@ def thermal_map_csv(folder, temperature, condition, condition_intensity, _data_to_csv(condition_intensity, result_file_dict['condition_intensity']) else: with open(result_file_dict['temperature'], 'wb') as fp: - np.save(fp, temperature) + np.save(fp, set_smallest_dtype(np.array(temperature))) with open(result_file_dict['condition'], 'wb') as fp: - np.save(fp, np.array(condition)) + np.save(fp, set_smallest_dtype(np.array(condition))) with open(result_file_dict['condition_intensity'], 'wb') as fp: - np.save(fp, np.array(condition_intensity)) + np.save(fp, set_smallest_dtype(np.array(condition_intensity))) return result_file_dict + + +def smallest_integer_dtype(array: np.ndarray): + """Return the smallest possible integer dtype. + + Args: + array: NumPy array. + + Returns: + A NumPy integer dtype. + """ + if np.all(array >= np.iinfo(np.int8).min) and \ + np.all(array <= np.iinfo(np.int8).max): + return np.int8 + elif np.all(array >= np.iinfo(np.int16).min) and \ + np.all(array <= np.iinfo(np.int16).max): + return np.int16 + elif np.all(array >= np.iinfo(np.int32).min) and \ + np.all(array <= np.iinfo(np.int32).max): + return np.int32 + elif np.all(array >= np.iinfo(np.int64).min) and \ + np.all(array <= np.iinfo(np.int64).max): + return np.int64 + + +def smallest_float_dtype(array: np.ndarray, rtol: float = 1e-5, atol: float = 1e-5): + """Return the smallest possible float dtype. + + The allclose function is used to check if a certain floating-point precision + can be used without losing accuracy. + + Args: + array: NumPy array. + rtol: The relative tolerance parameter for `np.allclose`. The default + is 1e-5. + atol: The absolute tolerance parameter for `np.allclose`. The default + is 1e-5. + + Returns: + A NumPy floating dtype. + """ + if np.all((array >= np.finfo(np.float16).min) & \ + (array <= np.finfo(np.float16).max)): + if np.allclose(array, array.astype(np.float16), rtol=rtol, atol=atol): + return np.float16 + if np.all((array >= np.finfo(np.float32).min) & \ + (array <= np.finfo(np.float32).max)): + if np.allclose(array, array.astype(np.float32), rtol=rtol, atol=atol): + return np.float32 + if np.all((array >= np.finfo(np.float64).min) & \ + (array <= np.finfo(np.float64).max)): + if np.allclose(array, array.astype(np.float64), rtol=rtol, atol=atol): + return np.float64 + + +def smallest_dtype(array: np.ndarray, rtol: float = 1e-5, atol: float = 1e-5): + """Return the smallest possible dtype. + + Args: + array: NumPy array. + rtol: The relative tolerance parameter for `np.allclose`. The default + is 1e-5. This is also used if the dtype of the array is np.floating. + atol: The absolute tolerance parameter for `np.allclose`. The default + is 1e-5. This is also used if the dtype of the array is np.floating. + + Returns: + A NumPy dtype. + """ + if np.issubdtype(array.dtype, np.integer): + return smallest_integer_dtype(array) + elif np.issubdtype(array.dtype, np.floating): + return smallest_float_dtype(array, rtol=rtol, atol=atol) + else: + raise TypeError(f'Expected integer or floating dtype. Got {array.dtype}') + + +def set_smallest_dtype(array: np.ndarray, rtol: float = 1e-5, atol: float = 1e-5): + """Return a NumPy array with the smallest possible dtype. + + Args: + array: NumPy array. + rtol: The relative tolerance parameter for `np.allclose`. The default + is 1e-5. This is also used if the dtype of the array is np.floating. + atol: The absolute tolerance parameter for `np.allclose`. The default + is 1e-5. This is also used if the dtype of the array is np.floating. + + Returns: + A new NumPy array with a smaller dtype. + """ + dtype = smallest_dtype(array, rtol=rtol, atol=atol) + return array.astype(dtype) diff --git a/ladybug_comfort/cli/map.py b/ladybug_comfort/cli/map.py index 7ad579c0..b3141622 100644 --- a/ladybug_comfort/cli/map.py +++ b/ladybug_comfort/cli/map.py @@ -34,7 +34,7 @@ from ._helper import load_values, load_analysis_period_str, \ load_pmv_par_str, load_adaptive_par_str, load_utci_par_str, \ - load_solarcal_par_str, thermal_map_csv, _data_to_ill + load_solarcal_par_str, thermal_map_csv, _data_to_ill, set_smallest_dtype _logger = logging.getLogger(__name__) @@ -590,7 +590,7 @@ def shortwave_mrt( output_file.write('') else: with open(output_file.name, 'wb') as fp: - np.save(fp, d_mrt_temps) + np.save(fp, set_smallest_dtype(np.array(d_mrt_temps))) except Exception as e: _logger.exception('Failed to run Shortwave MRT Delta map.\n{}'.format(e)) sys.exit(1) @@ -651,7 +651,7 @@ def longwave_mrt(result_sql, view_factors, modifiers, enclosure_info, epw_file, output_file.write('\n') else: with open(output_file.name, 'wb') as fp: - np.save(fp, mrt_temps) + np.save(fp, set_smallest_dtype(np.array(mrt_temps))) except Exception as e: _logger.exception('Failed to run Longwave MRT map.\n{}'.format(e)) sys.exit(1) @@ -677,8 +677,12 @@ def longwave_mrt(result_sql, view_factors, modifiers, enclosure_info, epw_file, @click.option('--output-file', '-f', help='Optional file to output the CSV matrix ' 'of values. By default this will be printed out to stdout', type=click.File('w'), default='-', show_default=True) -def air_temperature(result_sql, enclosure_info, epw_file, - run_period, air_temperature, output_file): +@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 air_temperature(result_sql, enclosure_info, epw_file, run_period, + air_temperature, output_file, plain_text): """Get CSV files with maps of air temperatures or humidity from EnergyPlus results. \b @@ -701,9 +705,13 @@ def air_temperature(result_sql, enclosure_info, epw_file, air_data = air_map(enclosure_info, result_sql, epw_file, run_period, humidity) # write out the final results to CSV files - for air_d in air_data: - output_file.write(','.join(str(v) for v in air_d)) - output_file.write('\n') + if plain_text: + for air_d in air_data: + output_file.write(','.join(str(v) for v in air_d)) + output_file.write('\n') + else: + with open(output_file.name, 'wb') as fp: + np.save(fp, set_smallest_dtype(np.array(air_data))) except Exception as e: _logger.exception('Failed to run Air Temperature map.\n{}'.format(e)) sys.exit(1) diff --git a/ladybug_comfort/cli/mtx.py b/ladybug_comfort/cli/mtx.py index 79e6ca19..1534dff4 100644 --- a/ladybug_comfort/cli/mtx.py +++ b/ladybug_comfort/cli/mtx.py @@ -4,7 +4,6 @@ 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, \ @@ -12,6 +11,7 @@ cooling_effect_ashrae55, cooling_effect_en16798, cooling_effect_en15251 from ladybug_comfort.utci import universal_thermal_climate_index +from ..map._helper import load_matrix from ._helper import load_value_list, thermal_map_csv, csv_to_num_matrix, \ load_pmv_par_str, load_adaptive_par_str, load_utci_par_str @@ -360,29 +360,15 @@ def utci_mtx( """ try: # load up the matrices of values - air_temp = np.genfromtxt(temperature_mtx, delimiter=',').tolist() - rel_h = np.genfromtxt(rel_humidity_mtx, delimiter=',').tolist() + air_temp = load_matrix(temperature_mtx).tolist() + rel_h = load_matrix(rel_humidity_mtx).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() + rad_temp = load_matrix(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: - 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() + d_rad_temp = load_matrix(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])