Source code for honeybee_vtk.model

"""A VTK representation of HBModel."""

from __future__ import annotations
import pathlib
import shutil
import webbrowser
import tempfile
import os
import json
import warnings

from collections import defaultdict
from typing import Dict, List, Union, Tuple

from honeybee.facetype import face_types
from honeybee.model import Model as HBModel
from honeybee_radiance.sensorgrid import SensorGrid
from honeybee_radiance.writer import _filter_by_pattern
from ladybug.color import Color
from ladybug_geometry.geometry3d import Mesh3D

from .actor import Actor
from .scene import Scene
from .camera import Camera
from .types import ModelDataSet, PolyData, RadialSensor
from .to_vtk import convert_aperture, convert_face, convert_room, convert_shade, \
    convert_sensor_grid, convert_door
from .vtkjs.schema import IndexJSON, DisplayMode, SensorGridOptions
from .vtkjs.helper import convert_directory_to_zip_file, add_data_to_viewer
from .types import DataSetNames, VTKWriters, JoinedPolyData, ImageTypes
from .config import DataConfig, Autocalculate
from .legend_parameter import Text
from .text_actor import TextActor


_COLORSET = {
    'Wall': [0.901, 0.705, 0.235, 1],
    'Aperture': [0.250, 0.705, 1, 0.5],
    'Door': [0.627, 0.588, 0.392, 1],
    'Shade': [0.470, 0.294, 0.745, 1],
    'Floor': [1, 0.501, 0.501, 1],
    'RoofCeiling': [0.501, 0.078, 0.078, 1],
    'AirBoundary': [1, 1, 0.784, 1],
    'Grid': [0.925, 0.250, 0.403, 1]
}


DATA_SETS = {
    'Aperture': 'apertures', 'Door': 'doors', 'Shade': 'shades',
    'Wall': 'walls', 'Floor': 'floors', 'RoofCeiling': 'roof_ceilings',
    'AirBoundary': 'air_boundaries'
}


[docs]class Model(object): """A honeybee-vtk model. The model objects are accessible based on their types: * apertures * doors * shades * walls * floors * roof_ceilings * air_boundaries * sensor_grids You can control the style for each type separately. """ def __init__( self, hb_model: HBModel, grid_options: SensorGridOptions = SensorGridOptions.Ignore, radial_sensor: RadialSensor = RadialSensor()) -> None: """Instantiate a honeybee-vtk model object. Args: model : A text string representing the path to the hbjson file. load_grids: A SensorGridOptions object. Defaults to SensorGridOptions.Ignore which will ignore the grids in hbjson and will not load them in the honeybee-vtk model. radial_sensor: A RadialSensor object to customize the triangles to be created in the radial sensor grid in case the radial grid is selected from the sensor grid options. """ super().__init__() self._hb_model = hb_model self._sensor_grids_option = grid_options self._apertures = ModelDataSet('Aperture', color=self.get_default_color('Aperture')) self._doors = ModelDataSet('Door', color=self.get_default_color('Door')) self._shades = ModelDataSet('Shade', color=self.get_default_color('Shade')) self._walls = ModelDataSet('Wall', color=self.get_default_color('Wall')) self._floors = ModelDataSet('Floor', color=self.get_default_color('Floor')) self._roof_ceilings = ModelDataSet('RoofCeiling', color=self.get_default_color('RoofCeiling')) self._air_boundaries = ModelDataSet('AirBoundary', color=self.get_default_color('AirBoundary')) self._sensor_grids = ModelDataSet('Grid', color=self.get_default_color('Grid')) self._cameras = [] self._radial_sensor = radial_sensor self._convert_model() self._load_grids() self._load_cameras()
[docs] @classmethod def from_hbjson(cls, hbjson: str, load_grids: SensorGridOptions = SensorGridOptions.Ignore, radial_sensor: RadialSensor = RadialSensor()) -> Model: """Translate hbjson to a honeybee-vtk model. Args: model : A text string representing the path to the hbjson file. load_grids: A SensorGridOptions object. Defaults to SensorGridOptions.Ignore which will ignore the grids in hbjson and will not load them in the honeybee-vtk model. radial_sensor: A RadialSensor object to customize the triangles to be created in the radial sensor grid in case the radial grid is selected from the sensor grid options. Returns: A honeybee-vtk model object. """ hb_file = pathlib.Path(hbjson) assert hb_file.is_file(), f'{hbjson} doesn\'t exist.' model = HBModel.from_hbjson(hb_file.as_posix()) return cls(model, load_grids, radial_sensor)
@property def walls(self) -> ModelDataSet: """Model walls.""" return self._walls @property def apertures(self) -> ModelDataSet: """Model aperture.""" return self._apertures @property def shades(self) -> ModelDataSet: """Model shades.""" return self._shades @property def doors(self) -> ModelDataSet: """Model doors.""" return self._doors @property def floors(self) -> ModelDataSet: """Model floors.""" return self._floors @property def roof_ceilings(self) -> ModelDataSet: """Roof and ceilings.""" return self._roof_ceilings @property def air_boundaries(self) -> ModelDataSet: """Air boundaries.""" return self._air_boundaries @property def sensor_grids(self) -> ModelDataSet: """Sensor grids.""" return self._sensor_grids @property def cameras(self): """List of Camera objects attached to this Model object.""" return self._cameras @cameras.setter def cameras(self, cams: List(Camera)) -> None: """Set the cameras for this Model object.""" self._cameras = cams
[docs] def get_modeldataset(self, dataset: DataSetNames) -> ModelDataSet: """Get a ModelDataSet object from a model. Args: dataset: A DataSetNames object. Returns: A ModelDataSet object. """ ds = {ds.name.lower(): ds for ds in self} return ds[dataset.value]
def __iter__(self): """This dunder method makes this class an iterator object. Due to this method, you can access apertures, walls, shades, doors, floors, roof_ceilings, air_boundaries and sensor_grids in a model like items of a list in Python. Which means, you can use loops on these objects of a model. """ for dataset in ( self.apertures, self.walls, self.shades, self.doors, self.floors, self.roof_ceilings, self.air_boundaries, self.sensor_grids ): yield dataset def _load_grids(self) -> None: """Load sensor grids.""" if self._sensor_grids_option == SensorGridOptions.Ignore: return if hasattr(self._hb_model.properties, 'radiance') and \ self._hb_model.properties.radiance.sensor_grids: grids = self._hb_model.properties.radiance.sensor_grids # list of unique sensor_grid identifiers in the model ids = set([grid.identifier for grid in grids]) # if all the grids have the same identifier, merge them into one grid if len(ids) == 1: id = grids[0].identifier # if it's just one grid, use it if len(grids) == 1: sensor_grid = grids[0] # if there are more than one grid, merge them first else: grid_meshes = [grid.mesh for grid in grids] if all(grid_meshes): mesh = Mesh3D.join_meshes(grid_meshes) sensor_grid = SensorGrid.from_mesh3d(id, mesh) else: sensors = [sensor for grid in grids for sensor in grid.sensors] sensor_grid = SensorGrid(id, sensors) # TODO extract this to a function try: convert_sensor_grid(sensor_grid, self._sensor_grids_option, self._radial_sensor.angle, self._radial_sensor.radius) except ValueError: warnings.warn(f'Grid {id} does not have mesh information. Hence, ' 'it will not be converted to a sensor grid. Try with' ' SensorGridOptions.Sensors.') else: self._sensor_grids.data.append(convert_sensor_grid( sensor_grid, self._sensor_grids_option, self._radial_sensor.angle, self._radial_sensor.radius)) # else add them as separate grids else: for sensor_grid in grids: # TODO extract this to a function try: convert_sensor_grid(sensor_grid, self._sensor_grids_option, self._radial_sensor.angle, self._radial_sensor.radius) except ValueError: warnings.warn(f'Grid {sensor_grid.identifier} does not have' ' mesh information. Hence, it will not be' ' converted to a sensor grid. Try with' ' SensorGridOptions.Sensors.') else: self._sensor_grids.data.append(convert_sensor_grid( sensor_grid, self._sensor_grids_option, self._radial_sensor.angle, self._radial_sensor.radius)) def _load_cameras(self) -> None: """Load radiance views.""" if len(self._hb_model.properties.radiance.views) > 0: for view in self._hb_model.properties.radiance.views: self._cameras.append(Camera.from_view(view))
[docs] def update_display_mode(self, value: DisplayMode) -> None: """Change display mode for all the object types in the model. Sensor grids display model will not be affected. For changing the display model for a single object type, change the display_mode property separately. .. code-block:: python model.sensor_grids.display_mode = DisplayMode.Wireframe """ for attr in DATA_SETS.values(): self.__getattribute__(attr).display_mode = value
def _convert_model(self) -> None: """An internal method to convert the objects on class initiation.""" if hasattr(self._hb_model, 'rooms'): for room in self._hb_model.rooms: objects = convert_room(room) self._add_objects(self.separate_by_type(objects)) if hasattr(self._hb_model, 'orphaned_shades'): for face in self._hb_model.orphaned_shades: self._shades.data.append(convert_shade(face)) if hasattr(self._hb_model, 'orphaned_apertures'): for face in self._hb_model.orphaned_apertures: self._apertures.data.extend(convert_aperture(face)) if hasattr(self._hb_model, 'orphaned_doors'): for face in self._hb_model.orphaned_doors: self._doors.data.extend(convert_door(face)) if hasattr(self._hb_model, 'orphaned_faces'): for face in self._hb_model.orphaned_faces: objects = convert_face(face) self._add_objects(self.separate_by_type(objects)) def _add_objects(self, data: Dict) -> None: """Add object to different fields based on data type. This method is called from inside ``_convert_model``. Valid values for key are different types: * aperture * door * shade * wall * floor * roof_ceiling * air_boundary * sensor_grid """ for key, value in data.items(): try: # Note: this approach will fail for air_boundary attr = DATA_SETS[key] self.__getattribute__(attr).data.extend(value) except KeyError: raise ValueError(f'Unsupported type: {key}') except AttributeError: raise AttributeError(f'Invalid attribute: {attr}')
[docs] def actors(self) -> List[Actor]: """Create a list of vtk actors from a honeybee-vtk model. Args: model: A honeybee-vtk model. Returns: A list of vtk actors. """ return [Actor(modeldataset=ds) for ds in self if len(ds.data) > 0]
[docs] def to_vtkjs(self, *, folder: str = '.', name: str = None, config: str = None, validation: bool = False, model_display_mode: DisplayMode = DisplayMode.Shaded, grid_display_mode: DisplayMode = DisplayMode.Shaded) -> str: """Write the model to a vtkjs file. Write your honeybee-vtk model to a vtkjs file that you can open in Paraview-Glance or the Pollination Viewer. Args: folder: A valid text string representing the location of folder where you'd want to write the vtkjs file. Defaults to current working directory. name : Name for the vtkjs file. File name will be Model.vtkjs if not provided. config: Path to the config file in JSON format. Defaults to None. validation: Boolean to indicate whether to validate the data before loading. Defaults to False. model_display_mode: Display mode for the model. Defaults to shaded. grid_display_mode: Display mode for the Grids. Defaults to shaded. Returns: A text string representing the file path to the vtkjs file. """ scene = Scene() actors = self.actors() scene.add_actors(actors) self.update_display_mode(model_display_mode) self.sensor_grids.display_mode = grid_display_mode # load data if provided if config: self.load_config(config, scene=scene, validation=validation, legend=True) # name of the vtkjs file file_name = name or 'model' # create a temp folder temp_folder = tempfile.mkdtemp() # The folder set by the user is the target folder target_folder = os.path.abspath(folder) # Set a file path to move the .zip file to the target folder target_vtkjs_file = os.path.join(target_folder, file_name + '.vtkjs') # write every dataset scene = [] for data_set in DATA_SETS.values(): data = getattr(self, data_set) path = data.to_folder(temp_folder) if not path: # empty dataset continue scene.append(data.as_data_set()) # add sensor grids # it is separate from other DATA_SETS mainly for data visualization data = self.sensor_grids path = data.to_folder(temp_folder) if path: scene.append(data.as_data_set()) # write index.json index_json = IndexJSON() index_json.scene = scene index_json.to_json(temp_folder) # zip as vtkjs temp_vtkjs_file = convert_directory_to_zip_file(temp_folder, extension='vtkjs', move=False) # Move the generated vtkjs to target folder shutil.move(temp_vtkjs_file, target_vtkjs_file) try: shutil.rmtree(temp_folder) except Exception: pass return target_vtkjs_file
[docs] def to_html(self, *, folder: str = '.', name: str = None, show: bool = False, config: str = None, validation: bool = False, model_display_mode: DisplayMode = DisplayMode.Shaded, grid_display_mode: DisplayMode = DisplayMode.Shaded) -> str: """Write the model to an HTML file. Write your honeybee-vtk model to an HTML file that you can open in any modern browser and can also share with other. Args: folder: A valid text string representing the location of folder where you'd want to write the HTML file. Defaults to current working directory. name : Name for the HTML file. File name will be Model.html if not provided. show: A boolean value. If set to True, the HTML file will be opened in the default browser. Defaults to False config: Path to the config file in JSON format. Defaults to None. validation: Boolean to indicate whether to validate the data before loading. model_display_mode: Display mode for the model. Defaults to shaded. grid_display_mode: Display mode for the Grids. Defaults to shaded. Returns: A text string representing the file path to the HTML file. """ self.update_display_mode(model_display_mode) # Name of the html file file_name = name or 'model' # Set the target folder target_folder = os.path.abspath(folder) # Set a file path to move the .html file to the target folder html_file = os.path.join(target_folder, file_name + '.html') # Set temp folder to do the operation temp_folder = tempfile.mkdtemp() vtkjs_file = self.to_vtkjs( folder=temp_folder, config=config, validation=validation, model_display_mode=model_display_mode, grid_display_mode=grid_display_mode) temp_html_file = add_data_to_viewer(vtkjs_file) shutil.copy(temp_html_file, html_file) try: shutil.rmtree(temp_folder) except Exception: pass if show: webbrowser.open(html_file) return html_file
[docs] def to_files(self, *, folder: str = '.', name: str = None, writer: VTKWriters = VTKWriters.binary, config: str = None, validation: bool = False, model_display_mode: DisplayMode = DisplayMode.Shaded, grid_display_mode: DisplayMode = DisplayMode.Shaded) -> str: """ Write a .zip of VTK/VTP files. Args: folder: File path to the output folder. The file will be written to the current folder if not provided. name: A text string for the name of the .zip file to be written. If no text string is provided, the name of the HBJSON file will be used as a file name for the .zip file. writer: A VTkWriters object. Default is binary which will write .vtp files. config: Path to the config file in JSON format. Defaults to None. validation: Boolean to indicate whether to validate the data before loading. model_display_mode: Display mode for the model. Defaults to shaded. grid_display_mode: Display mode for the Grids. Defaults to shaded. Returns: A text string containing the path to the .zip file with VTK/VTP files. """ scene = Scene() actors = self.actors() scene.add_actors(actors) self.update_display_mode(model_display_mode) self.sensor_grids.display_mode = grid_display_mode # load data if provided if config: self.load_config(config, scene=scene, validation=validation, legend=True) # Name of the html file file_name = name or 'model' # Set the target folder target_folder = os.path.abspath(folder) # Set a file path to move the .zip file to the target folder target_zip_file = os.path.join(target_folder, file_name + '.zip') # Set temp folder to do the operation temp_folder = tempfile.mkdtemp() # Write datasets to vtk/vtp files for ds in self: if len(ds.data) == 0: continue elif len(ds.data) > 1: jp = JoinedPolyData() jp.extend(ds.data) jp.to_vtk(temp_folder, ds.name, writer) elif len(ds.data) == 1: polydata = ds.data[0] polydata.to_vtk(temp_folder, ds.name, writer) # collect files in a zip temp_zip_file = convert_directory_to_zip_file(temp_folder, extension='zip', move=False) # Move the generated zip file to the target folder shutil.move(temp_zip_file, target_zip_file) try: shutil.rmtree(temp_folder) except Exception: pass return target_zip_file
[docs] def to_images(self, *, folder: str = '.', config: str = None, validation: bool = False, model_display_mode: DisplayMode = DisplayMode.Shaded, grid_display_mode: DisplayMode = DisplayMode.Shaded, background_color: Tuple[int, int, int] = None, view: List[str] = None, image_type: ImageTypes = ImageTypes.png, image_width: int = 0, image_height: int = 0, image_scale: int = 1 ) -> List[str]: """Export images from model. Args: folder: A valid text string representing the location of folder where you'd want to write the images. Defaults to current working directory. config: Path to the config file in JSON format. Defaults to None. validation: Boolean to indicate whether to validate the data before loading. model_display_mode: Display mode for the model. Defaults to shaded. grid_display_mode: Display mode for the grid. Defaults to shaded. background_color: Background color of the image. Defaults to white. view: A list of paths to radiance view files. Defaults to None. image_type: Image type to be exported. Defaults to png. image_width: Image width. Defaults to 0. Which will use the default radiance view's horizontal angle. image_height: Image height. Defaults to 0. Which will use the default radiance view's vertical angle. image_scale: An integer value as a scale factor. Defaults to 1. Returns: A list of text strings representing the file paths to the images. """ scene = Scene(background_color=background_color) actors = self.actors() scene.add_actors(actors) self.update_display_mode(model_display_mode) self.sensor_grids.display_mode = grid_display_mode if config: self.load_config(config, scene=scene, legend=True, validation=validation) # Set a default camera if there are no cameras in the model if not self.cameras and not view: camera = Camera(identifier='plan', projection='l') scene.add_cameras(camera) bounds = Actor.get_bounds(actors) centroid = Actor.get_centroid(actors) aerial_cameras = camera.aerial_cameras(bounds, centroid) scene.add_cameras(aerial_cameras) else: if len(self.cameras) != 0: cameras = self.cameras scene.add_cameras(cameras) if view: for vf in view: camera = Camera.from_view_file(file_path=vf) scene.add_cameras(camera) for actor in scene.actors: if actor.name == 'Grid': assert actor.modeldataset.display_mode == grid_display_mode, 'Grid display'\ ' mode is not set correctly.' else: assert actor.modeldataset.display_mode == model_display_mode, 'Model display'\ ' mode is not set correctly.' return scene.export_images( folder=folder, image_type=image_type, image_width=image_width, image_height=image_height, image_scale=image_scale )
[docs] def to_grid_images(self, config: str, *, folder: str = '.', grid_filter: Union[str, List[str]] = '*', full_match: bool = False, grid_display_mode: DisplayMode = DisplayMode.SurfaceWithEdges, background_color: Tuple[int, int, int] = None, image_type: ImageTypes = ImageTypes.png, image_width: int = 0, image_height: int = 0, image_name: str = '', image_scale: int = 1, text_actor: TextActor = None, grid_camera_dict: Dict[str, vtk.vtkCamera] = None, extract_camera: bool = False, grid_colors: List[Color] = None, sub_folder_name: str = None) -> Union[Dict[str, Camera], List[str]]: """Export am image for each grid in the model. Use the config file to specify which grids with which data to export. For instance, if the config file has DataConfig objects for 'DA' and 'UDI', and 'DA' is kept hidden, then all grids with 'UDI' data will be exported. Additionally, images from a selected number of grids can be exported by using the by specifying the identifiers of the grids to export in the grid_filter object in the config file. Note that the parameters grid_camera_dict and extract_camera are mutually exclusive. If both are provided, the extract_camera will be used and the grid_camera_dict will be ignored. Args: config: Path to the config file in JSON format. folder: Path to the folder where you'd like to export the images. Defaults to the current working directory. grid_filter: A list of grid identifiers or a regex pattern as a string to filter the grids. Defaults to None. full_match: A boolean to filter grids by their identifiers as full matches. Defaults to False. display_mode: Display mode for the grid. Defaults to surface with edges. background_color: Background color of the image. Defaults to white. image_type: Image type to be exported. Defaults to png. image_width: Image width in pixels. Defaults to 0. Which will use the default radiance view's horizontal angle to derive the width. image_height: Image height in pixels. Defaults to 0. Which will use the default radiance view's vertical angle to derive the height. image_name: A text string that sets the name of the image. Defaults to ''. image_scale: An integer value as a scale factor. Defaults to 1. text_actor: A TextActor object that defines the properties of the text to be added to the image. Defaults to None. grid_camera_dict: A dictionary of grid identifiers and vtkCamera objects. If provided, the camera objects specified in the dict will be used to export the images. This is useful when a camera from another run is to be used in this run to export an image. Defaults to None. extract_camera: Boolean to indicate whether to extract the camera from the for this run to use for the next run. Defaults to False. sub_folder_name: A text string that sets the name of the subfolder where the images will be exported. This is useful when the images are to be exported for multiple time periods such as whole day of June 21, and the whole day of March, 21. Defaults to None. Returns: Path to the folder where the images are exported for each grid. Or a dictionary of grid identifiers and camera objects. """ assert len(self.sensor_grids.data) != 0, 'No sensor grids found in the model.' if self._sensor_grids_option == SensorGridOptions.Sensors: grid_display_mode = DisplayMode.Points config_data = self.load_config(config) grid_polydata_lst = _filter_grid_polydata( self.sensor_grids.data, self._hb_model, grid_filter, full_match) output: Union[Dict[str, Camera], List[str]] = {} if extract_camera else [] grid_colors_supplied = True if grid_colors else False for data in config_data: for grid_polydata in grid_polydata_lst: dataset = ModelDataSet(name=grid_polydata.identifier, data=[grid_polydata], display_mode=grid_display_mode) if not grid_colors_supplied and data.grid_colors: grid_colors = [Color(r, g, b) for r, g, b in data.grid_colors] dataset.color_by = data.identifier if grid_colors: if len(grid_colors) == 1: dataset.active_field_info.legend_parameter._assign_colors( [Color(255, 255, 255), grid_colors[0]]) else: dataset.active_field_info.legend_parameter._assign_colors( grid_colors) actor = Actor(dataset) camera = _camera_to_grid_actor(actor, data.identifier) scene = Scene(background_color=background_color, actors=[actor], cameras=[camera], text_actor=text_actor) legend_range = self._get_legend_range(data) self._load_legend_parameters(data, scene, legend_range) if extract_camera: vtk_camera = None output[grid_polydata.identifier] = scene.export_images( folder=folder, image_type=image_type, image_width=image_width, image_height=image_height, image_name=image_name, image_scale=image_scale, vtk_camera=vtk_camera, extract_camera=extract_camera ) else: if grid_camera_dict: vtk_camera = grid_camera_dict[grid_polydata.identifier] else: vtk_camera = None # this is not a good design but it takes too much refactoring to # remove it. I'm writing this down for whenever we get a chance # to refactor honeybee-vtk. if not sub_folder_name: grid_folder = pathlib.Path( f'{folder}/{grid_polydata.identifier}') else: grid_folder = pathlib.Path( f'{folder}/{grid_polydata.identifier}/{sub_folder_name}') if not grid_folder.exists(): grid_folder.mkdir(parents=True, exist_ok=True) output += scene.export_images(folder=grid_folder, image_type=image_type, image_width=image_width, image_height=image_height, image_name=image_name, image_scale=image_scale, vtk_camera=vtk_camera) return output
[docs] @ staticmethod def get_default_color(face_type: face_types) -> Color: """Get the default color based of face type. Use these colors to generate visualizations that are familiar for Ladybug Tools users. User can overwrite these colors as needed. This method converts decimal RGBA to integer RGBA values. """ color = _COLORSET.get(face_type, [1, 1, 1, 1]) return Color(*(v * 255 for v in color))
[docs] @ staticmethod def separate_by_type(data: List[PolyData]) -> Dict: """Separate PolyData objects by type.""" data_dict = defaultdict(list) for d in data: data_dict[d.type].append(d) return data_dict
[docs] def load_config(self, json_path: str, scene: Scene = None, validation: bool = False, legend: bool = False) -> List[DataConfig]: """Mount data on model from config json. Args: json_path: File path to the config json file. scene: A honeybee-vtk scene object. Defaults to None. validation: A boolean indicating whether to validate the data before loading. legend: A boolean indicating whether to load legend parameters. Returns: A list of parsed DataConfig objects. """ assert len(self.sensor_grids.data) > 0, 'Sensor grids are not loaded on' ' this model. Reload them using grid options.' config_dir = pathlib.Path(json_path).parent config_data: List[DataConfig] = [] try: with open(json_path) as fh: config = json.load(fh) except json.decoder.JSONDecodeError: raise TypeError( 'Not a valid json file.' ) else: for json_obj in config['data']: # validate config data = DataConfig.parse_obj(json_obj) # only if data is requested move forward. if not data.hide: folder_path = pathlib.Path(data.path) if not folder_path.is_dir(): folder_path = config_dir.joinpath( folder_path).resolve().absolute() data.path = folder_path.as_posix() if not folder_path.is_dir(): raise FileNotFoundError( f'No folder found at {data.path}') grid_type = self._get_grid_type() # Validate data if asked for if validation: self._validate_simulation_data(data, grid_type) # get legend range if provided by the user legend_range = self._get_legend_range(data) # Load data self._load_data(data, grid_type, legend_range) # Load legend parameters if legend: self._load_legend_parameters(data, scene, legend_range) config_data.append(data) else: warnings.warn( f'Data for {data.identifier} is not loaded.' ) return config_data
def _get_grid_type(self) -> str: """Get the type of grid in the model Args: model(Model): A honeybee-vtk model. Returns: A string indicating whether the model has points and meshes. """ if self.sensor_grids.data[0].GetNumberOfCells() == 1: return 'points' else: return 'meshes' def _validate_simulation_data(self, data: DataConfig, grid_type: str) -> None: """Match result data with the sensor grids in the model. It will be checked if the number of data files and the names of the data files match with the grid identifiers. This function does not support validating result data for other than sensor grids as of now. This is a helper method to the public load_config method. Args: data: A DataConfig object. grid_type: A string indicating whether the model has points and meshes. """ # file path to the json file grids_info_json = pathlib.Path(data.path).joinpath('grids_info.json') # read the json file with open(grids_info_json) as fh: grids_info = json.load(fh) # TODO: Make sure to remove this limitation. A user should not have to always # TODO: load all the grids assert len(self.sensor_grids.data) == len(grids_info), 'The number of result'\ f' files {len(grids_info)} does for {data.identifier} does not match'\ f' the number of sensor grids in the model {len(self.sensor_grids.data)}.' # match identifiers of the grids with the identifiers of the result files grids_model_identifiers = [grid.identifier for grid in self.sensor_grids.data] grids_info_identifiers = [grid['identifier'] for grid in grids_info] assert grids_model_identifiers == grids_info_identifiers, 'The identifiers of'\ ' the sensor grids in the model do not match the identifiers of the grids'\ f' in the grids_info.json for {data.identifier}.' # make sure length of each file matches the number of sensors in grid file_lengths = [grid['count'] for grid in grids_info] # check if the grid data is meshes or points # if grid is sensors if grid_type == 'points': num_sensors = [polydata.GetNumberOfPoints() for polydata in self.sensor_grids.data] # if grid is meshes else: num_sensors = [polydata.GetNumberOfCells() for polydata in self.sensor_grids.data] # lastly check if the length of a file matches the number of sensors or # meshes on grid if file_lengths != num_sensors: length_matching = { grids_info_identifiers[i]: file_lengths[i] == num_sensors[i] for i in range(len(grids_model_identifiers)) } names_to_report = [ id for id in length_matching if length_matching[id] is False] raise ValueError( 'File lengths of result files must match the number of sensors on grids.' ' Lengths of files with following names do not match' f' {tuple(names_to_report)}.') def _load_data(self, data: DataConfig, grid_type: str, legend_range: List[Union[float, int]]) -> None: """Load validated data on a honeybee-vtk model. This is a helper method to the public load_config method. Args: data (DataConfig): A Dataconfig object. model: A honeybee-vtk model. grid_type: A string indicating whether the sensor grid in the model is made of points or meshes. legend_range: A list of min and max values of the legend parameters provided by the user in the config file. """ folder_path = pathlib.Path(data.path) folder_path = folder_path.as_posix() folder_path = pathlib.Path(folder_path) identifier = data.identifier if isinstance(data.lower_threshold, float): lower_threshold = data.lower_threshold else: lower_threshold = None if isinstance(data.upper_threshold, float): upper_threshold = data.upper_threshold else: upper_threshold = None result_file_paths = _get_result_file_paths(folder_path) result = [] for res_file_path in result_file_paths: assert res_file_path.exists(), f'No file found at {res_file_path}' grid_res = [float(v) for v in res_file_path.read_text().splitlines()] result.append(grid_res) ds = self.get_modeldataset(DataSetNames.grid) if grid_type == 'meshes': ds.add_data_fields(result, name=identifier, per_face=True, data_range=legend_range, lower_threshold=lower_threshold, upper_threshold=upper_threshold) ds.color_by = identifier else: ds.add_data_fields(result, name=identifier, per_face=False, data_range=legend_range, lower_threshold=lower_threshold, upper_threshold=upper_threshold) ds.color_by = identifier @ staticmethod def _get_legend_range(data: DataConfig) -> List[Union[float, int]]: """Read and get legend min and max values from data if provided by the user. The value provided by this function is processed and validated in _get_data_range function in the type module. Args: data (DataConfig): A Dataconfig object. Returns: A list of two numbers representing min and max values for data. """ if data.legend_parameters: legend_params = data.legend_parameters if isinstance(legend_params.min, Autocalculate): min = None else: min = legend_params.min if isinstance(legend_params.max, Autocalculate): max = None else: max = legend_params.max return [min, max] def _load_legend_parameters(self, data: DataConfig, scene: Scene, legend_range: List[Union[float, int]]) -> None: """Load legend_parameters. Args: data: A Dataconfig object. scene: A honeyebee-vtk scene object. legend_range: A list of min and max values of the legend parameters provided by the user in the config file. """ legend_params = data.legend_parameters legend = scene.legend_parameter(data.identifier) legend.colorset = legend_params.color_set legend.reverse_colorset = legend_params.reverse_color_set legend.unit = data.unit if legend_range: legend.min, legend.max = legend_range else: legend.min, legend.max = [None, None] legend.hide_legend = legend_params.hide_legend legend.orientation = legend_params.orientation legend.position = legend_params.position legend.width = legend_params.width legend.height = legend_params.height if isinstance(legend_params.color_count, int): legend.color_count = legend_params.color_count else: legend.color_count = None if isinstance(legend_params.label_count, int): legend.label_count = legend_params.label_count else: legend.label_count = None legend.decimal_count = legend_params.decimal_count legend.preceding_labels = legend_params.preceding_labels label_params = legend_params.label_parameters legend.label_parameters = Text( label_params.color, label_params.size, label_params.bold) title_params = legend_params.title_parameters legend.title_parameters = Text( title_params.color, title_params.size, title_params.bold)
def _camera_to_grid_actor(actor: Actor, data_name: str, zoom: int = 2, auto_zoom: bool = True, camera_offset: int = 3, clipping_range: Tuple[int, int] = (0, 4), ) -> Camera: """Create a Camera for a grid actor. This function uses the center point of a grid actor to create a camera that is setup at the camera_offset distance from the center point. Args: actor: An Actor object. data_name: name of the data being loaded on the grid. This is used in naming the image files. zoom: The zoom level of the camera. Defaults to 2. auto_zoom: A boolean to set the camera to auto zoom. Setting this to True will discard the Zoom level. Set this to False to use the zoom level. Defaults to True. camera_offset: The distance between the camera and the sensor grid. Defaults to 100. clipping_range: The clipping range of the camera. Defaults to (100, 101). Returns: A Camera object. """ cent_pt = actor.centroid return Camera(identifier=f'{data_name}_{actor.name}', position=(cent_pt.x, cent_pt.y, cent_pt.z + camera_offset), projection='l', focal_point=cent_pt, clipping_range=clipping_range, parallel_scale=zoom, reset_camera=auto_zoom) def _get_result_file_paths(folder_path: Union[str, pathlib.Path]): if not isinstance(folder_path, pathlib.Path): folder_path = pathlib.Path(folder_path) grids_info_json = folder_path.joinpath('grids_info.json') with open(grids_info_json) as fh: grids_info = json.load(fh) # finding file extension for grid results # This could have been avoided if the file extension was provided in the # grids_info.json file. extension = None match = pathlib.Path(grids_info[0]['full_id']).stem for path in folder_path.rglob('*'): if path.stem == match: extension = path.suffix break else: raise ValueError(f'Failed to find the extension from {path}') # result file paths return [folder_path.joinpath(f"{grid['full_id']}{extension}") for grid in grids_info] def _filter_grid_polydata(grid_polydata_lst: List[PolyData], model: HBModel, grid_filter: Union[str, List[str]], full_match) -> List[PolyData]: """Filter grid polydata based on sensor grids. Args: grid_polydata_lst: A list of grid polydata objects. model: A honeybee model object. grid_filter: A list of grid identifiers or a regex pattern as a string to filter the grid polydata. full_match: A boolean to filter grids by their identifiers as full matches. Returns: A list of PolyData objects for Grids. """ if not grid_filter or grid_filter[0] == '*': return grid_polydata_lst else: filtered_sensor_grids = _filter_by_pattern( model.properties.radiance.sensor_grids, grid_filter, full_match) sensorgrid_identifiers = [ grid.identifier for grid in filtered_sensor_grids] filtered_grid_polydata_lst = [grid for grid in grid_polydata_lst if grid.name in sensorgrid_identifiers] if not filtered_grid_polydata_lst: raise ValueError('No grids found in the model that match the' f' filter {grid_filter}.') return filtered_grid_polydata_lst