Skip to content

Commit

Permalink
fix(map): Replace loop with NumPy post-processing
Browse files Browse the repository at this point in the history
* fix(mrt): Load view factors as NumPy file

* fix(mrt): Read binary to array for shortwave

* fix(map): Write shortwave and longwave MRT as NumPy

* fix(tcp): Load NumPy files in TCP calculation

* fix(mtx): Load NumPy files in UTCI matrix

* fix(helper): Save files as NumPy files

* fix(map): Replace loop with NumPy post-processing

* fix(map): Add helper function to read Radiance files

* fix(map): Add binary output option to CLI commands

* fix(map): Use load matrix helper function

* fix(map): Change plain text to default to True

* style(pep8): Make a few tweaks for PEP8 and Python 2 compatibility

---------

Co-authored-by: Chris Mackey <chris@ladybug.tools>
  • Loading branch information
mikkelkp and chriswmackey authored Jul 25, 2024
1 parent 023ce3f commit 0b88748
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 47 deletions.
18 changes: 14 additions & 4 deletions ladybug_comfort/cli/_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""
import os
import json
import numpy as np

from ladybug.analysisperiod import AnalysisPeriod
from ladybug.header import Header
Expand Down Expand Up @@ -162,15 +163,24 @@ 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 = {
'temperature': os.path.join(folder, 'temperature.csv'),
'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
49 changes: 37 additions & 12 deletions ladybug_comfort/cli/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import json
import os
import shutil
import numpy as np

from ladybug.epw import EPW
from ladybug.legend import LegendParameters
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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)
Expand Down
38 changes: 31 additions & 7 deletions ladybug_comfort/cli/mtx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, \
Expand Down Expand Up @@ -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.
Expand All @@ -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])
Expand Down Expand Up @@ -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))
Expand Down
97 changes: 97 additions & 0 deletions ladybug_comfort/map/_helper.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 0b88748

Please sign in to comment.