Skip to content

Commit

Permalink
feat(model): Add method to translate model to vis set
Browse files Browse the repository at this point in the history
  • Loading branch information
chriswmackey committed Mar 20, 2024
1 parent f40f116 commit aeb0b7e
Show file tree
Hide file tree
Showing 9 changed files with 361 additions and 4 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r extras-requirements.txt
pip install -r dev-requirements.txt
- name: run tests
run: python -m pytest tests/
Expand Down
4 changes: 4 additions & 0 deletions dragonfly_display/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from dragonfly_display.cli import display

if __name__ == '__main__':
display()
6 changes: 6 additions & 0 deletions dragonfly_display/_extend_dragonfly.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# coding=utf-8
# import the core dragonfly modules
from dragonfly.model import Model

# import the extension functions
from .model import model_to_vis_set

# inject the methods onto the classes
Model.to_vis_set = model_to_vis_set
187 changes: 187 additions & 0 deletions dragonfly_display/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
"""dragonfly-display commands."""
import click
import sys
import os
import logging
import json
import pickle

from honeybee_display.attr import FaceAttribute, RoomAttribute

from dragonfly.model import Model
from dragonfly.cli import main


_logger = logging.getLogger(__name__)


# command group for all display extension commands.
@click.group(help='dragonfly display commands.')
@click.version_option()
def display():
pass


@display.command('model-to-vis')
@click.argument('model-file', type=click.Path(
exists=True, file_okay=True, dir_okay=False, resolve_path=True))
@click.option(
'--multiplier/--full-geometry', ' /-fg', help='Flag to note if the '
'multipliers on each Building story should be passed along to the '
'generated Honeybee Room objects or if full geometry objects should be '
'written for each story in the building.', default=True, show_default=True)
@click.option(
'--no-ceil-adjacency/--ceil-adjacency', ' /-a', help='Flag to indicate '
'whether adjacencies should be solved between interior stories when '
'Room2D floor and ceiling geometries are coplanar. This ensures '
'that Surface boundary conditions are used instead of Adiabatic ones. '
'Note that this input has no effect when the object-per-model is Story.',
default=True, show_default=True)
@click.option(
'--color-by', '-c', help='Text for the property that dictates the colors of '
'the Model geometry. Choose from: type, boundary_condition, none. '
'If none, only a wireframe of the Model will be generated (assuming the '
'--exclude-wireframe option is not used). None is useful when the primary '
'purpose of the visualization is to display results in relation to the Model '
'geometry or display some room_attr or face_attr as an AnalysisGeometry '
'or Text labels.', type=str, default='type', show_default=True)
@click.option(
'--wireframe/--exclude-wireframe', ' /-xw', help='Flag to note whether a '
'ContextGeometry dedicated to the Model Wireframe (in DisplayLineSegment3D) should '
'be included in the output VisualizationSet.', default=True, show_default=True)
@click.option(
'--mesh/--faces', help='Flag to note whether the colored model geometries should '
'be represented with DisplayMesh3D objects instead of DisplayFace3D objects. '
'Meshes can usually be rendered faster and they scale well for large models '
'but all geometry is triangulated (meaning that their wireframe in certain '
'platforms might not appear ideal).', default=True, show_default=True)
@click.option(
'--show-color-by/--hide-color-by', ' /-hcb', help='Flag to note whether the '
'color-by geometry should be hidden or shown by default. Hiding the color-by '
'geometry is useful when the primary purpose of the visualization is to display '
'grid-data or room/face attributes but it is still desirable to have the option '
'to turn on the geometry.', default=True, show_default=True)
@click.option(
'--room-attr', '-r', help='An optional text string of an attribute that the Model '
'Rooms have, which will be used to construct a visualization of this attribute '
'in the resulting VisualizationSet. Multiple instances of this option can be passed '
'and a separate VisualizationData will be added to the AnalysisGeometry that '
'represents the attribute in the resulting VisualizationSet (or a separate '
'ContextGeometry layer if room_text_labels is True). Room attributes '
'input here can have . that separates the nested attributes from '
'one another. For example, properties.energy.program_type.',
type=click.STRING, multiple=True, default=None, show_default=True)
@click.option(
'--face-attr', '-f', help='An optional text string of an attribute that the Model '
'Faces have, which will be used to construct a visualization of this attribute in '
'the resulting VisualizationSet. Multiple instances of this option can be passed and'
' a separate VisualizationData will be added to the AnalysisGeometry that '
'represents the attribute in the resulting VisualizationSet (or a separate '
'ContextGeometry layer if face_text_labels is True). Face attributes '
'input here can have . that separates the nested attributes from '
'one another. For example, properties.energy.construction.',
type=click.STRING, multiple=True, default=None, show_default=True)
@click.option(
'--color-attr/--text-attr', help='Flag to note whether to note whether the '
'input room-attr and face-attr should be expressed as a colored AnalysisGeometry '
'or a ContextGeometry as text labels.', default=True, show_default=True)
@click.option(
'--grid-display-mode', '-m', help='Text that dictates how the ContextGeometry '
'for Model SensorGrids should display in the resulting visualization. The Default '
'option will draw sensor points whenever there is no grid_data_path and will not '
'draw them at all when grid data is provided, assuming the AnalysisGeometry of '
'the grids is sufficient. Choose from: Default, Points, Wireframe, Surface, '
'SurfaceWithEdges, None.',
type=str, default='Default', show_default=True)
@click.option(
'--hide-grid/--show-grid', ' /-sg', help='Flag to note whether the SensorGrid '
'ContextGeometry should be hidden or shown by default.',
default=True, show_default=True)
@click.option(
'--output-format', '-of', help='Text for the output format of the resulting '
'VisualizationSet File (.vsf). Choose from: vsf, json, pkl, vtkjs, html. Note '
'that both vsf and json refer to the the JSON version of the VisualizationSet '
'file and the distinction between the two is only for help in coordinating file '
'extensions (since both .vsf and .json can be acceptable). Also note that '
'ladybug-vtk must be installed in order for the vtkjs or html options to be usable '
'and the html format refers to a web page with the vtkjs file embedded within it. '
'The vtkjs and html options also require an explicit --output-file to be specified.',
type=str, default='vsf', show_default=True)
@click.option(
'--output-file', help='Optional file to output the JSON string of '
'the config object. By default, it will be printed out to stdout',
type=click.File('w'), default='-', show_default=True)
def model_to_vis_set(
model_file, multiplier, no_ceil_adjacency,
color_by, wireframe, mesh, show_color_by,
room_attr, face_attr, color_attr, grid_display_mode, hide_grid,
output_format, output_file):
"""Translate a Dragonfly Model file (.dfjson) to a VisualizationSet file (.vsf).
This command can also optionally translate the Dragonfly Model to a .vtkjs file,
which can be visualized in the open source Visual ToolKit (VTK) platform.
\b
Args:
model_file: Full path to a Dragonfly Model (DFJSON or DFpkl) file.
"""
try:
model_obj = Model.from_file(model_file)
room_attrs = [] if len(room_attr) == 0 or room_attr[0] == '' else room_attr
face_attrs = [] if len(face_attr) == 0 or face_attr[0] == '' else face_attr
text_labels = not color_attr
hide_color_by = not show_color_by

face_attributes = []
for fa in face_attrs:
faa = FaceAttribute(name=fa, attrs=[fa], color=color_attr, text=text_labels)
face_attributes.append(faa)

room_attributes = []
for ra in room_attrs:
raa = RoomAttribute(name=ra, attrs=[ra], color=color_attr, text=text_labels)
room_attributes.append(raa)

ceil_adjacency = not no_ceil_adjacency
vis_set = model_obj.to_vis_set(
multiplier, ceil_adjacency, color_by=color_by, include_wireframe=wireframe,
use_mesh=mesh, hide_color_by=hide_color_by, room_attrs=room_attributes,
face_attrs=face_attributes, grid_display_mode=grid_display_mode,
hide_grid=hide_grid)
output_format = output_format.lower()
if output_format in ('vsf', 'json'):
output_file.write(json.dumps(vis_set.to_dict()))
elif output_format == 'pkl':
if output_file.name != '<stdout>':
out_folder, out_file = os.path.split(output_file.name)
vis_set.to_pkl(out_file, out_folder)
else:
output_file.write(pickle.dumps(vis_set.to_dict()))
elif output_format in ('vtkjs', 'html'):
assert output_file.name != '<stdout>', \
'Must specify an --output-file to use --output-format vtkjs.'
out_folder, out_file = os.path.split(output_file.name)
try:
if out_file.endswith('.vtkjs'):
out_file = out_file[:-6]
elif out_file.endswith('.html'):
out_file = out_file[:-5]
if output_format == 'vtkjs':
vis_set.to_vtkjs(output_folder=out_folder, file_name=out_file)
if output_format == 'html':
vis_set.to_html(output_folder=out_folder, file_name=out_file)
except AttributeError as ae:
raise AttributeError(
'Ladybug-vtk must be installed in order to use --output-format '
'vtkjs.\n{}'.format(ae))
else:
raise ValueError('Unrecognized output-format "{}".'.format(output_format))
except Exception as e:
_logger.exception('Failed to translate Model to VisualizationSet.\n{}'.format(e))
sys.exit(1)
else:
sys.exit(0)


# add display sub-group to dragonfly CLI
main.add_command(display)
78 changes: 78 additions & 0 deletions dragonfly_display/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Method to translate a Dragonfly Model to a VisualizationSet."""
from honeybee_display.model import model_to_vis_set as hb_model_to_vis_set


def model_to_vis_set(
model, use_multiplier=True, solve_ceiling_adjacencies=False,
color_by='type', include_wireframe=True, use_mesh=True,
hide_color_by=False, room_attrs=None, face_attrs=None,
grid_display_mode='Default', hide_grid=True):
"""Translate a Dragonfly Model to a VisualizationSet.
Args:
model: A Dragonfly Model object to be converted to a VisualizationSet.
use_multiplier: If True, the multipliers on this Model's Stories will be
passed along to the generated Honeybee Room objects, indicating the
simulation will be run once for each unique room and then results
will be multiplied. If False, full geometry objects will be written
for each and every floor in the building that are represented through
multipliers and all resulting multipliers will be 1. (Default: True).
solve_ceiling_adjacencies: Boolean to note whether adjacencies should be
solved between interior stories when Room2D floor and ceiling
geometries are coplanar. This ensures that Surface boundary
conditions are used instead of Adiabatic ones. Note that this input
has no effect when the object_per_model is Story. (Default: False).
color_by: Text that dictates the colors of the Model geometry.
If none, only a wireframe of the Model will be generated, assuming
include_wireframe is True. This is useful when the primary purpose of
the visualization is to display results in relation to the Model
geometry or display some room_attrs or face_attrs as an AnalysisGeometry
or Text labels. (Default: type). Choose from the following:
* type
* boundary_condition
* None
include_wireframe: Boolean to note whether a ContextGeometry dedicated to
the Model Wireframe (in DisplayLineSegment3D) should be included
in the output VisualizationSet. (Default: True).
use_mesh: Boolean to note whether the colored model geometries should
be represented with DisplayMesh3D objects (True) instead of DisplayFace3D
objects (False). Meshes can usually be rendered faster and they scale
well for large models but all geometry is triangulated (meaning that
the wireframe in certain platforms might not appear ideal). (Default: True).
hide_color_by: Boolean to note whether the color_by geometry should be
hidden or shown by default. Hiding the color-by geometry is useful
when the primary purpose of the visualization is to display grid_data
or room/face attributes but it is still desirable to have the option
to turn on the geometry.
room_attrs: An optional list of room attribute objects from the
honeybee_display.attr module.
face_attrs: An optional list of face attribute objects from the
honeybee_display.attr module.
grid_display_mode: Text that dictates how the ContextGeometry for Model
SensorGrids should display in the resulting visualization. The Default
option will draw sensor points. Choose from the following:
* Default
* Points
* Wireframe
* Surface
* SurfaceWithEdges
* None
hide_grid: Boolean to note whether the SensorGrid ContextGeometry should be
hidden or shown by default. (Default: True).
Returns:
A VisualizationSet object that represents the model.
"""
# create the Honeybee Model from the Dragonfly one
hb_model = model.to_honeybee(
'District', use_multiplier=use_multiplier,
solve_ceiling_adjacencies=solve_ceiling_adjacencies,
enforce_adj=False, enforce_solid=True)[0]
# convert the Honeybee Model to a VisualizationSet
return hb_model_to_vis_set(
hb_model, color_by, include_wireframe, use_mesh, hide_color_by,
room_attrs, face_attrs, grid_display_mode, hide_grid)
6 changes: 3 additions & 3 deletions extras-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
dragonfly-energy>=1.95.35
dragonfly-radiance>=1.64.118
ladybug-vtk>=0.13.9
dragonfly-energy>=1.25.28
dragonfly-radiance>=0.3.20
ladybug-vtk>=0.14.0
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
honeybee-display>=0.3.4
honeybee-display>=0.3.6
dragonfly-core>=1.42.18
1 change: 1 addition & 0 deletions tests/json/model_with_doors_skylights.dfjson

Large diffs are not rendered by default.

80 changes: 80 additions & 0 deletions tests/model_extend_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""Test the Model to_vis_set method."""
from ladybug_display.geometry3d import DisplayMesh3D, DisplayLineSegment3D, \
DisplayText3D
from ladybug_display.visualization import VisualizationSet, \
ContextGeometry, AnalysisGeometry, VisualizationData
from dragonfly.model import Model
from honeybee_display.attr import RoomAttribute, FaceAttribute


def test_default_to_vis_set():
"""Test the default output of Model.to_vis_set()."""
model_json = './tests/json/model_with_doors_skylights.dfjson'
parsed_model = Model.from_dfjson(model_json)
vis_set = parsed_model.to_vis_set()

assert isinstance(vis_set, VisualizationSet)
assert len(vis_set) == 10
for geo_obj in vis_set[:-1]:
assert isinstance(geo_obj, ContextGeometry)
assert isinstance(geo_obj[0], DisplayMesh3D)
assert isinstance(vis_set[-1], ContextGeometry)
assert vis_set[-1].display_name == 'Wireframe'
assert isinstance(vis_set[-1][0], DisplayLineSegment3D)

vis_set = parsed_model.to_vis_set(include_wireframe=False)
assert len(vis_set) == 9
for geo_obj in vis_set:
assert isinstance(geo_obj, ContextGeometry)
assert isinstance(geo_obj[0], DisplayMesh3D)

vis_set = parsed_model.to_vis_set(color_by='none')
assert len(vis_set) == 1
for geo_obj in vis_set:
assert isinstance(geo_obj, ContextGeometry)
assert isinstance(geo_obj[0], DisplayLineSegment3D)

vis_set = parsed_model.to_vis_set(
color_by='boundary_condition', include_wireframe=False)
assert len(vis_set) == 5
for geo_obj in vis_set:
assert isinstance(geo_obj, ContextGeometry)
assert isinstance(geo_obj[0], DisplayMesh3D)


def test_room_attr_to_vis_set():
"""Test the room attribute argument of Model.to_vis_set()."""
model_json = './tests/json/model_with_doors_skylights.dfjson'
parsed_model = Model.from_dfjson(model_json)
attr_color = RoomAttribute(
name='Floor Area', attrs=['floor_area'], text=False, color=True)
vis_set = parsed_model.to_vis_set(color_by='none', room_attrs=[attr_color])

assert isinstance(vis_set[0], AnalysisGeometry)
assert isinstance(vis_set[0][0], VisualizationData)
assert len(vis_set[0][0].values) == 15
attr_txt = RoomAttribute(
name='Floor Area', attrs=['floor_area'], text=True, color=False)
vis_set = parsed_model.to_vis_set(color_by='none', room_attrs=[attr_txt])
assert isinstance(vis_set[0], ContextGeometry)
assert len(vis_set[0]) == 15
for item in vis_set[0]:
assert isinstance(item, DisplayText3D)


def test_face_attr_to_vis_set():
"""Test the face attribute argument of Model.to_vis_set()."""
model_json = './tests/json/model_with_doors_skylights.dfjson'
parsed_model = Model.from_dfjson(model_json)
attr_color = FaceAttribute(name='Area', attrs=['area'], color=True, text=False)
vis_set = parsed_model.to_vis_set(color_by='None', face_attrs=[attr_color])
assert isinstance(vis_set[0], AnalysisGeometry)
assert isinstance(vis_set[0][0], VisualizationData)
assert len(vis_set[0][0].values) == 324

attr_txt = FaceAttribute(name='Area', attrs=['area'], color=False, text=True)
vis_set = parsed_model.to_vis_set(color_by='None', face_attrs=[attr_txt])
assert isinstance(vis_set[0], ContextGeometry)
assert len(vis_set[0]) == 324
for item in vis_set[0]:
assert isinstance(item, DisplayText3D)

0 comments on commit aeb0b7e

Please sign in to comment.