Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions dragonfly_trace/cli/translate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
import click
import sys
import logging
import io
import base64

from ladybug.commandutil import process_content_to_output
from dragonfly.model import Model
from dragonfly_trace.writer import model_to_trace700_csv as model_to_csv
from dragonfly_trace.writer import model_to_trace700_workbook as model_to_workbook


_logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -147,3 +150,157 @@ def model_to_trace700_csv(
)

return process_content_to_output(csv_str, output_file)


@translate.command('model-to-trace700-xlsx')
@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 will 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('--plenum/--separate-plenum', '-p/-sp', help='Flag to indicate whether '
'ceiling/floor plenum depths assigned to Room2Ds should simply be '
'reported as plenum depths in the tables or they should be used to generate '
'distinct separated plenum rooms in the translation.',
default=True, show_default=True)
@click.option('--merge-method', '-m', help='Text to describe how the Room2Ds should '
'be merged into individual Rooms during the translation. Specifying a '
'value here can be an effective way to reduce the number of Room '
'volumes in the resulting Model and, ultimately, yield a faster simulation '
'time with less results to manage. Choose from: None, Zones, PlenumZones, '
'Stories, PlenumStories.', type=str, default='None', show_default=True)
@click.option('--imperial/--metric', '-ip/-si', help='Flag to note whether imperial '
'or metric units should be used for values in the output XLSX.',
default=True, show_default=True)
@click.option('--geometry-ids/--geometry-names', ' /-gn', help='Flag to note whether a '
'cleaned version of all geometry display names should be used instead '
'of identifiers when translating the Model.',
default=True, show_default=True)
@click.option('--resource-ids/--resource-names', ' /-rn', help='Flag to note whether a '
'cleaned version of all resource display names should be used instead '
'of identifiers when translating the Model.',
default=True, show_default=True)
@click.option('--output-file', '-f', help='Optional XLSX file to output the content '
'of the translation. By default it printed out to stdout.',
type=click.File('wb'), default='-', show_default=True)
def model_to_trace700_xlsx_cli(
model_file, multiplier, plenum, merge_method, imperial,
geometry_ids, resource_ids, output_file
):
"""Translate a Dragonfly Model to an Excel file with tables for TRACE 700 attributes.

The resulting Excel tables can be copied into the tables that appear in the
Component Tree view of TRACE 700. The order and organization of rooms in
the resulting matrix should match that of the gbXML produced from the same model.

\b
Args:
model_file: Full path to a Dragonfly Model file (DFJSON or DFpkl).
"""
try:
full_geometry = not multiplier
separate_plenum = not plenum
metric = not imperial
geo_names = not geometry_ids
res_names = not resource_ids
model_to_trace700_xlsx(
model_file, full_geometry, separate_plenum, merge_method,
metric, geo_names, res_names, output_file
)
except Exception as e:
_logger.exception('System translation failed.\n{}'.format(e))
sys.exit(1)
else:
sys.exit(0)


def model_to_trace700_xlsx(
model_file, full_geometry=False, separate_plenum=False, merge_method='None',
metric=False, geometry_names=False, resource_names=False, output_file=None,
multiplier=True, plenum=True, imperial=True, geometry_ids=True, resource_ids=True
):
"""Translate a Dragonfly Model to an Excel file with tables for TRACE 700 attributes.

The resulting Excel tables can be copied into the tables that appear in the
Component Tree view of TRACE 700. The order and organization of rooms in
the resulting matrix should match that of the gbXML produced from the same model.

Args:
model: A dragonfly Model for which a TRACE 700 XLSX file will be returned.
multiplier: If True, the multipliers on this Model's Stories will be
passed along to the XLSX. 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).
separate_plenum: Boolean to indicate whether ceiling/floor plenum depths
assigned to Room2Ds should simply be reported as plenum depths in the
tables or they should be used to generate distinct separated plenum
rooms in the translation. (Default: False).
merge_method: An optional text string to describe how the Room2Ds should
be merged into individual Rooms during the translation. Specifying a
value here can be an effective way to reduce the number of Room
volumes in the resulting model and, ultimately, yield a faster
simulation time in the destination engine with fewer results
to manage. Note that Room2Ds will only be merged if they form a
continuous volume. Otherwise, there will be multiple Rooms per
zone or story, each with an integer added at the end of their
identifiers. Choose from the following options:

* None - No merging of Room2Ds will occur
* Zones - Room2Ds in the same zone will be merged
* PlenumZones - Only plenums in the same zone will be merged
* Stories - Rooms in the same story will be merged
* PlenumStories - Only plenums in the same story will be merged

metric: Boolean to note whether the units of the values in the resulting
matrix are in SI (True) instead of IP (False). (Default: False).
geometry_names: Boolean to note whether a cleaned version of all geometry
display names should be used instead of identifiers when translating
the Model to OSM and IDF. Using this flag will affect all Rooms, Faces,
Apertures, Doors, and Shades. It will generally result in more read-able
names in the OSM and IDF but this means that it will not be easy to map
the EnergyPlus results back to the original Honeybee Model. Cases
of duplicate IDs resulting from non-unique names will be resolved
by adding integers to the ends of the new IDs that are derived from
the name. (Default: False).
resource_names: Boolean to note whether a cleaned version of all resource
display names should be used instead of identifiers when translating
the Model to OSM and IDF. Using this flag will affect all Materials,
Constructions, ConstructionSets, Schedules, Loads, and ProgramTypes.
It will generally result in more read-able names for the resources
in the OSM and IDF. Cases of duplicate IDs resulting from non-unique
names will be resolved by adding integers to the ends of the new IDs
that are derived from the name. (Default: False).
output_file: Optional XLSX file to output the XLSX content of the translation.
By default this content will be returned from this method as a
base64 string.
"""
# load the model and translate it to a Workbook
model = Model.from_file(model_file)
exclude_plenums = not separate_plenum
workbook = model_to_workbook(
model, multiplier, exclude_plenums, merge_method,
metric, geometry_names, resource_names
)
if isinstance(output_file, str):
workbook.save(output_file)
else:
# save workbook to a byte stream
out = io.BytesIO()
workbook.save(out)
# retrieve bytes and write them directly to file objects with wb encoding
out.seek(0) # reset stream position to the beginning
excel_bytes = out.read()
if output_file is not None and output_file.name != '<stdout>' and \
output_file.mode == 'wb':
output_file.write(excel_bytes)
else: # convert the bytes to a base64 string
b = base64.b64encode(excel_bytes)
if output_file is None:
f_contents = b.decode('utf-8')
return f_contents
elif output_file.mode == 'w':
f_contents = b.decode('utf-8')
output_file.write(f_contents)
else:
output_file.write(b)
15 changes: 2 additions & 13 deletions dragonfly_trace/loads.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from ladybug.datatype.power import Power
from ladybug.datatype.energyflux import EnergyFlux
from honeybee.altnumber import autocalculate


def people_and_lights_trace700_matrix(rooms, si_units=False):
Expand Down Expand Up @@ -51,18 +50,8 @@ def people_and_lights_trace700_matrix(rooms, si_units=False):
ppl_obj = room.properties.energy.people
if ppl_obj is not None:
ppl_count = room.floor_area * ppl_obj.people_per_area
act_sch = ppl_obj.activity_schedule
try: # ScheduleRuleset
vals = []
for sch in act_sch.typical_day_schedules:
vals.extend(sch.values)
act_level = max(vals)
except AttributeError: # ScheduleFixedInterval
act_level = max(act_sch.values)
latent_fract = ppl_obj.latent_fraction \
if ppl_obj.latent_fraction != autocalculate else 0.5
sensible_ppl = act_level * (1 - latent_fract)
latent_ppl = act_level * latent_fract
sensible_ppl = ppl_obj.activity_max_sensible
latent_ppl = ppl_obj.activity_max_latent
else:
ppl_count = 0
sensible_ppl = 73.26775
Expand Down
154 changes: 153 additions & 1 deletion dragonfly_trace/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
from __future__ import division

from collections import OrderedDict
try:
import openpyxl
except Exception: # we are in Python 2 and Excel export is not possible
openpyxl = None

from ladybug.datatype.area import Area
from ladybug.datatype.distance import Distance
Expand Down Expand Up @@ -51,7 +55,7 @@ def rooms_to_trace700_matrix(rooms, si_units=False):
'Acoustic Ceiling Resistance ({})'.format(r_unit),
'Cooling Dry Bulb ({})'.format(temp_unit),
'Heating Dry Bulb ({})'.format(temp_unit),
'Relative Humidity (%)'
'Relative Humidity (%)',
'Cooling Driftpoint ({})'.format(temp_unit),
'Heating Driftpoint ({})'.format(temp_unit),
'Thermostat Cooling Schedule',
Expand Down Expand Up @@ -396,3 +400,151 @@ def model_to_trace700_csv(
csv_matrix.append(','.join([str(val) for val in row]))

return '\n'.join(csv_matrix)


def model_to_trace700_workbook(
model, use_multiplier=True, exclude_plenums=True, merge_method=None,
si_units=False, geometry_names=False, resource_names=False
):
"""Generate an Excel Workbook (openpyxl) with TRACE 700 attributes of a Model.

The resulting openpyxl Workbook can be saved and opened in Excel. The data
in the tables can then be copied into the tables that appear in the Component
Tree view of TRACE 700. The order and organization of rooms in the resulting
matrix should match that of the gbXML produced from the same model.

Args:
model: A dragonfly Model for which a TRACE 700 Excel Workbook will be returned.
use_multiplier: If True, the multipliers on this Model's Stories will be
passed along to the Workbook. 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).
exclude_plenums: Boolean to indicate whether ceiling/floor plenum depths
assigned to Room2Ds should be ignored during translation. This
results in each Room2D translating to a single Honeybee Room at
the full floor_to_ceiling_height instead of a base Room with (a)
plenum Room(s). (Default: True).
merge_method: An optional text string to describe how the Room2Ds should
be merged into individual Rooms during the translation. Specifying a
value here can be an effective way to reduce the number of Room
volumes in the resulting model and, ultimately, yield a faster
simulation time in the destination engine with fewer results
to manage. Note that Room2Ds will only be merged if they form a
continuous volume. Otherwise, there will be multiple Rooms per
zone or story, each with an integer added at the end of their
identifiers. Choose from the following options:

* None - No merging of Room2Ds will occur
* Zones - Room2Ds in the same zone will be merged
* PlenumZones - Only plenums in the same zone will be merged
* Stories - Rooms in the same story will be merged
* PlenumStories - Only plenums in the same story will be merged

si_units: Boolean to note whether the units of the values in the resulting
matrix are in SI (True) instead of IP (False). (Default: False).
geometry_names: Boolean to note whether a cleaned version of all geometry
display names should be used instead of identifiers when translating
the Model to OSM and IDF. Using this flag will affect all Rooms, Faces,
Apertures, Doors, and Shades. It will generally result in more read-able
names in the OSM and IDF but this means that it will not be easy to map
the EnergyPlus results back to the original Honeybee Model. Cases
of duplicate IDs resulting from non-unique names will be resolved
by adding integers to the ends of the new IDs that are derived from
the name. (Default: False).
resource_names: Boolean to note whether a cleaned version of all resource
display names should be used instead of identifiers when translating
the Model to OSM and IDF. Using this flag will affect all Materials,
Constructions, ConstructionSets, Schedules, Loads, and ProgramTypes.
It will generally result in more read-able names for the resources
in the OSM and IDF. Cases of duplicate IDs resulting from non-unique
names will be resolved by adding integers to the ends of the new IDs
that are derived from the name. (Default: False).

Returns:
A base64 string of content to be written into an Excel file. The contents
contain all tables
needed to specify room loads in TRACE 700.
"""
# check that we could successfully import openpyxl
assert openpyxl is not None, 'Export to Excel is only available in Python 3. ' \
'Either switch to using Python 3 or use the model_to_trace700_csv instead.'

# get the matrices to be written to CSV format
room_matrix, airflows_matrix, people_and_lights_matrix, misc_loads_matrix = \
model_to_trace700_matrix(
model, use_multiplier, exclude_plenums, merge_method,
si_units, geometry_names, resource_names
)

# put all of the matrices into one master Excel workbook
workbook = openpyxl.Workbook()
# add the Room table
ws = workbook.active
_add_workbook_table(ws, 'Rooms', room_matrix)
# add the Airflows table
ws = workbook.create_sheet('Airflows')
_add_workbook_table(ws, 'Airflows', airflows_matrix)
# add the People & Lighting table
ws = workbook.create_sheet('People & Lighting')
_add_workbook_table(ws, 'People & Lighting', people_and_lights_matrix)
# add the Miscellaneous Loads table
ws = workbook.create_sheet('Miscellaneous Loads')
_add_workbook_table(ws, 'Miscellaneous Loads', misc_loads_matrix)

return workbook


def _add_workbook_table(ws, title, matrix):
# define formatting to be used throughout the excel
title_font = openpyxl.styles.Font(size=16, bold=True)
bold_font = openpyxl.styles.Font(bold=True)
side = openpyxl.styles.Side(border_style='thin', color='000000')
all_border = openpyxl.styles.Border(top=side, left=side, right=side, bottom=side)
grey_fill = openpyxl.styles.PatternFill(
start_color='D3D3D3', end_color='D3D3D3', fill_type='solid'
)
row_length = len(matrix[0])
column_letter = openpyxl.utils.get_column_letter(row_length)

# add the title and create a border around the top row
ws.title = title
title_cell = ws['A1']
title_cell.value = title
title_cell.font = title_font
for col_idx, cell in enumerate(ws['A1:{}1'.format(column_letter)][0]):
border = cell.border
left = side if col_idx == 0 else border.left
right = side if col_idx == row_length - 1 else border.right
cell.border = openpyxl.styles.Border(top=side, bottom=side, left=left, right=right)
cell.fill = grey_fill

# add each row of the matrix to the sheet
for row in matrix:
ws.append(row)
new_row_idx = ws.max_row
for cell in ws[new_row_idx]:
cell.border = all_border

# auto-fit the column width of the table to the text
for col in ws.columns:
max_length = 0
column_letter = openpyxl.utils.get_column_letter(col[0].column)
for cell in col:
try:
# measure length of the cell's string representation
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except AttributeError:
pass # not a cell that sets the max dimension
# apply width
adjusted_width = (max_length + 2)
ws.column_dimensions[column_letter].width = adjusted_width

# put the first column and row in bold
for cell in ws['A']:
cell.font = bold_font
cell.fill = grey_fill
for cell in ws['2:2']:
cell.font = bold_font
cell.fill = grey_fill
title_cell.font = title_font
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
dragonfly-energy>=1.42.0
dragonfly-energy>=1.42.10
openpyxl==3.1.5
Loading