# coding=utf-8
"""Methods to write Dragonfly Models to Trane TRACE."""
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
from ladybug.datatype.temperature import Temperature
from ladybug.datatype.rvalue import RValue
from honeybee.typing import clean_and_number_string
from dragonfly.room2d import Room2D
from .airflows import airflows_trace700_matrix, AIRFLOW_TABLE_FORMAT
from .loads import people_and_lights_trace700_matrix, \
miscellaneous_loads_trace700_matrix, PEOPLE_AND_LIGHTS_TABLE_FORMAT, \
MISCELLANEOUS_LOADS_TABLE_FORMAT
# formatting for each attribute in the rooms table
ROOM_TABLE_FORMAT = (
'user',
'locked',
'locked',
'default',
'default',
'default',
'user',
'user',
'user',
'user',
'user',
'default',
'user',
'user',
'varies',
'user',
'user',
'default',
'default',
'default',
'default',
'default',
'default',
'user',
'default',
'default',
'default',
'user',
'default'
)
[docs]
def rooms_to_trace700_matrix(rooms, si_units=False):
"""Get a matrix for the "Rooms" table of the TRACE 700 Component Tree.
Args:
rooms: A list of dragonfly Room2Ds and honeybee Rooms for which the
TRACE 700 "Rooms" matrix will be returned.
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).
Returns:
A list of list where each sublist represents a row of the Rooms table
of the TRACE 700 Component Tree.
"""
# set up things for unit conversion
dist_unit = 'm' if si_units else 'ft'
r_unit = 'm2-C/W' if si_units else 'hr-ft2-F/Btu'
temp_unit = 'C' if si_units else 'F'
area, distance, temperature, r_value = Area(), Distance(), Temperature(), RValue()
# set up the names of the rows
row_names = [
'Room Description',
'Assigned to System',
'Assigned to Zone',
'Room Template',
'Thermostat Template',
'Construction Template',
'Floor Length ({})'.format(dist_unit),
'Floor Width ({})'.format(dist_unit),
'Flr to Flr Height ({})'.format(dist_unit),
'Plenum Height ({})'.format(dist_unit),
'Height Above Ground ({})'.format(dist_unit),
'Acoustic Ceiling Resistance ({})'.format(r_unit),
'Cooling Dry Bulb ({})'.format(temp_unit),
'Heating Dry Bulb ({})'.format(temp_unit),
'Relative Humidity (%)',
'Cooling Driftpoint ({})'.format(temp_unit),
'Heating Driftpoint ({})'.format(temp_unit),
'Thermostat Cooling Schedule',
'Thermostat Heating Schedule',
'Thermostat Location',
'CO2 Sensor Location',
'Humidity Moisture Capacitance',
'Humidistat Location',
'Duplicate Floor Multiplier',
'Duplicate Rooms per Zone',
'Room Mass / # of Hours',
'Slab Construction Type',
'Room Type',
'Carpeted Floor'
]
# loop through the rooms and add each of the attributes
room_mtx = []
for room in rooms:
# figure out the values for certain attributes
f2f = room.floor_to_ceiling_height if isinstance(room, Room2D) \
else room.volume / room.floor_area
plenum = room.ceiling_plenum_depth if isinstance(room, Room2D) else 0
elev = room.floor_elevation if isinstance(room, Room2D) \
else room.average_floor_height
multiplier = room.parent.multiplier \
if isinstance(room, Room2D) and room.has_parent else room.multiplier
set_pt = room.properties.energy.setpoint
if room.properties.energy.setpoint is not None:
room_type = 'Conditioned'
cool_set_pt = set_pt.cooling_setpoint
heat_set_pt = set_pt.heating_setpoint
cool_set_back = set_pt.cooling_setback
heat_set_back = set_pt.heating_setback
humid_pt = set_pt.dehumidifying_setpoint \
if set_pt.dehumidifying_setpoint is not None else '50'
else:
room_type = 'Unconditioned'
cool_set_pt = 50
heat_set_pt = 0
cool_set_back = 50
heat_set_back = 0
humid_pt = '50'
# put all attributes into a list
room_attr = [
room.display_name,
'Default System',
room.display_name,
'Default',
'Default',
'Default',
room.floor_area,
1,
f2f,
plenum,
elev,
0.31451,
cool_set_pt,
heat_set_pt,
humid_pt,
cool_set_back,
heat_set_back,
'None',
'None',
'Room',
'None',
'Medium',
'Room',
multiplier,
1,
'Time delay based on actual mass',
'4" LW Concrete',
room_type,
'Yes'
]
room_mtx.append(room_attr)
# transpose the matrix and convert SI units to IP
room_matrix = [list(row) for row in zip(*room_mtx)]
if not si_units:
room_matrix[6] = list(area.to_unit(room_matrix[6], 'ft2', 'm2'))
room_matrix[8] = list(distance.to_unit(room_matrix[8], 'ft', 'm'))
room_matrix[9] = list(distance.to_unit(room_matrix[9], 'ft', 'm'))
room_matrix[10] = list(distance.to_unit(room_matrix[10], 'ft', 'm'))
room_matrix[11] = list(r_value.to_unit(room_matrix[11], 'F-ft2-h/Btu', 'm2-K/W'))
room_matrix[12] = list(temperature.to_unit(room_matrix[12], 'F', 'C'))
room_matrix[13] = list(temperature.to_unit(room_matrix[13], 'F', 'C'))
room_matrix[15] = list(temperature.to_unit(room_matrix[15], 'F', 'C'))
room_matrix[16] = list(temperature.to_unit(room_matrix[16], 'F', 'C'))
# round the numbers so that they display nicely
for row_i in (6, 8, 9, 10, 11):
room_matrix[row_i] = [round(val, 3) for val in room_matrix[row_i]]
for row_i in (12, 13, 15, 16):
room_matrix[row_i] = [round(val) for val in room_matrix[row_i]]
# insert the column for the row names
for row_name, row in zip(row_names, room_matrix):
row.insert(0, row_name)
return room_matrix
[docs]
def model_to_trace700_matrix(
model, use_multiplier=True, exclude_plenums=True, merge_method=None,
si_units=False, geometry_names=False, resource_names=False
):
"""Get matrices with TRACE 700 simulation attributes of a Model.
The resulting matrices can be written to a CSV and then copied into
the tables that appear in the Component Tree view of TRACE 700. The
order and organization of rooms in the resulting matrix matches that
of the gbXML produced from the same model.
Args:
model: A dragonfly Model for which a TRACE 700 CSV matrix will be returned.
use_multiplier: If True, the multipliers on this Model's Stories will be
passed along to the CSV. 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 tuple with four items.
room_matrix -- A list of list where each sublist represents a row of the
Rooms table of the TRACE 700 Component Tree.
airflows_matrix -- A list of list where each sublist represents a row of the
Airflows table of the TRACE 700 Component Tree.
people_and_lights_matrix -- A list of list where each sublist represents
a row of the People & Lighting table of the TRACE 700 Component Tree.
misc_loads_matrix -- A list of list where each sublist represents a row of
the Miscellaneous Loads table of the TRACE 700 Component Tree.
"""
# convert the rooms into the format in which it will go off to TRACE
rooms_for_trace = [] # list to hold the rooms for CSV reporting
assert len(model.room_2ds) != 0 or len(model.room_3ds) != 0, \
'Model must have rooms to be able to export to TRACE700 CSV.'
# scale the model if the units are not meters
model = model.duplicate() # duplicate model to avoid mutating it
if model.units != 'Meters':
model.convert_to_units('Meters')
tol = model.tolerance
# reset the IDs to be derived from the display_names if requested
if geometry_names:
model.reset_ids()
if resource_names:
model.properties.energy.reset_resource_ids()
# account for story multipliers, separated room plenums and room merge method
merge_map = model._extract_merge_map(merge_method, exclude_plenums, tol)
for building in model.buildings:
# separate the plenums unless they are excluded
if not exclude_plenums and building.has_room_2d_plenums:
building.convert_plenum_depths_to_room_2ds(tol)
# collect all of the unmerged rooms
if use_multiplier:
for story in building.unique_stories:
rooms_for_trace.extend(story.room_2ds)
else:
for story in building.all_stories():
rooms_for_trace.extend(story.room_2ds)
# apply the merge map if it exists
if merge_map is not None:
# gather the Room objects to be merged
merge_groups, remove_i = OrderedDict(), set()
insert_i, insert_count = [], 0
for i, room in enumerate(rooms_for_trace):
try:
merge_name = merge_map[room.identifier]
try:
merge_groups[merge_name].append(room)
except KeyError: # first item in the group
merge_groups[merge_name] = [room]
insert_i.append(insert_count)
remove_i.add(i)
except KeyError: # not a room to be merged
insert_count += 1
# create the new rooms and assign them to the model
new_rooms = [r for i, r in enumerate(rooms_for_trace) if i not in remove_i]
group_ids = {}
zip_obj = zip(reversed(insert_i), reversed(merge_groups.items()))
for ins_i, (group_name, room_group) in zip_obj:
merged_rooms = Room2D.join_room_2ds(room_group, tol, tol)
for room in merged_rooms:
room.identifier = clean_and_number_string(group_name, group_ids)
room.display_name = group_name
new_rooms.insert(ins_i, room)
rooms_for_trace = new_rooms
# add the 3D Honeybee Rooms to the list
for building in model.buildings:
rooms_for_trace.extend(building.room_3ds)
# sort the rooms alphanumerically based on their identifiers
rooms_for_trace.sort(key=lambda x: x.identifier) # this matches the gbXML export
# create the matrices of data from the model rooms
room_matrix = rooms_to_trace700_matrix(rooms_for_trace, si_units)
airflows_matrix = airflows_trace700_matrix(rooms_for_trace, si_units)
people_and_lights_matrix = people_and_lights_trace700_matrix(rooms_for_trace, si_units)
misc_loads_matrix = miscellaneous_loads_trace700_matrix(rooms_for_trace, si_units)
return room_matrix, airflows_matrix, people_and_lights_matrix, misc_loads_matrix
[docs]
def model_to_trace700_csv(
model, use_multiplier=True, exclude_plenums=True, merge_method=None,
si_units=False, geometry_names=False, resource_names=False
):
"""Generate a CSV string with TRACE 700 load simulation attributes of a Model.
The resulting CSV 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 CSV matrix will be returned.
use_multiplier: If True, the multipliers on this Model's Stories will be
passed along to the CSV. 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:
Text string of content to be written into a CSV file containing all tables
needed to specify room loads in TRACE 700.
"""
# 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 matrix for CSV export
mtx_width = len(room_matrix[0]) - 1
spacer_row = ',' * mtx_width
# add the Room table
csv_matrix = ['ROOMS{}'.format(spacer_row)]
for row in room_matrix:
csv_matrix.append(','.join([str(val) for val in row]))
# add the Airflows table
csv_matrix.append(spacer_row)
csv_matrix.append(spacer_row)
csv_matrix.append('AIRFLOWS{}'.format(spacer_row))
for row in airflows_matrix:
csv_matrix.append(','.join([str(val) for val in row]))
# add the People & Lighting table
csv_matrix.append(spacer_row)
csv_matrix.append(spacer_row)
csv_matrix.append('PEOPLE & LIGHTING{}'.format(spacer_row))
for row in people_and_lights_matrix:
csv_matrix.append(','.join([str(val) for val in row]))
# add the Miscellaneous Loads table
csv_matrix.append(spacer_row)
csv_matrix.append(spacer_row)
csv_matrix.append('MISCELLANEOUS LOADS{}'.format(spacer_row))
for row in misc_loads_matrix:
csv_matrix.append(','.join([str(val) for val in row]))
return '\n'.join(csv_matrix)
[docs]
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)
_apply_table_format(ws, ROOM_TABLE_FORMAT)
# add the Airflows table
ws = workbook.create_sheet('Airflows')
_add_workbook_table(ws, 'Airflows', airflows_matrix)
_apply_table_format(ws, AIRFLOW_TABLE_FORMAT)
# add the People & Lighting table
ws = workbook.create_sheet('People & Lighting')
_add_workbook_table(ws, 'People & Lighting', people_and_lights_matrix)
_apply_table_format(ws, PEOPLE_AND_LIGHTS_TABLE_FORMAT)
# add the Miscellaneous Loads table
ws = workbook.create_sheet('Miscellaneous Loads')
_add_workbook_table(ws, 'Miscellaneous Loads', misc_loads_matrix)
_apply_table_format(ws, MISCELLANEOUS_LOADS_TABLE_FORMAT)
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
def _apply_table_format(ws, formatting):
"""Apply formatting to a worksheet to make it look like a TRACE 700 table."""
# define formatting styles to be used to make the table like TRACE
default_font = openpyxl.styles.Font(color='FF0000')
locked_fill = openpyxl.styles.PatternFill(
start_color='A9A9A9', end_color='A9A9A9', fill_type='solid'
)
locked_font = openpyxl.styles.Font(color='808080')
# loop through the rows and apply the formatting
for row, tr_format in zip(ws.iter_rows(min_row=2), formatting):
if tr_format == 'locked':
for cell in row[1:]:
cell.fill = locked_fill
cell.font = locked_font
elif tr_format == 'default':
for cell in row[1:]:
cell.font = default_font
elif tr_format == 'varies':
for cell in row[1:]:
if isinstance(cell.value, str):
cell.font = default_font
cell.value = float(cell.value)