Source code for honeybee_radiance.writer

# coding=utf-8
"""Methods to write to rad."""
from honeybee_radiance.sensorgrid import SensorGrid
from ladybug.futil import write_to_file_by_name, preparedir
from honeybee.config import folders
from honeybee.face import Face
from honeybee.boundarycondition import Surface
from honeybee.facetype import AirBoundary
from honeybee_radiance_folder.folder import ModelFolder
import honeybee_radiance_folder.config as folder_config

from .geometry import Polygon
from .modifier.material import aBSDF, BSDF, Trans
from .lib.modifiers import black

import os
import sys
import json
import shutil
import re
import itertools
from collections import defaultdict


[docs] def shade_mesh_to_rad(shade_mesh, blk=False): """Generate a RAD string representation of a ShadeMesh. Note that the resulting string does not include modifier definitions. Args: shade: A honeybee ShadeMesh for which a RAD representation will be returned. blk: Boolean to note whether the "blacked out" version of the Shade should be output, which is useful for direct studies and isolation studies to understand the contribution of individual apertures. """ rad_prop = shade_mesh.properties.radiance modifier = rad_prop.modifier_blk if blk else rad_prop.modifier base_geo = modifier.identifier + ' polygon {} 0 0 9 {}' shd_id = shade_mesh.identifier geo_strs = [] str_vertices = tuple(tuple(str(v) for v in pt.to_array()) for pt in shade_mesh.vertices) for fi, f_geo in enumerate(shade_mesh.faces): coords = tuple(v for pt in f_geo for v in str_vertices[pt]) poly_id = '{}_{}'.format(shd_id, fi) geo_str = base_geo.format(poly_id, ' '.join(coords)) geo_strs.append(geo_str) return '\n'.join(geo_strs)
[docs] def shade_to_rad(shade, blk=False, minimal=False): """Generate a RAD string representation of a Shade. Note that the resulting string does not include modifier definitions. Nor does it include any states for dynamic geometry. Args: shade: A honeybee Shade for which a RAD representation will be returned. blk: Boolean to note whether the "blacked out" version of the Shade should be output, which is useful for direct studies and isolation studies to understand the contribution of individual apertures. minimal: Boolean to note whether the radiance string should be written in a minimal format (with spaces instead of line breaks). Default: False. """ rad_prop = shade.properties.radiance modifier = rad_prop.modifier_blk if blk else rad_prop.modifier rad_poly = Polygon(shade.identifier, shade.vertices, modifier) return rad_poly.to_radiance(minimal, False, False)
[docs] def door_to_rad(door, blk=False, minimal=False): """Generate a RAD string representation of a Door. Note that the resulting string does not include modifier definitions. Nor does it include any states for dynamic geometry. However, it does include any of the shades assigned to the Door. Args: door: A honeybee Door for which a RAD representation will be returned. blk: Boolean to note whether the "blacked out" version of the Door should be output, which is useful for direct studies and isolation studies to understand the contribution of individual apertures. minimal: Boolean to note whether the radiance string should be written in a minimal format (with spaces instead of line breaks). Default: False. """ rad_prop = door.properties.radiance modifier = rad_prop.modifier_blk if blk else rad_prop.modifier rad_poly = Polygon(door.identifier, door.vertices, modifier) door_strs = [rad_poly.to_radiance(minimal, False, False)] for shd in door.shades: door_strs.append(shade_to_rad(shd, blk, minimal)) return '\n\n'.join(door_strs)
[docs] def aperture_to_rad(aperture, blk=False, minimal=False): """Generate a RAD string representation of an Aperture. Note that the resulting string does not include modifier definitions. Nor does it include any states for dynamic geometry. However, it does include the shade geometry assigned to the Aperture. Args: aperture: A honeybee Aperture for which a RAD representation will be returned. blk: Boolean to note whether the "blacked out" version of the Aperture should be output, which is useful for direct studies and isolation studies to understand the contribution of individual apertures. minimal: Boolean to note whether the radiance string should be written in a minimal format (with spaces instead of line breaks). Default: False. """ rad_prop = aperture.properties.radiance modifier = rad_prop.modifier_blk if blk else rad_prop.modifier rad_poly = Polygon(aperture.identifier, aperture.vertices, modifier) ap_strs = [rad_poly.to_radiance(minimal, False, False)] for shd in aperture.shades: ap_strs.append(shade_to_rad(shd, blk, minimal)) return '\n\n'.join(ap_strs)
[docs] def face_to_rad(face, blk=False, minimal=False, exclude_sub_faces=False): """Get Face as a Radiance string. Note that the resulting string does not include modifier definitions. Nor does it include any states for dynamic geometry. However, it does include any of the shades assigned to the Face along with the Apertures and Doors in the Face. Args: face: A honeybee Face for which a RAD representation will be returned. blk: Boolean to note whether the "blacked out" version of the Face should be output, which is useful for direct studies and isolation studies to understand the contribution of individual apertures. minimal: Boolean to note whether the radiance string should be written in a minimal format (with spaces instead of line breaks). (Default: False). exclude_sub_faces:Boolean to note whether Apertures and Doors should be excluded from the output string. (Default: False). """ rad_prop = face.properties.radiance modifier = rad_prop.modifier_blk if blk else rad_prop.modifier rad_poly = Polygon(face.identifier, face.punched_vertices, modifier) face_strs = [rad_poly.to_radiance(minimal, False, False)] for shd in face.shades: face_strs.append(shade_to_rad(shd, blk, minimal)) if not exclude_sub_faces: for dr in face.doors: face_strs.append(door_to_rad(dr, blk, minimal)) for ap in face.apertures: face_strs.append(aperture_to_rad(ap, blk, minimal)) return '\n\n'.join(face_strs)
[docs] def room_to_rad(room, blk=False, minimal=False): """Generate a RAD string representation of a Room. This method will write all geometry associated with a Room including all Faces, Apertures, Doors, and Shades. However, it does not include modifiers for this geometry. Nor does it include any states for dynamic geometry and will only write the default state for each dynamic object. Args: room: A honeybee Room for which a RAD representation will be returned. blk: Boolean to note whether the "blacked out" version of the geometry should be output, which is useful for direct studies and isolation studies to understand the contribution of individual apertures. minimal: Boolean to note whether the radiance string should be written in a minimal format (with spaces instead of line breaks). Default: False. """ room_strs = [] for face in room.faces: room_strs.append(face_to_rad(face, blk, minimal)) for shd in room.shades: room_strs.append(shade_to_rad(shd, blk, minimal)) return '\n\n'.join(room_strs)
[docs] def model_to_rad(model, blk=False, minimal=False): r"""Generate a RAD string representation of a Model. The resulting strings will include all geometry (Rooms, Faces, Shades, Apertures, Doors) and all modifiers. However, it does not include any states for dynamic geometry and will only write the default state for each dynamic object. To correctly account for dynamic objects, the model_to_rad_folder should be used. Note that this method will also ensure that any Faces, Apertures, or Doors with Surface boundary condition only have one of such objects in the output string. Args: model: A honeybee Model for which a RAD representation will be returned. blk: Boolean to note whether the "blacked out" version of the geometry should be output, which is useful for direct studies and isolation studies to understand the contribution of individual apertures. minimal: Boolean to note whether the radiance string should be written in a minimal format (with spaces instead of line breaks). Default: False. Returns: A tuple of two strings. - model_str: A radiance string that contains all geometry of the Model. - modifier_str: A radiance string that contains all of the modifiers in the model. These will be modifier_blk if blk is True. """ # write all modifiers into the file modifier_str = ['# ============== MODIFIERS ==============\n'] rad_prop = model.properties.radiance modifiers = rad_prop.blk_modifiers if blk else rad_prop.modifiers if not blk: # must be imported here to avoid circular imports from .lib.modifiersets import generic_modifier_set_visible modifiers = set(modifiers + generic_modifier_set_visible.modifiers_unique) for mod in modifiers: modifier_str.append(mod.to_radiance(minimal)) # write all Faces into the file model_str = ['# ================ MODEL ================\n'] faces = model.faces interior_faces, offset = set(), model.tolerance * -2 if len(faces) != 0: model_str.append('# ================ FACES ================\n') for face in faces: if isinstance(face.boundary_condition, Surface): if face.identifier in interior_faces: face = face.duplicate() face.move(face.normal * offset) model_str.append(face_to_rad(face, blk, minimal, True)) else: interior_faces.add(face.boundary_condition.boundary_condition_object) model_str.append(face_to_rad(face, blk, minimal)) else: model_str.append(face_to_rad(face, blk, minimal)) # write all orphaned Apertures into the file apertures = model.orphaned_apertures interior_aps = set() if len(apertures) != 0: model_str.append('# ============== APERTURES ==============\n') for ap in apertures: if isinstance(ap.boundary_condition, Surface): if ap.identifier in interior_aps: continue interior_aps.add(ap.boundary_condition.boundary_condition_object) model_str.append(aperture_to_rad(ap, blk, minimal)) # write all orphaned Doors into the file doors = model.orphaned_doors interior_drs = set() if len(doors) != 0: model_str.append('# ================ DOORS ================\n') for dr in doors: if isinstance(dr.boundary_condition, Surface): if dr.identifier in interior_drs: continue interior_drs.add(dr.boundary_condition.boundary_condition_object) model_str.append(door_to_rad(dr, blk, minimal)) # write all Room shades into the file rooms = model.rooms if len(rooms) != 0: model_str.append('# ============== ROOM SHADES ==============\n') for room in rooms: for shd in room.shades: model_str.append(shade_to_rad(shd, blk, minimal)) # write all orphaned Shades into the file if len(model.orphaned_shades) != 0 or len(model.shade_meshes): model_str.append('# ============= CONTEXT SHADES =============\n') for shd in model.orphaned_shades: model_str.append(shade_to_rad(shd, blk, minimal)) for shd_msh in model.shade_meshes: model_str.append(shade_mesh_to_rad(shd_msh, blk)) return '\n\n'.join(model_str), '\n\n'.join(modifier_str)
[docs] def model_to_rad_folder( model, folder=None, config_file=None, minimal=False, grids=None, views=None, full_match=False ): r"""Write a honeybee model to a rad folder. The rad files in the resulting folders will include all geometry (Rooms, Faces, Shades, Apertures, Doors), all modifiers, and all states of dynamic objects. It also includes any SensorGrids and Views that are assigned to the model's radiance properties. Args: model: A honeybee Model for which radiance folder will be written. folder: An optional folder to be used as the root of the model's Radiance folder. If None, the files will be written into a sub-directory of the honeybee-core default_simulation_folder. This sub-directory is specifically: default_simulation_folder/[MODEL IDENTIFIER]/Radiance config_file: An optional config file path to modify the default folder names. If None, ``folder.cfg`` in ``honeybee-radiance-folder`` will be used. (Default: None). minimal: Boolean to note whether the radiance strings should be written in a minimal format (with spaces instead of line breaks). Default: False. grids: A list of sensor grid names to filter the sensor grids in the model. Use this argument to indicate specific sensor grids that should be included. By default all the sensor grids will be exported. You can use wildcard symbols in names. Use relative path from inside grids folder. views: A list of view files that should be exported to folder. Use this argument to limit what will be written to the radiance folder. You can use wildcard symbols in names. Use relative path from inside views folder. full_match: A boolean to filter grids and views by their identifiers as full matches. Setting this to True indicates that wildcard symbols will not be used in the filtering of grids and views. In this case the names of grids and views are filtered as is. (Default: False). """ # prepare the folder for simulation model_id = model.identifier if folder is None: folder = os.path.join(folders.default_simulation_folder, model_id, 'radiance') if not os.path.isdir(folder): preparedir(folder) # create the directory if it's not there model_folder = ModelFolder(folder, 'model', config_file) model_folder.write(folder_type=-1, cfg=folder_config.minimal, overwrite=True) # determine the number of places to which mesh vertices will be rounded dec_count = 3 # default value when there is no tolerance str_tol = str(model.tolerance).split('.') if len(str_tol) == 2 and str_tol[0] == '0': str_tol = str_tol[-1] dec_count = 0 for dig in str_tol: if dig == '0': dec_count += 1 else: dec_count += 1 break # gather and write static apertures to the folder aps, aps_blk = model.properties.radiance.subfaces_by_blk() mods, mods_blk, mod_combs, mod_names = _collect_modifiers(aps, aps_blk, True) _write_static_files( folder, model_folder.aperture_folder(full=True), 'aperture', aps, aps_blk, mods, mods_blk, mod_combs, mod_names, 'Face3D', minimal) # gather and write static faces faces, faces_blk = model.properties.radiance.faces_by_blk() f_mods, f_mods_blk, mod_combs, mod_names = _collect_modifiers(faces, faces_blk) _write_static_files( folder, model_folder.scene_folder(full=True), 'envelope', faces, faces_blk, f_mods, f_mods_blk, mod_combs, mod_names, 'PunchedFace3D', minimal) # gather and write static shades shades, shades_blk = model.properties.radiance.shades_by_blk() s_mods, s_mods_blk, mod_combs, mod_names = _collect_modifiers(shades, shades_blk) _write_static_files( folder, model_folder.scene_folder(full=True), 'shades', shades, shades_blk, s_mods, s_mods_blk, mod_combs, mod_names, 'Face3D', minimal) # gather and write static shade meshes shade_meshes, shade_meshes_blk = model.properties.radiance.shade_meshes_by_blk() sm_mods, sm_mods_blk, mod_combs, mod_names = \ _collect_modifiers(shade_meshes, shade_meshes_blk) _write_static_files( folder, model_folder.scene_folder(full=True), 'shade_meshes', shade_meshes, shade_meshes_blk, sm_mods, sm_mods_blk, mod_combs, mod_names, 'Mesh3D', minimal, dec_count) # write dynamic sub-face groups (apertures and doors) ext_dict = {} out_subfolder = model_folder.aperture_group_folder(full=True) dyn_subface = model.properties.radiance.dynamic_subface_groups if len(dyn_subface) != 0: preparedir(out_subfolder) for group in dyn_subface: if group.is_indoor: # TODO: Implement dynamic interior apertures once the radiance folder # structure is clear about how the "light path" should be input raise NotImplementedError('Dynamic interior apertures are not currently' ' supported by Model.to.rad_folder.') else: st_d = _write_dynamic_subface_files( folder, out_subfolder, group, minimal) _write_mtx_files(folder, out_subfolder, group, st_d, minimal) ext_dict[group.identifier] = st_d _write_dynamic_json(folder, out_subfolder, ext_dict) # write dynamic shade groups out_dict = {} in_dict = {} out_subfolder = model_folder.dynamic_scene_folder(full=True, indoor=False) in_subfolder = model_folder.dynamic_scene_folder(full=True, indoor=True) dyn_shade = model.properties.radiance.dynamic_shade_groups if len(dyn_shade) != 0: preparedir(out_subfolder) indoor_created = False for group in dyn_shade: if group.is_indoor: if not indoor_created: preparedir(in_subfolder) indoor_created = True st_d = _write_dynamic_shade_files(folder, in_subfolder, group, minimal) in_dict[group.identifier] = st_d else: st_d = _write_dynamic_shade_files(folder, out_subfolder, group, minimal) out_dict[group.identifier] = st_d _write_dynamic_json(folder, out_subfolder, out_dict) if indoor_created: _write_dynamic_json(folder, in_subfolder, in_dict) # copy all bsdfs into the bsdf folder bsdf_folder = model_folder.bsdf_folder(full=True) bsdf_mods = model.properties.radiance.bsdf_modifiers if len(bsdf_mods) != 0: preparedir(bsdf_folder) bsdfs_info = [] for bdf_mod in bsdf_mods: bsdf_name = os.path.split(bdf_mod.bsdf_file)[-1] new_bsdf_path = os.path.join(bsdf_folder, bsdf_name) shutil.copy(bdf_mod.bsdf_file, new_bsdf_path) bsdfs_info.append( { 'name': bdf_mod.display_name, 'identifier': bdf_mod.identifier, 'path': os.path.join(model_folder.bsdf_folder(full=False), bsdf_name) }) bsdf_info_file = os.path.join(bsdf_folder, '_info.json') with open(bsdf_info_file, 'w') as fp: json.dump(bsdfs_info, fp, indent=2) # write the assigned sensor grids and views into the correct folder grid_dir = model_folder.grid_folder(full=True) _write_sensor_grids(grid_dir, model, grids, full_match) view_dir = model_folder.view_folder(full=True) _write_views(view_dir, model, views, full_match) model_folder.combined_receivers(auto_mtx_path=False) return folder
def _write_sensor_grids(folder, model, grids_filter, full_match=False): """Write out the sensor grid files. Args: folder: The sensor grids folder. model: A Honeybee model. grids_filter: A list of sensor grid names to filter the sensor grids in the model. Use this argument to indicate specific sensor grids that should be included. By default all the sensor grids will be exported. You can use wildcard symbols in names. Use relative path from inside grids folder. full_match: A boolean to filter grids by their identifiers as full matches. (Default: False). Returns: A tuple for path to _info.json and _model_grids_info.json. The first file includes the information for the sensor grids that are written to the folder and the second one is the information for the input sensor grids from the model. Use ``_info.json`` for access the sensor grid information for running the commands and ``_model_grids_info`` file for loading the results back to match with the model. Model_grids_info has an extra key for `start_ln` which provides the start line for where the sensors for this grid starts in a pts file. Unless there are grids with same identifier this value will be set to 0. """ sensor_grids = model.properties.radiance.sensor_grids filtered_grids = _filter_by_pattern( sensor_grids, grids_filter, full_match=full_match) if len(filtered_grids) != 0: grids_info = [] preparedir(folder) # group_by_identifier grouped_grids = _group_by_identifier(filtered_grids) for grid in grouped_grids: grid.to_file(folder) grids_info.append(grid.info_dict(model)) # write information file for all the grids. grids_info_file = os.path.join(folder, '_info.json') if (sys.version_info < (3, 0)): # we need to manually encode it as UTF-8 with open(grids_info_file, 'wb') as fp: info_str = json.dumps(grids_info, indent=2, ensure_ascii=False) fp.write(info_str.encode('utf-8')) else: with open(grids_info_file, 'w', encoding='utf-8') as fp: json.dump(grids_info, fp, indent=2, ensure_ascii=False) # write input grids info model_grids_info = [] start_line = defaultdict(lambda: 0) for grid in filtered_grids: identifier = grid.identifier grid_info = { 'name': identifier, 'identifier': identifier, 'count': grid.count, 'group': grid.group_identifier or '', 'full_id': grid.full_identifier, 'start_ln': start_line[identifier], } start_line[identifier] += grid.count model_grids_info.append(grid_info) model_grids_info_file = os.path.join(folder, '_model_grids_info.json') with open(model_grids_info_file, 'w') as fp: json.dump(model_grids_info, fp, indent=2) return grids_info_file, model_grids_info_file elif len(sensor_grids) != 0: raise ValueError('All sensor grids were filtered out of the model folder!') def _write_views(folder, model, views_filter, full_match=False): """Write out the view files. Args: folder: The views folder. model: A Honeybee model. views_filter: A list of view names to filter the views in the model. Use this argument to indicate specific views that should be included. By default, all the views will be exported. You can use wildcard symbols in names. Use relative path from inside views folder. full_match: A boolean to filter views by their identifiers as full matches. (Default: False). Returns: The path to _info.json, which includes the information for the views that are written to the folder. """ model_views = model.properties.radiance.views filtered_views = _filter_by_pattern(model_views, views_filter, full_match=full_match) if len(filtered_views) != 0: preparedir(folder) # group_by_identifier views_info = [] for view in filtered_views: view.to_file(folder) info_file = os.path.join(folder, '{}.json'.format(view.identifier)) with open(info_file, 'w') as fp: json.dump(view.info_dict(model), fp, indent=4) view_info = { 'name': view.identifier, 'identifier': view.identifier, 'group': view.group_identifier or '', 'full_id': view.full_identifier } views_info.append(view_info) # write information file for all the views. views_info_file = os.path.join(folder, '_info.json') with open(views_info_file, 'w') as fp: json.dump(views_info, fp, indent=2) return views_info_file elif len(model_views) != 0: raise ValueError('All views were filtered out of the model folder!') def _write_dynamic_shade_files(folder, sub_folder, group, minimal=False): """Write out the files that need to go into any dynamic model folder. Args: folder: The model folder location on this machine. sub_folder: The sub-folder for the three files (relative to the model folder). group: A DynamicShadeGroup object to be written into files. minimal: Boolean noting whether radiance strings should be written minimally. Returns: A list of dictionaries to be written into the states.json file. """ # destination folder for all of the radiance files dest = os.path.join(folder, sub_folder) # loop through all states and write out the .rad files for them states_list = group.states_json_list for state_i, file_names in enumerate(states_list): default_str = group.to_radiance(state_i, direct=False, minimal=minimal) direct_str = group.to_radiance(state_i, direct=True, minimal=minimal) write_to_file_by_name(dest, file_names['default'].replace('./', ''), default_str) write_to_file_by_name(dest, file_names['direct'].replace('./', ''), direct_str) return states_list def _write_dynamic_subface_files(folder, sub_folder, group, minimal=False): """Write out the files that need to go into any dynamic model folder. Args: folder: The model folder location on this machine. sub_folder: The sub-folder for the three files (relative to the model folder). group: A DynamicSubFaceGroup object to be written into files. minimal: Boolean noting whether radiance strings should be written minimally. Returns: A list of dictionaries to be written into the states.json file. """ # destination folder for all of the radiance files dest = os.path.join(folder, sub_folder) # loop through all states and write out the .rad files for them states_list = group.states_json_list for state_i, file_names in enumerate(states_list): default_str = group.to_radiance(state_i, direct=False, minimal=minimal) direct_str = group.to_radiance(state_i, direct=True, minimal=minimal) write_to_file_by_name(dest, file_names['default'].replace('./', ''), default_str) write_to_file_by_name(dest, file_names['direct'].replace('./', ''), direct_str) # write out the black representation of the aperture black_str = group.blk_to_radiance(minimal) write_to_file_by_name(dest, file_names['black'].replace('./', ''), black_str) return states_list def _write_mtx_files(folder, sub_folder, group, states_json_list, minimal=False): """Write out the mtx files needed for 3-phase simulation into a model folder. Args: folder: The model folder location on this machine. sub_folder: The sub-folder for the three files (relative to the model folder). group: A DynamicSubFaceGroup object to be written into files. states_json_list: A list to be written into the states.json file. minimal: Boolean noting whether radiance strings should be written minimally. Returns: A list of dictionaries to be written into the states.json file. """ dest = os.path.join(folder, sub_folder) # destination folder for radiance files # check if all of the states of all of the vmtx and dmtx geometry are default one_mtx = all(st.mtxs_default for obj in group.dynamic_objects for st in obj.properties.radiance._states) if one_mtx: # if they're all default, we can use one file mtx_file = './{}..mtx.rad'.format(group.identifier) # loop through all states and write out the .rad files for them tmxt_valid = False for state_i, _ in enumerate(states_json_list): tmtx_bsdf = group.tmxt_bsdf(state_i) if tmtx_bsdf is not None: # it's a valid state for 3-phase tmxt_valid = True # add the tmxt to the states_json_list bsdf_name = os.path.split(tmtx_bsdf.bsdf_file)[-1] states_json_list[state_i]['tmtx'] = bsdf_name # add the vmtx and the dmtx to the states_json_list if one_mtx: states_json_list[state_i]['vmtx'] = mtx_file states_json_list[state_i]['dmtx'] = mtx_file else: states_json_list[state_i]['vmtx'] = \ './{}..vmtx..{}.rad'.format(group.identifier, str(state_i)) states_json_list[state_i]['dmtx'] = \ './{}..dmtx..{}.rad'.format(group.identifier, str(state_i)) vmtx_str = group.vmtx_to_radiance(state_i, minimal) dmtx_str = group.dmtx_to_radiance(state_i, minimal) write_to_file_by_name( dest, states_json_list[state_i]['vmtx'].replace('./', ''), vmtx_str) write_to_file_by_name( dest, states_json_list[state_i]['dmtx'].replace('./', ''), dmtx_str) # write the single mtx file if everything is default if one_mtx and tmxt_valid: mtx_str = group.vmtx_to_radiance(state_i, minimal) write_to_file_by_name(dest, mtx_file, mtx_str) def _write_dynamic_json(folder, sub_folder, json_dict): """Write out the files that need to go into any dynamic model folder. Args: folder: The model folder location on this machine. sub_folder: The sub-folder for the three files (relative to the model folder). json_dict: A dictionary to be written into the states.json file. """ if json_dict != {}: dest_file = os.path.join(folder, sub_folder, 'states.json') with open(dest_file, 'w') as fp: json.dump(json_dict, fp, indent=4) def _write_static_files( folder, sub_folder, file_id, geometry, geometry_blk, modifiers, modifiers_blk, mod_combs, mod_names, geo_type='Face3D', minimal=False, decimal_count=3): """Write out the three files that need to go into any static radiance model folder. This includes a .rad, .mat, and .blk file for the folder. This method will also catch any cases of BSDF modifiers and copy the XML files to the bsdf folder of the model folder. Args: folder: The model folder location on this machine. sub_folder: The sub-folder for the three files (relative to the model folder). file_id: An identifier to be used for the names of each of the files. geometry: A list of geometry objects all with default blk modifiers. geometry_blk: A list of geometry objects with overridden blk modifiers. modifiers: A list of modifiers to write. modifiers_blk: A list of modifier_blk to write. mod_combs: Dictionary of modifiers from _unique_modifier_blk_combinations method. mod_names: Modifier names from the _unique_modifier_blk_combinations method. geo_type: Text for the type of static geometry being written (either Face3D, PunchedFace3D, or Mesh3D). minimal: Boolean noting whether radiance strings should be written minimally. decimal_count: Integer for the number of decimal places to round mesh vertices """ def is_air_boundary(face): return isinstance(face, Face) and isinstance(face.type, AirBoundary) if len(geometry) != 0 or len(geometry_blk) != 0: # write the strings for the geometry face_strs = [] if geo_type == 'Face3D': for face in geometry: modifier = face.properties.radiance.modifier rad_poly = Polygon(face.identifier, face.vertices, modifier) face_strs.append(rad_poly.to_radiance(minimal, False, False)) for face, mod_name in zip(geometry_blk, mod_names): modifier = mod_combs[mod_name][0] rad_poly = Polygon(face.identifier, face.vertices, modifier) face_strs.append(rad_poly.to_radiance(minimal, False, False)) elif geo_type == 'Mesh3D': tol_f_str = '{:.' + str(decimal_count) + 'f}' for shade_mesh in geometry: str_vertices = tuple(tuple(tol_f_str.format(v) for v in pt.to_array()) for pt in shade_mesh.vertices) modifier = shade_mesh.properties.radiance.modifier base_geo = modifier.identifier + ' polygon {} 0 0 {} {}' shd_id = shade_mesh.identifier for fi, f_geo in enumerate(shade_mesh.faces): coords = tuple(v for pt in f_geo for v in str_vertices[pt]) poly_id = '{}_{}'.format(shd_id, fi) geo_str = base_geo.format(poly_id, len(coords), ' '.join(coords)) face_strs.append(geo_str) for shade_mesh, mod_name in zip(geometry_blk, mod_names): str_vertices = tuple(tuple(tol_f_str.format(v) for v in pt.to_array()) for pt in shade_mesh.vertices) modifier = mod_combs[mod_name][0] base_geo = modifier.identifier + ' polygon {} 0 0 {} {}' shd_id = shade_mesh.identifier for fi, f_geo in enumerate(shade_mesh.faces): coords = tuple(v for pt in f_geo for v in str_vertices[pt]) poly_id = '{}_{}'.format(shd_id, fi) geo_str = base_geo.format(poly_id, len(coords), ' '.join(coords)) face_strs.append(geo_str) else: # assume that it is punched Face3D for face in geometry: if not is_air_boundary(face): modifier = face.properties.radiance.modifier geo = face.punched_vertices if hasattr(face, 'punched_vertices') \ else face.vertices rad_poly = Polygon(face.identifier, geo, modifier) face_strs.append(rad_poly.to_radiance(minimal, False, False)) for face, mod_name in zip(geometry_blk, mod_names): if not is_air_boundary(face): modifier = mod_combs[mod_name][0] geo = face.punched_vertices if hasattr(face, 'punched_vertices') \ else face.vertices rad_poly = Polygon(face.identifier, geo, modifier) face_strs.append(rad_poly.to_radiance(minimal, False, False)) # write the strings for the modifiers mod_strs = [] mod_blk_strs = [] for mod in modifiers: if isinstance(mod, (aBSDF, BSDF)): _process_bsdf_modifier(mod, mod_strs, minimal) elif isinstance(mod, Trans): r_values = (mod.r_reflectance, mod.g_reflectance, mod.b_reflectance) if mod.identifier != 'air_boundary' and not \ all(v == 1 for v in r_values): mod_strs.append(mod.to_radiance(minimal)) else: mod_strs.append(mod.to_radiance(minimal)) for mod in modifiers_blk: if isinstance(mod, (aBSDF, BSDF)): _process_bsdf_modifier(mod, mod_blk_strs, minimal) elif isinstance(mod, Trans): r_values = (mod.r_reflectance, mod.g_reflectance, mod.b_reflectance) if mod.identifier != 'air_boundary' and not \ all(v == 1 for v in r_values): mod_blk_strs.append(mod.to_radiance(minimal)) else: mod_blk_strs.append(mod.to_radiance(minimal)) # write the three files for the model sub-folder dest = os.path.join(folder, sub_folder) if geo_type == 'Mesh3D': # write minimum specification to reduce file size write_to_file_by_name(dest, '{}.rad'.format(file_id), '\n'.join(face_strs)) else: write_to_file_by_name(dest, '{}.rad'.format(file_id), '\n\n'.join(face_strs)) write_to_file_by_name(dest, '{}.mat'.format(file_id), '\n\n'.join(mod_strs)) write_to_file_by_name(dest, '{}.blk'.format(file_id), '\n\n'.join(mod_blk_strs)) def _unique_modifiers(geometry_objects): """Get a list of unique modifiers across an array of geometry objects. Args: geometry_objects: An array of geometry objects (Faces, Apertures, Doors, Shades) for which unique modifiers will be determined. Returns: A list of all unique modifiers across the input geometry_objects """ modifiers = [] for obj in geometry_objects: mod = obj.properties.radiance.modifier if not _instance_in_array(mod, modifiers): modifiers.append(mod) return list(set(modifiers)) def _unique_modifier_blk_combinations(geometry_objects): """Get lists of unique modifier/modifier_blk combinations across geometry objects. Args: geometry_objects: An array of geometry objects (Faces, Apertures, Doors, Shades) for which unique combinations of modifier and modifier_blk will be determined. Returns: A tuple with two objects. - modifier_combs: A dictionary of modifiers with identifiers of modifiers as keys and tuples with two modifiers as values. Each item in the dictionary represents a unique combination of modifier and modifier_blk found in the objects. Both modifiers in the pair have the same identifier (making them write-able into a radiance folder). The first item in the tuple is the true modifier while the second one is the modifier_blk. - modifier_names: A list of modifier names with one name for each of the input geometry_objects. These names can be looked up in the modifier_combs dictionary to get the modifier combination for a given geometry object """ modifier_combs = {} modifier_names = [] for obj in geometry_objects: mod = obj.properties.radiance.modifier mod_blk = obj.properties.radiance.modifier_blk comb_name = '{}_{}'.format(mod.identifier, mod_blk.identifier) modifier_names.append(comb_name) try: # test to see if the combination already exists modifier_combs[comb_name] except KeyError: # new combination of modifier and modifier_blk new_mod = mod.duplicate() new_mod.identifier = comb_name new_mod_blk = mod_blk.duplicate() new_mod_blk.identifier = comb_name modifier_combs[comb_name] = (new_mod, new_mod_blk) return modifier_combs, modifier_names def _collect_modifiers(geo, geo_blk, aperture=False): """Collect all modifier and modifier_blk across geometry.""" mods = _unique_modifiers(geo) mods_blk = [] for mod in mods: if mod.is_opaque or aperture: # static transparent apertures still use black mod_blk = black.duplicate() mod_blk.identifier = mod.identifier mods_blk.append(mod_blk) else: mods_blk.append(mod) mod_combs, mod_names = _unique_modifier_blk_combinations(geo_blk) mods.extend([mod_comb[0] for mod_comb in mod_combs.values()]) mods_blk.extend([mod_comb[1] for mod_comb in mod_combs.values()]) return mods, mods_blk, mod_combs, mod_names def _process_bsdf_modifier(modifier, mod_strs, minimal): """Process a BSDF modifier for a radiance model folder.""" bsdf_name = os.path.split(modifier.bsdf_file)[-1] mod_dup = modifier.duplicate() # duplicate to avoid editing the original # we must edit the hidden _bsdf_file property since the file has not yet been copied mod_dup._bsdf_file = os.path.join('model', 'bsdf', bsdf_name) mod_strs.append(mod_dup.to_radiance(minimal)) def _instance_in_array(object_instance, object_array): """Check if a specific object instance is already in an array. This can be much faster than `if object_instance in object_array` when you expect to be testing a lot of the same instance of an object for inclusion in an array since the builtin method uses an == operator to test inclusion. """ for val in object_array: if val is object_instance: return True return False def _filter_by_pattern(input_objects, filter, full_match=False): """Filter model grids and views based on user input.""" if not filter or filter == '*': return input_objects elif len(filter) == 1 and filter[0].replace('"', '').replace("'", '').strip() == '*': return input_objects if not isinstance(filter, (list, tuple)): filter = [filter] if not full_match: patterns = [ re.compile(f.strip().replace('*', '.+').replace('?', '.')) for f in filter ] else: patterns = [ re.compile(f.strip()) if f.startswith('^') and f.endswith('$') else re.compile('^%s$' % f.strip()) for f in filter ] indexes = [] for count, obj in enumerate(input_objects): try: id_ = obj.full_identifier except AttributeError: id_ = obj['full_id'] for pattern in patterns: if re.search(pattern, id_): indexes.append(count) indexes = list(set(indexes)) indexes.sort() return [input_objects[i] for i in indexes] def _group_by_identifier(sensor_grids): """Group sensor grids or views if they have the same full identifier.""" group_func = lambda grid: grid.full_identifier # noqa: E731 ordered_sensor_grids = sorted(sensor_grids, key=group_func) # check if there is any duplicated identifiers ids = {grid.full_identifier for grid in sensor_grids} if len(list(ids)) == len(sensor_grids): # there is no duplicated identifier - return the original list return sensor_grids updated_grids = [] for group_identifier, grids in itertools.groupby(ordered_sensor_grids, group_func): grids = list(grids) if len(grids) > 1: # merge grids into one sensors = [] for grid in grids: sensors.extend(grid.sensors) joined_grid = SensorGrid(grids[0].identifier, sensors) joined_grid.group_identifier = grids[0].group_identifier updated_grids.append(joined_grid) else: updated_grids.append(grids[0]) return updated_grids