# coding=utf-8
"""Methods to write to idf."""
import math
from datetime import datetime
import platform
from collections import OrderedDict
import xml.etree.ElementTree as ET
try:
from itertools import izip as zip # python 2
except ImportError:
xrange = range # python 3
from ladybug_geometry.util import rounding_tolerance
from ladybug_geometry.geometry3d import Vector3D, Plane, Face3D
from honeybee.typing import clean_xml_tag_string
from honeybee.room import Room
from honeybee.face import Face
from honeybee.shade import Shade
from honeybee.boundarycondition import Outdoors, Surface, Ground, boundary_conditions
from honeybee.facetype import Wall, Floor, RoofCeiling, AirBoundary
from honeybee.units import parse_distance_string, conversion_factor_to_meters
from .config import folders
from .units import convert_ventilation_flow_per_zone
"""____________IDF TRANSLATORS____________"""
[docs]
def generate_idf_string(object_type, values, comments=None):
"""Get an IDF string representation of an EnergyPlus object.
Args:
object_type: Text representing the expected start of the IDF object.
(ie. WindowMaterial:Glazing).
values: A list of values associated with the EnergyPlus object in the
order that they are supposed to be written to IDF format.
comments: A list of text comments with the same length as the values.
If None, no comments will be written into the object.
Returns:
ep_str -- Am EnergyPlus IDF string representing a single object.
"""
if comments is not None:
space_count = tuple((25 - len(str(n))) for n in values)
spaces = tuple(s_c * ' ' if s_c > 0 else ' ' for s_c in space_count)
body_str = '\n '.join('{},{}!- {}'.format(val, spc, com) for val, spc, com in
zip(values[:-1], spaces[:-1], comments[:-1]))
ep_str = '{},\n {}'.format(object_type, body_str)
if len(values) == 1: # ensure we don't have an extra line break
ep_str = ''.join(
(ep_str, '{};{}!- {}'.format(values[-1], spaces[-1], comments[-1])))
else: # include an extra line break
end_str = '\n {};{}!- {}'.format(values[-1], spaces[-1], comments[-1]) \
if comments[-1] != '' else '\n {};'.format(values[-1])
ep_str = ''.join((ep_str, end_str))
else:
body_str = '\n '.join('{},'.format(val) for val in values[:-1])
ep_str = '{},\n {}'.format(object_type, body_str)
if len(values) == 1: # ensure we don't have an extra line break
ep_str = ''.join((ep_str, '{};'.format(values[-1])))
else: # include an extra line break
ep_str = ''.join((ep_str, '\n {};'.format(values[-1])))
return ep_str
[docs]
def shade_mesh_to_idf(shade_mesh):
"""Generate an IDF string representation of a ShadeMesh.
Note that the resulting string will possess both the Shading object
as well as a ShadingProperty:Reflectance if the Shade's construction
is not in line with the EnergyPlus default of 0.2 reflectance.
Args:
shade_mesh: A honeybee ShadeMesh for which an IDF representation
will be returned.
"""
trans_sched = shade_mesh.properties.energy.transmittance_schedule.identifier if \
shade_mesh.properties.energy.transmittance_schedule is not None else ''
all_shd_str = []
for i, shade in enumerate(shade_mesh.geometry.face_vertices):
# process the geometry to get upper-left vertices
shade_face = Face3D(shade)
ul_verts = shade_face.upper_left_counter_clockwise_vertices
# create the Shading:Detailed IDF string
values = (
'{}_{}'.format(shade_mesh.identifier, i),
trans_sched,
len(ul_verts),
',\n '.join('%.3f, %.3f, %.3f' % (v.x, v.y, v.z) for v in ul_verts)
)
comments = (
'name',
'transmittance schedule',
'number of vertices',
''
)
shade_str = generate_idf_string('Shading:Building:Detailed', values, comments)
all_shd_str.append(shade_str)
# create the ShadingProperty:Reflectance if construction is not default
construction = shade_mesh.properties.energy.construction
if not construction.is_default:
values = (
shade_mesh.identifier,
construction.solar_reflectance,
construction.visible_reflectance
)
comments = (
'shading surface name',
'diffuse solar reflectance',
'diffuse visible reflectance'
)
if construction.is_specular:
values = values + (1, construction.identifier)
comments = comments + ('glazed fraction', 'glazing construction')
constr_str = generate_idf_string(
'ShadingProperty:Reflectance', values, comments)
all_shd_str.append(constr_str)
return '\n\n'.join(all_shd_str)
[docs]
def shade_to_idf(shade):
"""Generate an IDF string representation of a Shade.
Note that the resulting string will possess both the Shading object
as well as a ShadingProperty:Reflectance if the Shade's construction
is not in line with the EnergyPlus default of 0.2 reflectance.
Args:
shade: A honeybee Shade for which an IDF representation will be returned.
"""
# create the Shading:Detailed IDF string
trans_sched = shade.properties.energy.transmittance_schedule.identifier if \
shade.properties.energy.transmittance_schedule is not None else ''
ul_verts = shade.upper_left_vertices
if shade.has_parent and not isinstance(shade.parent, Room):
if isinstance(shade.parent, Face):
base_srf = shade.parent.identifier
else: # aperture or door for parent
try:
base_srf = shade.parent.parent.identifier
except AttributeError:
base_srf = 'unknown' # aperture without a parent (not simulate-able)
values = (
shade.identifier,
base_srf,
trans_sched,
len(shade.vertices),
',\n '.join('%.3f, %.3f, %.3f' % (v.x, v.y, v.z) for v in ul_verts)
)
comments = (
'name',
'base surface',
'transmittance schedule',
'number of vertices',
''
)
shade_str = generate_idf_string('Shading:Zone:Detailed', values, comments)
else: # orphaned shade
values = (
shade.identifier,
trans_sched,
len(shade.vertices),
',\n '.join('%.3f, %.3f, %.3f' % (v.x, v.y, v.z) for v in ul_verts)
)
comments = (
'name',
'transmittance schedule',
'number of vertices',
''
)
shade_str = generate_idf_string('Shading:Building:Detailed', values, comments)
# create the ShadingProperty:Reflectance IDF string if construction is not default
construction = shade.properties.energy.construction
if construction.is_default:
return shade_str
else:
values = (
shade.identifier,
construction.solar_reflectance,
construction.visible_reflectance
)
comments = (
'shading surface name',
'diffuse solar reflectance',
'diffuse visible reflectance'
)
if construction.is_specular:
values = values + (1, construction.identifier)
comments = comments + ('glazed fraction of surface', 'glazing construction')
constr_str = generate_idf_string('ShadingProperty:Reflectance', values, comments)
return '\n\n'.join((shade_str, constr_str))
[docs]
def door_to_idf(door):
"""Generate an IDF string representation of a Door.
Note that the resulting string does not include full construction definitions
but it will include a WindowShadingControl definition if a WindowConstructionShade
is assigned to the door. It will also include a ventilation object if the door
has a VentilationOpening object assigned to it.
Also note that shades assigned to the Door are not included in the resulting
string. To write these objects into a final string, you must loop through the
Door.shades, and call the to.idf method on each one.
If the input door is orphaned, the resulting string will possess both the
Shading object as well as a ShadingProperty:Reflectance that aligns with the
Doors's exterior construction properties. However, a transmittance schedule
that matches the transmittance of the window construction will only be
referenced and not included in the resulting string. All transmittance schedules
follow the format of 'Constant %.3f Transmittance'.
Args:
door: A honeybee Door for which an IDF representation will be returned.
"""
# IF ORPHANED: write the door as a shade
if not door.has_parent:
# create the Shading:Detailed IDF string
cns = door.properties.energy.construction
trans_sch = 'Constant %.3f Transmittance' % cns.solar_transmittance \
if door.is_glass else ''
verts = door.upper_left_vertices
verts_str = ',\n '.join('%.3f, %.3f, %.3f' % (v.x, v.y, v.z) for v in verts)
values = (door.identifier, trans_sch, len(verts), verts_str)
comments = ('name', 'transmittance schedule', 'number of vertices', '')
shade_str = generate_idf_string('Shading:Building:Detailed', values, comments)
# create the ShadingProperty:Reflectance
comments = (
'shade surface name', 'diffuse solar reflectance', 'diffuse visible reflectance')
if door.is_glass:
values = (door.identifier, 0.2, 0.2, 1, cns.identifier)
comments = comments + ('glazed fraction of surface', 'glazing construction')
else:
values = (door.identifier, cns.outside_solar_reflectance,
cns.outside_visible_reflectance)
constr_str = generate_idf_string('ShadingProperty:Reflectance', values, comments)
return '\n\n'.join((shade_str, constr_str))
# IF CHILD: write the door as a fenestration surface
# set defaults for missing fields
door_bc_obj = door.boundary_condition.boundary_condition_object if \
isinstance(door.boundary_condition, Surface) else ''
construction = door.properties.energy.construction
frame_name = construction.frame.identifier if construction.has_frame else ''
if construction.has_shade:
constr_name = construction.window_construction.identifier
elif construction.is_dynamic:
constr_name = '{}State0'.format(construction.constructions[0].identifier)
else:
constr_name = construction.identifier
if door.has_parent:
parent_face = door.parent.identifier
parent_room = door.parent.parent.identifier if door.parent.has_parent \
else 'unknown'
else:
parent_room = parent_face = 'unknown'
# create the fenestration surface string
ul_verts = door.upper_left_vertices
values = (
door.identifier,
'Door' if not door.is_glass else 'GlassDoor',
constr_name,
parent_face,
door_bc_obj,
door.boundary_condition.view_factor,
frame_name,
'1',
len(door.vertices),
',\n '.join('%.3f, %.3f, %.3f' % (v.x, v.y, v.z) for v in ul_verts)
)
comments = (
'name',
'surface type',
'construction name',
'building surface name',
'boundary condition object',
'view factor to ground',
'frame and divider name',
'multiplier',
'number of vertices',
''
)
fen_str = generate_idf_string('FenestrationSurface:Detailed', values, comments)
# create the WindowShadingControl object if it is needed
if construction.has_shade:
shd_prop_str = construction.to_shading_control_idf(door.identifier, parent_room)
fen_str = '\n\n'.join((fen_str, shd_prop_str))
# create the VentilationOpening object if it is needed
if door.properties.energy.vent_opening is not None:
try:
vent_str = door.properties.energy.vent_opening.to_idf()
fen_str = '\n\n'.join((fen_str, vent_str))
except AssertionError: # door does not have a parent room
pass
return fen_str
[docs]
def aperture_to_idf(aperture):
"""Generate an IDF string representation of an Aperture.
Note that the resulting string does not include full construction definitions
but it will include a WindowShadingControl definition if a WindowConstructionShade
is assigned to the aperture. It will also include a ventilation object if the
aperture has a VentilationOpening object assigned to it.
Also note that shades assigned to the Aperture are not included in the resulting
string. To write these objects into a final string, you must loop through the
Aperture.shades, and call the to.idf method on each one.
If the input aperture is orphaned, the resulting string will possess both the
Shading object as well as a ShadingProperty:Reflectance that aligns with the
Aperture's exterior construction properties. However, a transmittance schedule
that matches the transmittance of the window construction will only be
referenced and not included in the resulting string. All transmittance schedules
follow the format of 'Constant %.3f Transmittance'.
Args:
aperture: A honeybee Aperture for which an IDF representation will be returned.
"""
# IF ORPHANED: write the aperture as a shade
if not aperture.has_parent:
# create the Shading:Detailed IDF string
cns = aperture.properties.energy.construction
trans_sch = 'Constant %.3f Transmittance' % cns.solar_transmittance
verts = aperture.upper_left_vertices
verts_str = ',\n '.join('%.3f, %.3f, %.3f' % (v.x, v.y, v.z) for v in verts)
values = (aperture.identifier, trans_sch, len(verts), verts_str)
comments = ('name', 'transmittance schedule', 'number of vertices', '')
shade_str = generate_idf_string('Shading:Building:Detailed', values, comments)
# create the ShadingProperty:Reflectance
values = (aperture.identifier, 0.2, 0.2, 1, cns.identifier)
comments = (
'shade surface name', 'diffuse solar reflectance', 'diffuse visible reflectance',
'glazed fraction of surface', 'glazing construction'
)
constr_str = generate_idf_string('ShadingProperty:Reflectance', values, comments)
return '\n\n'.join((shade_str, constr_str))
# IF CHILD: write the aperture as a fenestration surface
# set defaults for missing fields
ap_bc_obj = aperture.boundary_condition.boundary_condition_object if \
isinstance(aperture.boundary_condition, Surface) else ''
construction = aperture.properties.energy.construction
frame_name = construction.frame.identifier if construction.has_frame else ''
if construction.has_shade:
constr_name = construction.window_construction.identifier
elif construction.is_dynamic:
constr_name = '{}State0'.format(construction.constructions[0].identifier)
else:
constr_name = construction.identifier
if aperture.has_parent:
parent_face = aperture.parent.identifier
parent_room = aperture.parent.parent.identifier if aperture.parent.has_parent \
else 'unknown'
else:
parent_room = parent_face = 'unknown'
# create the fenestration surface string
ul_verts = aperture.upper_left_vertices
values = (
aperture.identifier,
'Window',
constr_name,
parent_face,
ap_bc_obj,
aperture.boundary_condition.view_factor,
frame_name,
'1',
len(aperture.vertices),
',\n '.join('%.3f, %.3f, %.3f' % (v.x, v.y, v.z) for v in ul_verts)
)
comments = (
'name',
'surface type',
'construction name',
'building surface name',
'boundary condition object',
'view factor to ground',
'frame and divider name',
'multiplier',
'number of vertices',
''
)
fen_str = generate_idf_string('FenestrationSurface:Detailed', values, comments)
# create the WindowShadingControl object if it is needed
if construction.has_shade:
shd_prop_str = construction.to_shading_control_idf(
aperture.identifier, parent_room)
fen_str = '\n\n'.join((fen_str, shd_prop_str))
# create the VentilationOpening object if it is needed
if aperture.properties.energy.vent_opening is not None:
try:
vent_str = aperture.properties.energy.vent_opening.to_idf()
fen_str = '\n\n'.join((fen_str, vent_str))
except AssertionError: # aperture does not have a parent room
pass
return fen_str
[docs]
def face_to_idf(face):
"""Generate an IDF string representation of a Face.
Note that the resulting string does not include full construction definitions.
Also note that this does not include any of the shades assigned to the Face
in the resulting string. Nor does it include the strings for the
apertures or doors. To write these objects into a final string, you must
loop through the Face.shades, Face.apertures, and Face.doors and call the
to.idf method on each one.
If the input face is orphaned, the resulting string will possess both the
Shading object as well as a ShadingProperty:Reflectance that aligns with
the Face's exterior construction properties. Furthermore, any child
apertures of doors in the face will also be included as shading geometries.
Args:
face: A honeybee Face for which an IDF representation will be returned.
"""
# IF ORPHANED: write the face as a shade
if not face.has_parent:
# create the Shading:Detailed IDF string
verts = face.punched_geometry.upper_left_counter_clockwise_vertices
verts_str = ',\n '.join('%.3f, %.3f, %.3f' % (v.x, v.y, v.z) for v in verts)
values = (face.identifier, '', len(verts), verts_str)
comments = ('name', 'transmittance schedule', 'number of vertices', '')
shade_str = generate_idf_string('Shading:Building:Detailed', values, comments)
# create the ShadingProperty:Reflectance IDF string
cns = face.properties.energy.construction
values = (
face.identifier, cns.outside_solar_reflectance, cns.outside_visible_reflectance)
comments = (
'shade surface name', 'diffuse solar reflectance', 'diffuse visible reflectance')
constr_str = generate_idf_string('ShadingProperty:Reflectance', values, comments)
# translate any child apertures or doors
face_str = [shade_str, constr_str]
for ap in face.apertures:
ap._parent = None # remove parent to translate as orphaned
face_str.append(aperture_to_idf(ap))
ap._parent = face # put back the parent
for dr in face.doors:
dr._parent = None # remove parent to translate as orphaned
face_str.append(door_to_idf(dr))
dr._parent = face # put back the parent
return '\n\n'.join(face_str)
# IF CHILD: write the aperture as a fenestration surface
# select the correct face type
if isinstance(face.type, AirBoundary):
face_type = 'Wall' # air boundaries are not a Surface type in EnergyPlus
elif isinstance(face.type, RoofCeiling):
if face.altitude < 0:
face_type = 'Wall' # ensure E+ does not try to flip the Face
elif isinstance(face.boundary_condition, (Outdoors, Ground)):
face_type = 'Roof' # E+ distinguishes between Roof and Ceiling
else:
face_type = 'Ceiling'
elif isinstance(face.type, Floor) and face.altitude > 0:
face_type = 'Wall' # ensure E+ does not try to flip the Face
else:
face_type = face.type.name
# select the correct boundary condition
bc_name, append_txt = face.boundary_condition.name, None
if isinstance(face.boundary_condition, Surface):
face_bc_obj = face.boundary_condition.boundary_condition_object
elif face.boundary_condition.name == 'OtherSideTemperature':
face_bc_obj = '{}_OtherTemp'.format(face.identifier)
append_txt = face.boundary_condition.to_idf(face_bc_obj)
bc_name = 'OtherSideCoefficients'
else:
face_bc_obj = ''
# process the geometry correctly if it has holes
ul_verts = face.upper_left_vertices
if face.geometry.has_holes and isinstance(face.boundary_condition, Surface):
# check if the first vertex is the upper-left vertex
pt1, found_i = ul_verts[0], False
for pt in ul_verts[1:]:
if pt == pt1:
found_i = True
break
if found_i: # reorder the vertices to have boundary first
ul_verts = reversed(ul_verts)
# assemble the values and the comments
if face.has_parent:
if face.parent.identifier == face.parent.zone:
zone_name, space_name = face.parent.zone, ''
else:
zone_name, space_name = face.parent.zone, face.parent.identifier
else:
zone_name, space_name = 'unknown', ''
values = (
face.identifier,
face_type,
face.properties.energy.construction.identifier,
zone_name,
space_name,
bc_name,
face_bc_obj,
face.boundary_condition.sun_exposure_idf,
face.boundary_condition.wind_exposure_idf,
face.boundary_condition.view_factor,
len(face.vertices),
',\n '.join('%.3f, %.3f, %.3f' % (v.x, v.y, v.z) for v in ul_verts)
)
comments = (
'name',
'surface type',
'construction name',
'zone name',
'space name',
'boundary condition',
'boundary condition object',
'sun exposure',
'wind exposure',
'view factor to ground',
'number of vertices',
''
)
face_idf = generate_idf_string('BuildingSurface:Detailed', values, comments)
return face_idf if not append_txt else face_idf + append_txt
[docs]
def room_to_idf(room):
"""Generate an IDF string representation of a Room.
The resulting string will include all internal gain definitions for the Room
(people, lights, equipment, process) and the infiltration definition. It will
also include internal masses, ventilation fans, and daylight controls. However,
complete schedule definitions assigned to these load objects are excluded.
If the room's zone name is the same as the room identifier, the resulting IDF
string will be for an EnergyPlus Zone and it will include ventilation
requirements and thermostat objects. Otherwise, the IDF string will be for
a Space with ventilation and thermostats excluded (with the assumption
that these objects are to be written separately with the parent Zone).
The Room's HVAC is always excluded in the string returned from this method
regardless of whether the room represents an entire zone or an individual
space within a larger zone.
Also note that this method does not write any of the geometry of the Room
into the resulting string. To represent the Room geometry, you must loop
through the Room.shades and Room.faces and call the to.idf method on
each one. Note that you will likely also need to call to.idf on the
apertures, doors and shades of each face as well as the shades on each
aperture.
Args:
room: A honeybee Room for which an IDF representation will be returned.
"""
# clean the room name so that it can be written into a comment
clean_name = room.display_name.replace('\n', '')
if room.identifier == room.zone: # write the zone definition
is_zone = True
room_str = ['!- ________ZONE:{}________\n'.format(clean_name)]
ceil_height = room.geometry.max.z - room.geometry.min.z
include_floor = 'No' if room.exclude_floor_area else 'Yes'
zone_values = (room.identifier, '', '', '', '', '', room.multiplier,
ceil_height, room.volume, room.floor_area, '', '', include_floor)
zone_comments = ('name', 'north', 'x', 'y', 'z', 'type', 'multiplier',
'ceiling height', 'volume', 'floor area', 'inside convection',
'outside convection', 'include floor area')
room_str.append(generate_idf_string('Zone', zone_values, zone_comments))
else: # write the space definition
is_zone = False
room_str = ['!- ________SPACE:{}________\n'.format(clean_name)]
ceil_height = room.geometry.max.z - room.geometry.min.z
space_values = (room.identifier, room.zone,
ceil_height, room.volume, room.floor_area)
space_comments = ('name', 'zone name', 'ceiling height', 'volume', 'floor area')
room_str.append(generate_idf_string('Space', space_values, space_comments))
# write the load definitions
people = room.properties.energy.people
lighting = room.properties.energy.lighting
electric_equipment = room.properties.energy.electric_equipment
gas_equipment = room.properties.energy.gas_equipment
shw = room.properties.energy.service_hot_water
infiltration = room.properties.energy.infiltration
ventilation = room.properties.energy.ventilation
if people is not None:
room_str.append(people.to_idf(room.identifier))
if lighting is not None:
room_str.append(lighting.to_idf(room.identifier))
if electric_equipment is not None:
room_str.append(electric_equipment.to_idf(room.identifier))
if gas_equipment is not None:
room_str.append(gas_equipment.to_idf(room.identifier))
if shw is not None:
shw_str, shw_sch = shw.to_idf(room)
room_str.append(shw_str)
room_str.extend(shw_sch)
if infiltration is not None:
room_str.append(infiltration.to_idf(room.identifier))
# write the ventilation and thermostat
if is_zone:
if ventilation is not None:
room_str.append(ventilation.to_idf(room.identifier))
if room.properties.energy.is_conditioned and \
room.properties.energy.setpoint is not None:
room_str.append(room.properties.energy.setpoint.to_idf(room.identifier))
# write any ventilation fan definitions
for fan in room.properties.energy._fans:
room_str.append(fan.to_idf(room.identifier))
# write the daylighting control
if room.properties.energy.daylighting_control is not None:
room_str.extend(room.properties.energy.daylighting_control.to_idf())
# write any process load definitions
for p_load in room.properties.energy._process_loads:
room_str.append(p_load.to_idf(room.identifier))
# write any internal mass definitions
for int_mass in room.properties.energy._internal_masses:
room_str.append(int_mass.to_idf(room.identifier, is_zone))
return '\n\n'.join(room_str)
[docs]
def model_to_idf(
model, schedule_directory=None, use_ideal_air_equivalent=True,
patch_missing_adjacencies=False, timestep=6
):
r"""Generate an IDF string representation of a Model.
The resulting string will include all geometry (Rooms, Faces, Shades, Apertures,
Doors), all fully-detailed constructions + materials, all fully-detailed
schedules, and the room properties (loads, thermostats with setpoints, and HVAC).
Essentially, the string includes everything needed to simulate the model
except the simulation parameters. So joining this string with the output of
SimulationParameter.to_idf() should create a simulate-able IDF.
Args:
model: A honeybee Model for which an IDF representation will be returned.
schedule_directory: An optional file directory to which all file-based
schedules should be written to. If None, all ScheduleFixedIntervals
will be translated to Schedule:Compact and written fully into the
IDF string instead of to Schedule:File. (Default: None).
use_ideal_air_equivalent: Boolean to note whether any detailed HVAC system
templates should be converted to an equivalent IdealAirSystem upon export.
If False and the Model contains detailed systems, a ValueError will
be raised since this method does not support the translation of
detailed systems. (Default:True).
patch_missing_adjacencies: Boolean to note whether any missing adjacencies
in the model should be replaced with Adiabatic boundary conditions.
This is useful when the input model is only a portion of a much
larger model. (Default: False).
timestep: An integer for the simulation timestep, which will be
used to balance air boundary flows to ensure that there is never
more air than the room volume mixed at a given simulation timestep.
If None, no balancing of air boundary flows wil occur. (Default: 6).
Usage:
.. code-block:: python
import os
from ladybug.futil import write_to_file
from honeybee.model import Model
from honeybee.room import Room
from honeybee.config import folders
from honeybee_energy.lib.programtypes import office_program
from honeybee_energy.hvac.idealair import IdealAirSystem
from honeybee_energy.simulation.parameter import SimulationParameter
# Get input Model
room = Room.from_box('Tiny House Zone', 5, 10, 3)
room.properties.energy.program_type = office_program
room.properties.energy.add_default_ideal_air()
model = Model('Tiny House', [room])
# Get the input SimulationParameter
sim_par = SimulationParameter()
sim_par.output.add_zone_energy_use()
ddy_file = 'C:/EnergyPlusV9-0-1/WeatherData/USA_CO_Golden-NREL.724666_TMY3.ddy'
sim_par.sizing_parameter.add_from_ddy_996_004(ddy_file)
# create the IDF string for simulation parameters and model
idf_str = '\n\n'.join((sim_par.to_idf(), model.to.idf(model)))
# write the final string into an IDF
idf = os.path.join(folders.default_simulation_folder, 'test_file', 'in.idf')
write_to_file(idf, idf_str, True)
"""
# duplicate model to avoid mutating it as we edit it for energy simulation
original_model = model
model = model.duplicate()
# scale the model if the units are not meters
if model.units != 'Meters':
model.convert_to_units('Meters')
# remove degenerate geometry within native E+ tolerance of 0.01 meters
try:
model.remove_degenerate_geometry(0.01)
except ValueError:
error = 'Failed to remove degenerate Rooms.\nYour Model units system is: {}. ' \
'Is this correct?'.format(original_model.units)
raise ValueError(error)
# convert model to simple ventilation and Ideal Air Systems
model.properties.energy.ventilation_simulation_control.vent_control_type = \
'SingleZone'
if use_ideal_air_equivalent:
for room in model.rooms:
room.properties.energy.assign_ideal_air_equivalent()
# balance the air boundary flows if there is a timestep
if timestep is not None:
model.properties.energy.balance_air_boundary_flows(timestep)
# patch missing adjacencies
if patch_missing_adjacencies:
model.properties.energy.missing_adjacencies_to_adiabatic()
# resolve the properties across zones
single_zones, zone_dict = model.properties.energy.resolve_zones()
# write the building object into the string
model_str = ['!- =======================================\n'
'!- ================ MODEL ================\n'
'!- =======================================\n']
# write all of the schedules and type limits
sched_strs = []
type_limits = []
used_day_sched_ids, used_day_count = {}, 1
always_on_included = False
all_scheds = model.properties.energy.schedules + \
model.properties.energy.orphaned_trans_schedules
for sched in all_scheds:
if sched.identifier == 'Always On':
always_on_included = True
try: # ScheduleRuleset
year_schedule, week_schedules = sched.to_idf()
if week_schedules is None: # ScheduleConstant
sched_strs.append(year_schedule)
else: # ScheduleYear
# check that day schedules aren't referenced by other model schedules
day_scheds = []
for day in sched.day_schedules:
if day.identifier not in used_day_sched_ids:
day_scheds.append(day.to_idf(sched.schedule_type_limit))
used_day_sched_ids[day.identifier] = day
elif day != used_day_sched_ids[day.identifier]:
new_day = day.duplicate()
new_day.identifier = 'Schedule Day {}'.format(used_day_count)
day_scheds.append(new_day.to_idf(sched.schedule_type_limit))
for i, week_sch in enumerate(week_schedules):
week_schedules[i] = \
week_sch.replace(day.identifier, new_day.identifier)
used_day_count += 1
sched_strs.extend([year_schedule] + week_schedules + day_scheds)
except TypeError: # ScheduleFixedInterval
if schedule_directory is None:
sched_strs.append(sched.to_idf_compact())
else:
sched_strs.append(sched.to_idf(schedule_directory))
t_lim = sched.schedule_type_limit
if t_lim is not None and not _instance_in_array(t_lim, type_limits):
type_limits.append(t_lim)
if not always_on_included:
always_schedule, _ = model.properties.energy._always_on_schedule().to_idf()
sched_strs.append(always_schedule)
model_str.append('!- ========= SCHEDULE TYPE LIMITS =========\n')
model_str.extend([type_limit.to_idf() for type_limit in set(type_limits)])
model_str.append('!- ============== SCHEDULES ==============\n')
model_str.extend(sched_strs)
# get the default generic construction set
# must be imported here to avoid circular imports
from .lib.constructionsets import generic_construction_set
# write all of the materials and constructions
materials = []
construction_strs = []
dynamic_cons = []
all_constrs = model.properties.energy.constructions + \
generic_construction_set.constructions_unique
for constr in set(all_constrs):
try:
materials.extend(constr.materials)
construction_strs.append(constr.to_idf())
if constr.has_frame:
materials.append(constr.frame)
if constr.has_shade:
if constr.window_construction in all_constrs:
construction_strs.pop(-1) # avoid duplicate specification
if constr.is_switchable_glazing:
materials.append(constr.switched_glass_material)
if constr.shade_location == 'Between': # write the un-split gap
gap_layer = constr.window_construction.materials[1]
materials.append(gap_layer)
construction_strs.append(constr.to_shaded_idf())
elif constr.is_dynamic:
dynamic_cons.append(constr)
except AttributeError:
try: # AirBoundaryConstruction or ShadeConstruction
construction_strs.append(constr.to_idf()) # AirBoundaryConstruction
except TypeError:
pass # ShadeConstruction; no need to write it
model_str.append('!- ============== MATERIALS ==============\n')
model_str.extend([mat.to_idf() for mat in set(materials)])
model_str.append('!- ============ CONSTRUCTIONS ============\n')
model_str.extend(construction_strs)
# write all of the HVAC systems for zones
model_str.append('!- ============ HVAC SYSTEMS ============\n')
for zone_id, zone_data in zone_dict.items():
rooms, z_prop, set_pt, vent = zone_data
mult, ceil_hgt, vol, flr_area, inc_flr = z_prop
model_str.append('!- ________ZONE:{}________\n'.format(zone_id))
zone_values = (zone_id, '', '', '', '', '', mult,
ceil_hgt, vol, flr_area, '', '', inc_flr)
zone_comments = ('name', 'north', 'x', 'y', 'z', 'type', 'multiplier',
'ceiling height', 'volume', 'floor area', 'inside convection',
'outside convection', 'include floor area')
model_str.append(generate_idf_string('Zone', zone_values, zone_comments))
if vent is not None:
model_str.append(vent.to_idf(zone_id))
hvacs = [r.properties.energy.hvac for r in rooms
if r.properties.energy.hvac is not None]
if set_pt is not None and len(hvacs) != 0:
model_str.append(set_pt.to_idf(zone_id))
try:
model_str.append(hvacs[0].to_idf_zone(zone_id, set_pt, vent))
except AttributeError:
raise TypeError(
'HVAC system type "{}" does not support direct translation to IDF.\n'
'Use the export to OpenStudio workflow instead.'.format(
room.properties.energy.hvac.__class__.__name__))
# write all of the HVAC systems for individual rooms not using zones
for room in single_zones:
if room.properties.energy.hvac is not None \
and room.properties.energy.setpoint is not None:
try:
model_str.append(room.properties.energy.hvac.to_idf(room))
except AttributeError:
raise TypeError(
'HVAC system type "{}" does not support direct translation to IDF.\n'
'Use the export to OpenStudio workflow instead.'.format(
room.properties.energy.hvac.__class__.__name__))
# get the default air boundary construction
# must be imported here to avoid circular imports
from .lib.constructions import air_boundary
# write all of the room geometry
model_str.append('!- ============ ROOM GEOMETRY ============\n')
sf_objs = []
found_ab = []
for room in model.rooms:
model_str.append(room.to.idf(room))
for face in room.faces:
model_str.append(face.to.idf(face))
if isinstance(face.type, AirBoundary): # write the air mixing objects
air_constr = face.properties.energy.construction
try:
if face.identifier not in found_ab:
adj_face = face.boundary_condition.boundary_condition_object
adj_room = face.boundary_condition.boundary_condition_objects[-1]
try:
model_str.append(
air_constr.to_cross_mixing_idf(face, adj_room))
except AttributeError: # opaque construction for air boundary
model_str.append(
air_boundary.to_cross_mixing_idf(face, adj_room))
found_ab.append(adj_face)
except AttributeError as e:
raise ValueError(
'Face "{}" is an Air Boundary but lacks a Surface boundary '
'condition.\n{}'.format(face.full_id, e))
for ap in face.apertures:
if len(ap.geometry) <= 4: # ignore apertures to be triangulated
model_str.append(ap.to.idf(ap))
sf_objs.append(ap)
for shade in ap.outdoor_shades:
model_str.append(shade.to.idf(shade))
for dr in face.doors:
if len(dr.geometry) <= 4: # ignore doors to be triangulated
model_str.append(dr.to.idf(dr))
sf_objs.append(dr)
for shade in dr.outdoor_shades:
model_str.append(shade.to.idf(shade))
for shade in face.outdoor_shades:
model_str.append(shade.to.idf(shade))
for shade in room.outdoor_shades:
model_str.append(shade.to.idf(shade))
# triangulate any apertures or doors with more than 4 vertices
tri_apertures, _ = model.triangulated_apertures()
for tri_aps in tri_apertures:
for i, ap in enumerate(tri_aps):
if i != 0:
ap.properties.energy.vent_opening = None
model_str.append(ap.to.idf(ap))
sf_objs.append(ap)
tri_doors, _ = model.triangulated_doors()
for tri_drs in tri_doors:
for i, dr in enumerate(tri_drs):
if i != 0:
dr.properties.energy.vent_opening = None
model_str.append(dr.to.idf(dr))
sf_objs.append(dr)
# write all context shade geometry
model_str.append('!- ========== CONTEXT GEOMETRY ==========\n')
pv_objects = []
for shade in model.orphaned_shades:
model_str.append(shade.to.idf(shade))
if shade.properties.energy.pv_properties is not None:
pv_objects.append(shade)
for shade_mesh in model.shade_meshes:
model_str.append(shade_mesh.to.idf(shade_mesh))
for face in model.orphaned_faces:
model_str.append(face_to_idf(face))
for ap in model.orphaned_apertures:
model_str.append(aperture_to_idf(ap))
for dr in model.orphaned_doors:
model_str.append(door_to_idf(dr))
# write any EMS programs for dynamic constructions
if len(dynamic_cons) != 0:
model_str.append('!- ========== EMS PROGRAMS ==========\n')
dyn_dict = {}
for sf in sf_objs:
con = sf.properties.energy.construction
try:
dyn_dict[con.identifier].append(sf.identifier)
except KeyError:
dyn_dict[con.identifier] = [sf.identifier]
for con in dynamic_cons:
model_str.append(con.to_program_idf(dyn_dict[con.identifier]))
model_str.append(dynamic_cons[0].idf_program_manager(dynamic_cons))
# write any generator objects that were discovered in the model
if len(pv_objects) != 0:
model_str.append('!- ========== PHOTOVOLTAIC GENERATORS ==========\n')
for shade in pv_objects:
model_str.append(shade.properties.energy.pv_properties.to_idf(shade))
model_str.extend(model.properties.energy.electric_load_center.to_idf(pv_objects))
return '\n\n'.join(model_str)
[docs]
def energyplus_idf_version(version_array=None):
"""Get IDF text for the version of EnergyPlus.
This will match the version of EnergyPlus found in the config if it it exists.
It will be None otherwise.
Args:
version_array: An array of up to 3 integers for the version of EnergyPlus
for which an IDF string should be generated. If None, the energyplus_version
from the config will be used if it exists.
"""
if version_array:
ver_str = '.'.join((str(d) for d in version_array))
return generate_idf_string('Version', [ver_str], ['version identifier'])
elif folders.energyplus_version:
ver_str = '.'.join((str(d) for d in folders.energyplus_version))
return generate_idf_string('Version', [ver_str], ['version identifier'])
return None
def _instance_in_array(object_instance, object_array):
"""Check if a specific object instance is already in an array.
This can be much faster than `if object_instance in object_array`
when you expect to be testing a lot of the same instance of an object for
inclusion in an array since the builtin method uses an == operator to
test inclusion.
"""
for val in object_array:
if val is object_instance:
return True
return False
"""___________gbXML TRANSLATORS___________"""
[docs]
def face_3d_to_gbxml_element(
face_3d, tolerance=0.001, rect_geo_format='BoundingRectangle', explicit_holes=False,
parent_element=None, rect_origin=None
):
"""Get gbXML PlanarGeometry and RectangularGeometry Elements from a Face3D.
Args:
face_3d: A ladybug-geometry Face3D for which gbXML PlanarGeometry and
RectangularGeometry Elements will be returned.
tolerance: The minimum difference in coordinate values below which
vertices are considered to be identical. (Default: 0.001, suitable
for objects in Meters or Feet).
rect_geo_format: Text string to note how the rectangular geometry for
all Surfaces is written into the gbXML. BoundingRectangle sets the
width and height of the rectangular geometry using the bounding
rectangle around the geometry, which results in an overestimated
area for non-rectangular geo. SimpleArea will set the rectangle width
always equal to geometry area and the height always equal to one,
ensuring accurate areas and making it easy to check the geometry
area in the gbXML. SimpleAreaForNonRectOnly will report the width and
height of rectangular Face3D correctly but use simpler areas
for non-rectangular geometry. (Default: BoundingRectangle). Choose
from the following.
* BoundingRectangle
* SimpleArea
* SimpleAreaForNonRectOnly
explicit_holes: Boolean to note whether holes in the Face3D should be
represented explicitly with their own PolyLoop or the hole and boundary
should be collapsed into a single PolyLoop that winds inwards to
cut out the holes. (Default: False).
parent_element: An optional XML Element for the Surface or Opening XML
Element to which the geometry will be added. If None, a new XML Element
will be generated. (Default: None).
rect_origin: An optional Point3D to set the origin of the rectangular
geometry. This is used for sub faces, which need to use the origin
of the parent Face. If None, the Face3D's lower left corner will
be used. (Default: None).
"""
# create the PlanarGeometry and RectangularGeometry elements
if parent_element is not None:
xml_rect_geo = ET.SubElement(parent_element, 'RectangularGeometry')
xml_plane_geo = ET.SubElement(parent_element, 'PlanarGeometry')
else:
xml_rect_geo = ET.Element('RectangularGeometry')
xml_plane_geo = ET.Element('PlanarGeometry')
decimal_count, _ = rounding_tolerance(tolerance)
# extract all of the rectangular geometry properties
rel_plane = face_3d.plane
llc = face_3d.lower_left_corner
urc = face_3d.upper_right_corner
origin = llc if rect_origin is None else rect_origin
if face_3d.is_horizontal(tolerance): # horizontal; adjust azimuth
proj_x = Vector3D(1, 0, 0)
tilt, azimuth = 0, 0
else: # vertical or tilted
proj_y = Vector3D(0, 0, 1).project(rel_plane.n)
proj_x = proj_y.rotate(rel_plane.n, math.pi / -2)
tilt = math.degrees(face_3d.tilt)
azimuth = math.degrees(face_3d.azimuth)
ref_plane = Plane(rel_plane.n, origin, proj_x)
min_2d = ref_plane.xyz_to_xy(llc)
max_2d = ref_plane.xyz_to_xy(urc)
if rect_geo_format == 'BoundingRectangle':
width = round(max_2d.x - min_2d.x, decimal_count)
height = round(max_2d.y - min_2d.y, decimal_count)
elif rect_geo_format == 'SimpleArea':
width = round(face_3d.area, decimal_count)
height = 1
else:
ang_tol = math.radians(1)
if face_3d.polygon2d.is_rectangle(ang_tol):
width = round(max_2d.x - min_2d.x, decimal_count)
height = round(max_2d.y - min_2d.y, decimal_count)
else:
width = round(face_3d.area, decimal_count)
height = 1
origin_coords = origin if rect_origin is None else min_2d
# add the rectangular geometry properties
xml_origin = ET.SubElement(xml_rect_geo, 'CartesianPoint')
for coord in origin_coords:
xml_coord = ET.SubElement(xml_origin, 'Coordinate')
xml_coord.text = str(round(coord, decimal_count))
if rect_origin is None:
xml_azimuth = ET.SubElement(xml_rect_geo, 'Azimuth')
xml_azimuth.text = str(round(azimuth))
xml_tilt = ET.SubElement(xml_rect_geo, 'Tilt')
xml_tilt.text = str(round(tilt))
xml_width = ET.SubElement(xml_rect_geo, 'Width')
xml_width.text = str(round(width, decimal_count))
xml_height = ET.SubElement(xml_rect_geo, 'Height')
xml_height.text = str(round(height, decimal_count))
# add the 3D vertices to PlanarGeometry
if explicit_holes and face_3d.has_holes:
xml_poly = ET.SubElement(xml_plane_geo, 'PolyLoop')
for pt in face_3d.boundary:
xml_pt = ET.SubElement(xml_poly, 'CartesianPoint')
for coord in pt:
xml_coord = ET.SubElement(xml_pt, 'Coordinate')
xml_coord.text = str(round(coord, decimal_count))
for hole in face_3d.holes:
xml_poly = ET.SubElement(xml_plane_geo, 'PolyLoop')
for pt in hole:
xml_pt = ET.SubElement(xml_poly, 'CartesianPoint')
for coord in pt:
xml_coord = ET.SubElement(xml_pt, 'Coordinate')
xml_coord.text = str(round(coord, decimal_count))
else: # write all vertices into one poly loop
xml_poly = ET.SubElement(xml_plane_geo, 'PolyLoop')
for pt in face_3d.vertices:
xml_pt = ET.SubElement(xml_poly, 'CartesianPoint')
for coord in pt:
xml_coord = ET.SubElement(xml_pt, 'Coordinate')
xml_coord.text = str(round(coord, decimal_count))
return xml_rect_geo, xml_plane_geo
[docs]
def shade_to_gbxml_element(
shade, tolerance=0.001, rect_geo_format='BoundingRectangle', explicit_holes=False,
campus_element=None
):
"""Get a gbXML Surface Element from a honeybee Shade.
Args:
shade: A honeybee Shade for which an gbXML Surface Element will
be returned.
tolerance: The minimum difference in coordinate values below which
vertices are considered to be identical. (Default: 0.001, suitable
for objects in Meters or Feet).
rect_geo_format: Text string to note how the rectangular geometry for
all Surfaces is written into the gbXML. BoundingRectangle sets the
width and height of the rectangular geometry using the bounding
rectangle around the geometry, which results in an overestimated
area for non-rectangular geo. SimpleArea will set the rectangle width
always equal to geometry area and the height always equal to one,
ensuring accurate areas and making it easy to check the geometry
area in the gbXML. SimpleAreaForNonRectOnly will report the width and
height of rectangular Face3D correctly but use simpler areas
for non-rectangular geometry. (Default: BoundingRectangle). Choose
from the following.
* BoundingRectangle
* SimpleArea
* SimpleAreaForNonRectOnly
explicit_holes: Boolean to note whether holes in the Face3D should be
represented explicitly with their own PolyLoop or the hole and boundary
should be collapsed into a single PolyLoop that winds inwards to
cut out the holes. (Default: False).
campus_element: An optional XML Element for the Campus to which the
surface element will be added. If None, a new XML Element
will be generated. (Default: None).
"""
# establish the properties of the shade object
surface_attr = {
'id': shade.identifier,
'surfaceType': 'Shade',
'constructionIdRef': 'Shading_Surface_Without_Construction'
}
# create the Surface element
if campus_element is not None:
xml_shade = ET.SubElement(campus_element, 'Surface', surface_attr)
else:
xml_shade = ET.Element('Surface', surface_attr)
# add the name and the associated space
xml_name = ET.SubElement(xml_shade, 'Name')
xml_name.text = str(shade.display_name)
if not isinstance(shade, Shade): # orphaned Face, Aperture or Door object
ET.SubElement(xml_shade, 'AdjacentSpaceId', spaceIdRef='Detached_Shades')
elif isinstance(shade.top_level_parent, Room):
ET.SubElement(xml_shade, 'AdjacentSpaceId',
spaceIdRef=shade.top_level_parent.identifier)
elif shade.is_detached:
ET.SubElement(xml_shade, 'AdjacentSpaceId', spaceIdRef='Detached_Shades')
else:
ET.SubElement(xml_shade, 'AdjacentSpaceId', spaceIdRef='Attached_Shades')
# add the geometry
face_3d_to_gbxml_element(
shade.geometry, tolerance=tolerance, rect_geo_format=rect_geo_format,
explicit_holes=explicit_holes, parent_element=xml_shade
)
return xml_shade
[docs]
def shade_mesh_to_gbxml_element(
shade_mesh, tolerance=0.001, rect_geo_format='BoundingRectangle',
explicit_holes=False, campus_element=None
):
"""Get a list of gbXML Elements from a honeybee ShadeMesh.
Args:
shade_mesh: A honeybee ShadeMesh for which a list of gbXML Surface Elements
will be returned.
tolerance: The minimum difference in coordinate values below which
vertices are considered to be identical. (Default: 0.001, suitable
for objects in Meters or Feet).
rect_geo_format: Text string to note how the rectangular geometry for
all Surfaces is written into the gbXML. BoundingRectangle sets the
width and height of the rectangular geometry using the bounding
rectangle around the geometry, which results in an overestimated
area for non-rectangular geo. SimpleArea will set the rectangle width
always equal to geometry area and the height always equal to one,
ensuring accurate areas and making it easy to check the geometry
area in the gbXML. SimpleAreaForNonRectOnly will report the width and
height of rectangular Face3D correctly but use simpler areas
for non-rectangular geometry. (Default: BoundingRectangle). Choose
from the following.
* BoundingRectangle
* SimpleArea
* SimpleAreaForNonRectOnly
explicit_holes: Boolean to note whether holes in the Face3D should be
represented explicitly with their own PolyLoop or the hole and boundary
should be collapsed into a single PolyLoop that winds inwards to
cut out the holes. (Default: False).
campus_element: An optional XML Element for the Campus to which all of the
surface elements will be added. If None, a new XML Element
will be generated. (Default: None).
"""
# establish the properties of the shade object
surface_attr = {
'surfaceType': 'Shade',
'constructionIdRef': 'Shading_Surface_Without_Construction'
}
# create the Surface elements
xml_shades = []
for i, face in enumerate(shade_mesh.geometry.face_vertices):
surface_attr['id'] = '{}_{}'.format(shade_mesh.identifier, i)
if campus_element is not None:
xml_shade = ET.SubElement(campus_element, 'Surface', surface_attr)
else:
xml_shade = ET.Element('Surface', surface_attr)
# add the name and the associated space
xml_name = ET.SubElement(xml_shade, 'Name')
xml_name.text = '{} {}'.format(shade_mesh.display_name, i)
if shade_mesh.is_detached:
ET.SubElement(xml_shade, 'AdjacentSpaceId', spaceIdRef='Detached Shades')
else:
ET.SubElement(xml_shade, 'AdjacentSpaceId', spaceIdRef='Attached Shades')
# add the geometry
face_3d_to_gbxml_element(
Face3D(face), tolerance=tolerance, rect_geo_format=rect_geo_format,
parent_element=xml_shade
)
xml_shades.append(xml_shade)
return xml_shades
[docs]
def sub_face_to_gbxml_element(
sub_face, tolerance=0.001, rect_geo_format='BoundingRectangle', explicit_holes=False,
surface_element=None, rect_origin=None
):
"""Get a gbXML Opening Element from a honeybee Aperture or Door.
Args:
sub_face: A honeybee Aperture or Door for which a gbXML Opening Element
object will be returned.
tolerance: The minimum difference in coordinate values below which
vertices are considered to be identical. (Default: 0.001, suitable
for objects in Meters or Feet).
rect_geo_format: Text string to note how the rectangular geometry for
all Surfaces is written into the gbXML. BoundingRectangle sets the
width and height of the rectangular geometry using the bounding
rectangle around the geometry, which results in an overestimated
area for non-rectangular geo. SimpleArea will set the rectangle width
always equal to geometry area and the height always equal to one,
ensuring accurate areas and making it easy to check the geometry
area in the gbXML. SimpleAreaForNonRectOnly will report the width and
height of rectangular Face3D correctly but use simpler areas
for non-rectangular geometry. (Default: BoundingRectangle). Choose
from the following.
* BoundingRectangle
* SimpleArea
* SimpleAreaForNonRectOnly
explicit_holes: Boolean to note whether holes in the Face3D should be
represented explicitly with their own PolyLoop or the hole and boundary
should be collapsed into a single PolyLoop that winds inwards to
cut out the holes. (Default: False).
surface_element: An optional XML Element for the Surface to which the
opening element will be added. If None, a new XML Element
will be generated. (Default: None).
rect_origin: An optional Point3D to set the origin of the rectangular
geometry. This is used for sub faces, which need to use the origin
of the parent Face. If None, the Face3D's lower left corner will
be used. (Default: None).
"""
# establish the properties of the opening object
construction = clean_xml_tag_string(sub_face.properties.energy.construction.identifier)
opening_attr = {
'id': sub_face.identifier,
'openingType': sub_face.gbxml_type,
'windowTypeIdRef': construction
}
# create the Opening element
if surface_element is not None:
xml_opening = ET.SubElement(surface_element, 'Opening', opening_attr)
else:
xml_opening = ET.Element('Opening', opening_attr)
# add the name and the associated space
xml_name = ET.SubElement(xml_opening, 'Name')
xml_name.text = str(sub_face.display_name)
# add the geometry
face_3d_to_gbxml_element(
sub_face.geometry, tolerance=tolerance, rect_geo_format=rect_geo_format,
explicit_holes=explicit_holes, parent_element=xml_opening, rect_origin=rect_origin
)
return xml_opening
[docs]
def face_to_gbxml_element(
face, tolerance=0.001, rect_geo_format='BoundingRectangle', explicit_holes=False,
campus_element=None
):
"""Get a gbXML Surface Element from a honeybee Face.
Note that the resulting Surface element includes all Apertures and Doors
assigned to the Face as gbXML Opening elements.
Args:
face: A honeybee Face for which an gbXML Surface Element will be returned.
tolerance: The minimum difference in coordinate values below which
vertices are considered to be identical. (Default: 0.001, suitable
for objects in Meters or Feet).
rect_geo_format: Text string to note how the rectangular geometry for
all Surfaces is written into the gbXML. BoundingRectangle sets the
width and height of the rectangular geometry using the bounding
rectangle around the geometry, which results in an overestimated
area for non-rectangular geo. SimpleArea will set the rectangle width
always equal to geometry area and the height always equal to one,
ensuring accurate areas and making it easy to check the geometry
area in the gbXML. SimpleAreaForNonRectOnly will report the width and
height of rectangular Face3D correctly but use simpler areas
for non-rectangular geometry. (Default: BoundingRectangle). Choose
from the following.
* BoundingRectangle
* SimpleArea
* SimpleAreaForNonRectOnly
explicit_holes: Boolean to note whether holes in the Face3D should be
represented explicitly with their own PolyLoop or the hole and boundary
should be collapsed into a single PolyLoop that winds inwards to
cut out the holes. (Default: False).
campus_element: An optional XML Element for the Campus to which the
surface element will be added. If None, a new XML Element
will be generated. (Default: None).
"""
# establish the properties of the face object
construction = face.properties.energy.construction.identifier.replace(' ', '_')
sun_exposed = isinstance(face.boundary_condition, Outdoors) and \
face.boundary_condition.sun_exposure
surface_attr = {
'id': face.identifier,
'surfaceType': face.gbxml_type,
'constructionIdRef': construction,
'exposedToSun': str(sun_exposed).lower()
}
# create the Surface element
if campus_element is not None:
xml_face = ET.SubElement(campus_element, 'Surface', surface_attr)
else:
xml_face = ET.Element('Surface', surface_attr)
# add the name and the associated space
xml_name = ET.SubElement(xml_face, 'Name')
xml_name.text = str(face.display_name)
if face.has_parent:
ET.SubElement(xml_face, 'AdjacentSpaceId', spaceIdRef=face.parent.identifier)
if isinstance(face.boundary_condition, Surface):
adj_room = face.boundary_condition.boundary_condition_objects[-1]
ET.SubElement(xml_face, 'AdjacentSpaceId', spaceIdRef=adj_room)
# add the geometry
face_3d_to_gbxml_element(
face.geometry, tolerance=tolerance, rect_geo_format=rect_geo_format,
explicit_holes=explicit_holes, parent_element=xml_face,
)
# add the apertures and doors as Opening elements
sub_faces = face.sub_faces
if len(sub_faces) != 0:
rect_origin = face.geometry.lower_left_corner
for sf in sub_faces:
sub_face_to_gbxml_element(
sf, tolerance=tolerance, rect_geo_format=rect_geo_format,
explicit_holes=explicit_holes,
surface_element=xml_face, rect_origin=rect_origin
)
return xml_face
[docs]
def room_to_gbxml_element(
room, ip_units=False, include_shell_geometry=False, include_space_boundaries=False,
tolerance=0.001, explicit_holes=False, building_element=None
):
"""Get a gbXML Space Element from a honeybee Room.
Note that the Space elements of gbXML do not contain any geometry given that
all geometry is specified with Surface elements.
Args:
room: A honeybee Room for which an gbXML Space Element will be returned.
ip_units: A boolean to note whether the space loads should be reported
in IP units (True) or SI units (False). (Default: False).
include_shell_geometry: Boolean for whether shell geometry should be
included in the Space definition. (Default: False).
include_space_boundaries: Boolean for whether space boundaries should be
included in the Space definition. (Default: False).
tolerance: The minimum difference in coordinate values below which
vertices are considered to be identical. (Default: 0.001, suitable
for objects in Meters or Feet).
explicit_holes: Boolean to note whether holes in Face3Ds should be
represented explicitly with their own PolyLoop or the hole and boundary
should be collapsed into a single PolyLoop that winds inwards to
cut out the holes. (Default: False).
building_element: An optional XML Element for the Building to which the
space element will be added. If None, a new XML Element will be
generated. (Default: None).
"""
# establish the properties of the room object
story = clean_xml_tag_string(room.story) if room.story is not None else 'Unknown_Level'
space_attr = {
'id': room.identifier,
'zoneIdRef': room.zone,
'buildingStoreyIdRef': story
}
# create the Space element
decimal_count, _ = rounding_tolerance(tolerance)
if building_element is not None:
xml_space = ET.SubElement(building_element, 'Space', space_attr)
else:
xml_space = ET.Element('Space', space_attr)
# add the name, area and volume properties
xml_name = ET.SubElement(xml_space, 'Name')
xml_name.text = str(room.display_name)
xml_area = ET.SubElement(xml_space, 'Area')
xml_area.text = str(round(room.floor_area)) \
if ip_units else str(round(room.floor_area, 1))
xml_volume = ET.SubElement(xml_space, 'Volume')
xml_volume.text = str(round(room.volume)) \
if ip_units else str(round(room.volume, 1))
# add the people loads if they exist
people = room.properties.energy.people
if people is not None:
if room.properties.energy._people is not None: # assume it's a person count
people_value = people.people_per_area * room.floor_area
if ip_units: # assume the room geometry is in square feet
people_value = people_value / 10.7639
unit = 'NumberOfPeople'
elif ip_units:
people_value, unit = people.people_per_area_ip, 'SquareFtPerPerson'
else:
people_value, unit = people.people_per_area_si, 'SquareMPerPerson'
xml_people = ET.SubElement(xml_space, 'PeopleNumber', unit=unit)
xml_people.text = str(round(people_value)) \
if ip_units else str(round(people_value, 1))
# write the people heat gain
if ip_units:
unit = 'BtuPerHourPerson'
sensible_ppl = people.activity_max_sensible_ip
latent_ppl = people.activity_max_latent_ip
else:
unit = 'WattPerPerson'
sensible_ppl = people.activity_max_sensible
latent_ppl = people.activity_max_latent
xml_s_ppl = ET.SubElement(xml_space, 'PeopleHeatGain',
unit=unit, heatGainType='Sensible')
xml_s_ppl.text = str(round(sensible_ppl))
xml_l_ppl = ET.SubElement(xml_space, 'PeopleHeatGain',
unit=unit, heatGainType='Latent')
xml_l_ppl.text = str(round(latent_ppl))
# add the lighting load if it exists
lighting = room.properties.energy.lighting
if lighting is not None:
if ip_units:
watts_per_area, unit = lighting.watts_per_area_ip, 'WattPerSquareFoot'
else:
watts_per_area, unit = lighting.watts_per_area_si, 'WattPerSquareMeter'
xml_lights = ET.SubElement(xml_space, 'LightPowerPerArea', unit=unit)
xml_lights.text = str(round(watts_per_area, decimal_count))
# add the equipment load if it exists
electric_equip = room.properties.energy.electric_equipment
gas_equip = room.properties.energy.gas_equipment
if electric_equip is not None or gas_equip is not None:
watts_per_area = 0
for equip in (electric_equip, gas_equip):
if equip is not None:
watts_per_area += equip.watts_per_area
unit = 'WattPerSquareMeter'
if ip_units:
unit = 'WattPerSquareFoot'
watts_per_area = watts_per_area / 10.7639
xml_equip = ET.SubElement(xml_space, 'EquipPowerPerArea', unit=unit)
xml_equip.text = str(round(watts_per_area, decimal_count))
# add the infiltration load if it exists
inf_obj = room.properties.energy.infiltration
if inf_obj is not None:
inf_per_area = inf_obj.flow_per_exterior_area
if inf_per_area <= 0.00015:
inf_class = 'Tight'
elif inf_per_area <= 0.00045:
inf_class = 'Average'
else:
inf_class = 'Loose'
inf_element = ET.SubElement(xml_space, 'InfiltrationFlow')
inf_element.set('type', inf_class)
total_inf = inf_per_area * room.exposed_area
total_ach = (total_inf * 3600) / room.volume
blower_element = ET.SubElement(inf_element, 'BlowerDoorValue')
blower_element.set('unit', 'AirChangesPerHour')
blower_element.text = str(round(total_ach, decimal_count))
# write the shell geometry and space boundaries if requested
if include_shell_geometry or include_space_boundaries:
geo_elements = []
for face in room:
# write the geometry of the face
geo_element = ET.Element('PlanarGeometry')
face_3d, xml_poly = face.geometry, None
if explicit_holes and face_3d.has_holes:
xml_poly = ET.SubElement(geo_element, 'PolyLoop')
for pt in face_3d.boundary:
xml_pt = ET.SubElement(xml_poly, 'CartesianPoint')
for coord in pt:
xml_coord = ET.SubElement(xml_pt, 'Coordinate')
xml_coord.text = str(round(coord, decimal_count))
for hole in face_3d.holes:
xml_poly = ET.SubElement(geo_element, 'PolyLoop')
for pt in hole:
xml_pt = ET.SubElement(xml_poly, 'CartesianPoint')
for coord in pt:
xml_coord = ET.SubElement(xml_pt, 'Coordinate')
xml_coord.text = str(round(coord, decimal_count))
# write all vertices into one poly loop for shell geometry
xml_poly = ET.SubElement(geo_element, 'PolyLoop') \
if xml_poly is None else ET.Element('PolyLoop')
for pt in face_3d.vertices:
xml_pt = ET.SubElement(xml_poly, 'CartesianPoint')
for coord in pt:
xml_coord = ET.SubElement(xml_pt, 'Coordinate')
xml_coord.text = str(round(coord, decimal_count))
geo_elements.append(xml_poly)
# write the geometry as a space boundary if requested
if include_space_boundaries:
sb_element = ET.Element('SpaceBoundary')
sb_element.set('isSecondLevelBoundary', 'false')
sb_element.set('surfaceIdRef', face.identifier)
sb_element.append(geo_element)
xml_space.append(sb_element)
# write it as shell geometry if requested
if include_shell_geometry:
shell_element = ET.SubElement(xml_space, 'ShellGeometry')
shell_element.set('id', '{}Shell'.format(room.identifier))
shell_geo_element = ET.SubElement(shell_element, 'ClosedShell')
for xml_geo in geo_elements:
shell_geo_element.append(xml_geo)
return xml_space
[docs]
def model_to_gbxml_element(
model, ip_units=False, include_shell_geometry=False, include_space_boundaries=False,
interior_face_type='InteriorFloor', ground_face_type='AutoAssign',
face_rename_format=None, subface_rename_format=None,
reset_geometry_ids=False, reset_resource_ids=False,
triangulate_subfaces=False, triangulate_non_planar=True,
rect_geo_format='BoundingRectangle', explicit_holes=False,
total_ventilation=True, program_name=None, program_version=None,
gbxml_schema_version=None
):
"""Get a gbXML ElementTree that represents ann entire model.
Args:
model: A honeybee Model for which a gbXML ElementTree will be returned.
ip_units: A boolean to note whether the geometry, space loads, and
construction properties are reported in IP units (True) or SI
units (False). (Default: False).
include_shell_geometry: Boolean for whether shell geometry should be included vs.
just the minimal required non-manifold geometry. (Default: False).
include_space_boundaries: Boolean for whether space boundaries should be included
vs. just the minimal required non-manifold geometry. (Default: False).
interior_face_type: Text string for the type to be used for all interior
floor/ceiling faces. (Default: InteriorFloor). Choose from the following.
* InteriorFloor
* Ceiling
ground_face_type: Text string for the type to be used for all ground-contact
floor faces. If AutoAssign, the ground types will be SlabOnGrade for floors
belonging to rooms with any above-ground walls and UndergroundSlab
for floors in rooms with all underground walls. Choose from the following.
* AutoAssign
* UndergroundSlab
* SlabOnGrade
* RaisedFloor
face_rename_format: An optional text string for the pattern with which
faces will be renamed. Any property on the honeybee Face class may be
used (eg. gbxml_str) and each property should be put in curly brackets.
Nested properties can be specified by using "." to denote nesting levels
(eg. properties.energy.construction.display_name). Functions that
return string outputs can also be passed here as long as these
functions defaults specified for all arguments.
subface_rename_format: An optional text string for the pattern with which
apertures and doors will be renamed. Any property that exists on both
the honeybee Aperture and honeybee Door class may be used (eg. gbxml_str)
and each property should be put in curly brackets. Nested
properties can be specified by using "." to denote nesting levels
(eg. properties.energy.construction.display_name). Functions that
return string outputs can also be passed here as long as these
functions defaults specified for all arguments.
reset_geometry_ids: Boolean to note whether a cleaned version of geometry
display names should be used for the IDs that appear within
the gbXML file. Using this flag will affect all Rooms, Faces,
Apertures, Doors, and Shades. It will generally result in more
read-able IDs in the gbXML file but this means that it will not be
easy to map results back to the input 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).
reset_resource_ids: Boolean to note whether a cleaned version of all
resource display names should be used for the IDs that appear within
the gbXML file. 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 gbXML file. 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).
triangulate_non_planar: Boolean to note whether any non-planar
orphaned geometry in the model should be triangulated.
This can be helpful because OpenStudio simply raises an error when
it encounters non-planar geometry, which would hinder the ability
to save files that are to be corrected later. (Default: False).
triangulate_subfaces: Boolean to note whether sub-faces (including
Apertures and Doors) should be triangulated if they have more
than 4 sides (True) or whether they should be left as they are (False).
This triangulation is necessary when exporting directly to EnergyPlus
since it cannot accept sub-faces with more than 4 vertices. (Default: False).
rect_geo_format: Text string to note how the rectangular geometry for
all Surfaces is written into the gbXML. BoundingRectangle sets the
width and height of the rectangular geometry using the bounding
rectangle around the geometry, which results in an overestimated
area for non-rectangular geo. SimpleArea will set the rectangle width
always equal to geometry area and the height always equal to one,
ensuring accurate areas and making it easy to check the geometry
area in the gbXML. SimpleAreaForNonRectOnly will report the width and
height of rectangular Face3D correctly but use simpler areas
for non-rectangular geometry. (Default: BoundingRectangle). Choose
from the following.
* BoundingRectangle
* SimpleArea
* SimpleAreaForNonRectOnly
explicit_holes: Boolean to note whether holes in Surfaces should be
represented explicitly with their own PolyLoop or the hole and boundary
should be collapsed into a single PolyLoop that winds inwards to
cut out the holes. (Default: False).
total_ventilation: Boolean to note whether outdoor air ventilation values
in the gbXML are written as a single total OAFlowPerZone (True)
or ventilation criteria are written as separate criteria (False).
That is, separate specifications for OAFlowPerPerson, OAFlowPerArea,
etc. Note that the total ventilation accounts for the ventilation
effectiveness while the individual flows do not. (Default: True).
program_name: Optional text to set the name of the software that will
appear under the programId and ProductName tags of the DocumentHistory
section. This can be set things like "Ladybug Tools" or "Pollination"
or some other software in which this gbXML export capability is being
run. If None, the "OpenStudio" will be used. (Default: None).
program_version: Optional text to set the version of the software that
will appear under the DocumentHistory section. If None, and the
program_name is also unspecified, only the version of OpenStudio will
appear. Otherwise, this will default to "0.0.0" given that the version
field is required. (Default: None).
gbxml_schema_version: Optional text to set the version of the gbXML schema
that is specified in the XML header (eg. "5.00"). If None, this
will default to the latest version.
"""
# duplicate model to avoid mutating it as we edit it for energy simulation
original_model = model
model = model.duplicate()
# scale the model if the units are not meters
if ip_units:
model.convert_to_units('Feet')
scale_fac = conversion_factor_to_meters('Feet')
else:
model.convert_to_units('Meters')
scale_fac = 1
# as a good practice, remove degenerate geometry within model tolerance
tol = model.tolerance
decimal_count, _ = rounding_tolerance(tol)
try:
model.remove_degenerate_geometry(tol)
except ValueError:
error = 'Failed to remove degenerate Rooms.\nYour Model units system is: {}. ' \
'Is this correct?'.format(original_model.units)
raise ValueError(error)
if triangulate_non_planar:
model.triangulate_non_planar_quads(tol)
# auto-assign stories if there are none
if len(model.stories) == 0 and len(model.rooms) != 0:
model.assign_stories_by_floor_height()
# rename the faces, apertures, and doors if requested
if face_rename_format:
for room in model.rooms:
room.rename_faces_by_attribute(face_rename_format)
if subface_rename_format:
for room in model.rooms:
room.rename_apertures_by_attribute(subface_rename_format)
room.rename_doors_by_attribute(subface_rename_format)
# ensure all display_names are unique because some gbXML interfaces require this
model.assign_unique_names()
# reset the IDs to be derived from the display_names if requested
if reset_geometry_ids:
model.reset_ids()
if reset_resource_ids:
model.properties.energy.reset_resource_ids()
# ensure that all identifiers are legal IDs for XML tags
for room in model.rooms:
room.identifier = clean_xml_tag_string(room.identifier)
for face in room.faces:
face.identifier = clean_xml_tag_string(face.identifier)
fbc = face.boundary_condition
if isinstance(fbc, Surface):
sbc_objs = tuple(clean_xml_tag_string(obj) for obj in
fbc.boundary_condition_objects)
face.boundary_condition = Surface(sbc_objs)
for sf in face.sub_faces:
sf.identifier = clean_xml_tag_string(sf.identifier)
fbc = sf.boundary_condition
if isinstance(fbc, Surface):
sbc_objs = tuple(clean_xml_tag_string(obj) for obj in
fbc.boundary_condition_objects)
sf.boundary_condition = Surface(sbc_objs, True)
# resolve the properties across zones
zone_name_dict = {r.identifier: r.zone for r in model.rooms}
for room in model.rooms: # set all zone IDs to be acceptable in gbXML
room.zone = clean_xml_tag_string(room.zone)
single_zones, zone_dict = model.properties.energy.resolve_zones()
# depending on the unit system, set the units for the file
if not ip_units:
t_units, result_units = 'C', 'true'
l_units, a_units, v_units = 'Meters', 'SquareMeters', 'CubicMeters'
else:
t_units, result_units = 'F', 'false'
l_units, a_units, v_units = 'Feet', 'SquareFeet', 'CubicFeet'
# set the gbXML schema version in the header if specified
SCHEMA_VERSIONS = (
'0.35', '0.36', '0.37', '5.00', '5.01', '5.10', '5.11', '5.12',
'6.00', '6.01', '7.03', '8.01'
)
now_ver = SCHEMA_VERSIONS[-1]
if gbxml_schema_version is not None and gbxml_schema_version != now_ver:
if gbxml_schema_version not in SCHEMA_VERSIONS:
raise ValueError(
'The specified gbXML schema version "{}" is not recognized. '
'Please choose from the following: {}'.format(
gbxml_schema_version, ', '.join(SCHEMA_VERSIONS)
)
)
gbxml_version = now_ver if gbxml_schema_version is None else gbxml_schema_version
# create the ElementTree that holds everything
xsd_template = 'http://gbxml.org/schema/{}/GreenBuildingXML_Ver{}.xsd'
xsd_url = xsd_template.format(gbxml_version.replace('.', '-'), gbxml_version)
gbxml_attr = {
'xmlns': 'http://www.gbxml.org/schema',
'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'xsi:schemaLocation': 'http://www.gbxml.org/schema {}'.format(xsd_url),
'temperatureUnit': t_units,
'lengthUnit': l_units,
'areaUnit': a_units,
'volumeUnit': v_units,
'useSIUnitsForResults': result_units,
'version': gbxml_version,
'SurfaceReferenceLocation': 'Centerline'
}
gbxml_root = ET.Element('gbXML', gbxml_attr)
# create the campus and building element
xml_campus = ET.SubElement(gbxml_root, 'Campus', id='Facility')
xml_campus_name = ET.SubElement(xml_campus, 'Name')
xml_campus_name.text = 'Facility'
xml_bldg = ET.SubElement(xml_campus, 'Building')
xml_bldg_name = ET.SubElement(xml_bldg, 'Name')
xml_bldg_name.text = str(model.display_name)
xml_floor_area = ET.SubElement(xml_bldg, 'Area')
xml_floor_area.text = str(round(model.floor_area)) \
if ip_units else str(round(model.floor_area, 1))
# write all of the rooms into the gbXML as spaces
story_dict, xml_rooms = OrderedDict(), []
for room in model.rooms:
xml_room = room_to_gbxml_element(
room, ip_units, include_shell_geometry, include_space_boundaries,
tol, explicit_holes, xml_bldg
)
xml_rooms.append(xml_room)
try:
story_dict[room.story].append(room)
except KeyError:
story_dict[room.story] = [room]
# add spaces for unassigned shades if they exist in the model
detached_shades, detached_sms, attached_shades, attached_sms = [], [], [], []
for face in model.orphaned_faces:
detached_shades.append(face)
for aperture in model.orphaned_apertures:
detached_shades.append(aperture)
for door in model.orphaned_doors:
detached_shades.append(door)
for shade in model.orphaned_shades:
if shade.is_detached:
detached_shades.append(shade)
else:
attached_shades.append(shade)
for shade_mesh in model.shade_meshes:
if shade_mesh.is_detached:
detached_sms.append(shade_mesh)
else:
attached_sms.append(shade_mesh)
if len(attached_shades) != 0 or len(attached_sms) != 0:
xml_shd_space = ET.SubElement(xml_bldg, 'Space', id='Attached_Shades')
xml_shd_name = ET.SubElement(xml_shd_space, 'Name')
xml_shd_name.text = 'Attached Shades'
if len(detached_shades) != 0 or len(detached_sms) != 0:
xml_shd_space = ET.SubElement(xml_bldg, 'Space', id='Detached_Shades')
xml_shd_name = ET.SubElement(xml_shd_space, 'Name')
xml_shd_name.text = 'Detached Shades'
# get the stories of the model and write them into the gbXML
for story_name, story_rooms in story_dict.items():
elevation = min(r.min.z for r in story_rooms)
xml_story = ET.SubElement(
xml_bldg, 'BuildingStorey', id=clean_xml_tag_string(story_name)
)
xml_story_name = ET.SubElement(xml_story, 'Name')
xml_story_name.text = story_name
xml_story_elev = ET.SubElement(xml_story, 'Level')
xml_story_elev.text = str(round(elevation, decimal_count))
# all of the room faces and openings to the gbxml ad non-manifold geometry
adj_to_ignore = {}
for room in model.rooms:
for face in room.faces:
# ensure that interior surfaces are not added twice
fbc = face.boundary_condition
if isinstance(fbc, Surface):
if face.identifier in adj_to_ignore:
continue
if isinstance(face.type, RoofCeiling) and interior_face_type == 'InteriorFloor':
continue
elif isinstance(face.type, Floor) and interior_face_type == 'Ceiling':
continue
adj_to_ignore[fbc.boundary_condition_object] = face.identifier
# add the face element to the gbxml
xml_face = face_to_gbxml_element(
face, tolerance=tol, rect_geo_format=rect_geo_format,
explicit_holes=explicit_holes, campus_element=xml_campus
)
# if the floor type was specified, overwrite it
if ground_face_type != 'AutoAssign' and isinstance(fbc, Ground):
if isinstance(face.type, Floor):
xml_face.set('surfaceType', ground_face_type)
# if space boundaries were requested, loop through adj_to_ignore and replace them
if include_space_boundaries:
for xml_room in xml_rooms:
for xml_sb in xml_room.findall('SpaceBoundary'):
srf_id = xml_sb.get('surfaceIdRef')
if srf_id in adj_to_ignore:
xml_sb.set('surfaceIdRef', adj_to_ignore[srf_id])
# add all of the shade geometries to the gbxml
for shade in attached_shades + detached_shades:
shade.identifier = clean_xml_tag_string(shade.identifier)
shade_to_gbxml_element(shade, tol, rect_geo_format, explicit_holes, xml_campus)
for sm in attached_sms + detached_sms:
sm.identifier = clean_xml_tag_string(sm.identifier)
shade_mesh_to_gbxml_element(sm, tol, rect_geo_format, explicit_holes, xml_campus)
# get the default generic construction set
# must be imported here to avoid circular imports
from .lib.constructionsets import generic_construction_set
# add the construction objects and window types to the gbxml
if len(attached_shades) != 0 or len(attached_sms) != 0 or \
len(detached_shades) != 0 or len(detached_sms) != 0:
xml_shd_con = ET.SubElement(gbxml_root, 'Construction')
xml_shd_con.set('id', 'Shading_Surface_Without_Construction')
xml_shd_con_name = ET.SubElement(xml_shd_con, 'Name')
xml_shd_con_name.text = 'Shading Surface Without Construction'
materials = []
all_constrs = model.properties.energy.constructions + \
generic_construction_set.constructions_unique
for constr in set(all_constrs):
try:
if constr.__class__.__name__ == 'OpaqueConstruction':
materials.extend(constr.materials)
try: # first assume it is a window construction
constr.to_gbxml_element(ip_units=ip_units, parent_element=gbxml_root)
except TypeError: # opaque or air boundary construction
constr.to_gbxml_element(parent_element=gbxml_root)
except AttributeError: # ShadeConstruction; no need to write it
pass
# add the material objects to the gbxml
for mat in set(materials):
mat.to_gbxml_element(ip_units=ip_units, parent_element=gbxml_root)
# add the zone information to the gbxml
for room in single_zones:
e_prop = room.properties.energy
zone_dict[room.zone] = [(room,), None, e_prop.setpoint, e_prop.ventilation]
for zone_id, zone_data in zone_dict.items():
rooms, _, set_pt, vent = zone_data
xml_zone = ET.SubElement(gbxml_root, 'Zone', id=zone_id)
xml_zone_name = ET.SubElement(xml_zone, 'Name')
xml_zone_name.text = zone_name_dict[rooms[0].identifier]
# assign the setpoint to the zone
if set_pt:
xml_h_set = ET.SubElement(xml_zone, 'DesignHeatT', unit=t_units)
h_set = set_pt.heating_setpoint_ip if ip_units else set_pt.heating_setpoint
xml_h_set.text = str(round(h_set, 2))
xml_c_set = ET.SubElement(xml_zone, 'DesignCoolT', unit=t_units)
c_set = set_pt.cooling_setpoint_ip if ip_units else set_pt.cooling_setpoint
xml_c_set.text = str(round(c_set, 2))
if set_pt.humidifying_setpoint:
xml_hu_set = ET.SubElement(xml_zone, 'DesignHeatRH')
xml_hu_set.text = str(round(set_pt.humidifying_setpoint))
xml_dhu_set = ET.SubElement(xml_zone, 'DesignCoolRH')
xml_dhu_set.text = str(round(set_pt.dehumidifying_setpoint))
# assign the outdoor air criteria to the zone
if vent:
if total_ventilation: # write ventilation as a single air flow
rooms_si, flow_units, unit_abbrev = rooms, 'LPerSec', 'si'
if ip_units:
rooms_si = []
for r in rooms:
new_r = r.duplicate()
new_r.scale(scale_fac)
rooms_si.append(new_r)
flow_units, unit_abbrev = 'CFM', 'ip'
total_flows = [vent.flow_per_zone]
if vent.flow_per_person != 0:
person_flow = 0
for r in rooms_si:
people = r.properties.energy.people
if people is not None:
person_count = people.people_per_area * r.floor_area
person_flow += vent.flow_per_person * person_count
total_flows.append(person_flow)
if vent.flow_per_area != 0:
total_area = sum(r.floor_area for r in rooms_si)
total_flows.append(vent.flow_per_area * total_area)
if vent.air_changes_per_hour != 0:
total_volume = sum(r.volume for r in rooms_si)
total_flows.append((vent.air_changes_per_hour * total_volume) / 3600)
total_flow = sum(total_flows) if vent.method == 'Sum' else max(total_flows)
vent_eff = min(vent.effectiveness_cooling, vent.effectiveness_heating)
total_flow = total_flow / vent_eff
if total_flow != 0:
flow_element = ET.SubElement(xml_zone, 'OAFlowPerZone')
flow_element.set('unit', flow_units)
flow = convert_ventilation_flow_per_zone(total_flow, unit_abbrev)
flow_element.text = str(round(flow, 2))
else: # write individual airflow criteria
ach = vent.air_changes_per_hour
if ip_units:
per_person, per_area = vent.flow_per_person_ip, vent.flow_per_area_ip
flow = vent.flow_per_zone_ip
flow_units, per_area_units = 'CFM', 'CFMPerSquareFoot'
else:
per_person, per_area = vent.flow_per_person_si, vent.flow_per_area_si
flow = vent.flow_per_zone_si
flow_units, per_area_units = 'LPerSec', 'LPerSecPerSquareM'
if per_person != 0:
flow_element = ET.SubElement(xml_zone, 'OAFlowPerPerson')
flow_element.set('unit', flow_units)
flow_element.text = str(round(per_person, 2))
if per_area != 0:
flow_element = ET.SubElement(xml_zone, 'OAFlowPerArea')
flow_element.set('unit', per_area_units)
flow_element.text = str(round(per_area, 3))
if ach != 0:
flow_element = ET.SubElement(xml_zone, 'AirChangesPerHour')
flow_element.text = str(round(ach, 3))
if flow != 0:
flow_element = ET.SubElement(xml_zone, 'OAFlowPerZone')
flow_element.set('unit', flow_units)
flow_element.text = str(round(flow, 3))
# add the document history to the gbxml
program_name = 'Ladybug Tools Python SDK' \
if program_name is None else program_name
program_version = 'Unknown' if program_version is None else program_version
xml_history = ET.SubElement(gbxml_root, 'DocumentHistory')
prog_id = clean_xml_tag_string(program_name).lower()
created_info = {
'programId': prog_id,
'date': str(datetime.now().astimezone().isoformat(timespec='seconds')),
'personId': 'unknown'
}
ET.SubElement(xml_history, 'CreatedBy', created_info)
xml_p_info = ET.SubElement(xml_history, 'ProgramInfo', id=prog_id)
xml_p_name = ET.SubElement(xml_p_info, 'ProductName')
xml_p_name.text = program_name
xml_c_name = ET.SubElement(xml_p_info, 'CompanyName')
xml_c_name.text = 'Ladybug Tools'
xml_version = ET.SubElement(xml_p_info, 'Version')
xml_version.text = str(program_version)
xml_platform = ET.SubElement(xml_p_info, 'Platform')
xml_platform.text = platform.system()
xml_person = ET.SubElement(xml_history, 'PersonInfo', id='unknown')
xml_f_name = ET.SubElement(xml_person, 'FirstName')
xml_f_name.text = 'Unknown'
xml_l_name = ET.SubElement(xml_person, 'LastName')
xml_l_name.text = 'Unknown'
return gbxml_root
[docs]
def model_to_gbxml(
model, ip_units=False, include_shell_geometry=False, include_space_boundaries=False,
interior_face_type='InteriorFloor', ground_face_type='AutoAssign',
face_rename_format=None, subface_rename_format=None,
reset_geometry_ids=False, reset_resource_ids=False,
triangulate_subfaces=False, triangulate_non_planar=True,
rect_geo_format='BoundingRectangle', explicit_holes=False,
total_ventilation=True, program_name=None, program_version=None,
gbxml_schema_version=None
):
"""Get a gbXML string for a Model.
Args:
model: A honeybee Model for which a gbXML text string will be returned.
ip_units: A boolean to note whether the geometry, space loads, and
construction properties are reported in IP units (True) or SI
units (False). (Default: False).
include_shell_geometry: Boolean for whether shell geometry should be included vs.
just the minimal required non-manifold geometry. (Default: False).
include_space_boundaries: Boolean for whether space boundaries should be included
vs. just the minimal required non-manifold geometry. (Default: False).
interior_face_type: Text string for the type to be used for all interior
floor/ceiling faces. (Default: InteriorFloor). Choose from the following.
* InteriorFloor
* Ceiling
ground_face_type: Text string for the type to be used for all ground-contact
floor faces. If AutoAssign, the ground types will be SlabOnGrade for floors
belonging to rooms with any above-ground walls and UndergroundSlab
for floors in rooms with all underground walls. Choose from the following.
* AutoAssign
* UndergroundSlab
* SlabOnGrade
* RaisedFloor
face_rename_format: An optional text string for the pattern with which
faces will be renamed. Any property on the honeybee Face class may be
used (eg. gbxml_str) and each property should be put in curly brackets.
Nested properties can be specified by using "." to denote nesting levels
(eg. properties.energy.construction.display_name). Functions that
return string outputs can also be passed here as long as these
functions defaults specified for all arguments.
subface_rename_format: An optional text string for the pattern with which
apertures and doors will be renamed. Any property that exists on both
the honeybee Aperture and honeybee Door class may be used (eg. gbxml_str)
and each property should be put in curly brackets. Nested
properties can be specified by using "." to denote nesting levels
(eg. properties.energy.construction.display_name). Functions that
return string outputs can also be passed here as long as these
functions defaults specified for all arguments.
reset_geometry_ids: Boolean to note whether a cleaned version of geometry
display names should be used for the IDs that appear within
the gbXML file. Using this flag will affect all Rooms, Faces,
Apertures, Doors, and Shades. It will generally result in more
read-able IDs in the gbXML file but this means that it will not be
easy to map results back to the input 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).
reset_resource_ids: Boolean to note whether a cleaned version of all
resource display names should be used for the IDs that appear within
the gbXML file. 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 gbXML file. 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).
triangulate_non_planar: Boolean to note whether any non-planar
orphaned geometry in the model should be triangulated.
This can be helpful because OpenStudio simply raises an error when
it encounters non-planar geometry, which would hinder the ability
to save files that are to be corrected later. (Default: False).
triangulate_subfaces: Boolean to note whether sub-faces (including
Apertures and Doors) should be triangulated if they have more
than 4 sides (True) or whether they should be left as they are (False).
This triangulation is necessary when exporting directly to EnergyPlus
since it cannot accept sub-faces with more than 4 vertices. (Default: False).
rect_geo_format: Text string to note how the rectangular geometry for
all Surfaces is written into the gbXML. BoundingRectangle sets the
width and height of the rectangular geometry using the bounding
rectangle around the geometry, which results in an overestimated
area for non-rectangular geo. SimpleArea will set the rectangle width
always equal to geometry area and the height always equal to one,
ensuring accurate areas and making it easy to check the geometry
area in the gbXML. SimpleAreaForNonRectOnly will report the width and
height of rectangular Face3D correctly but use simpler areas
for non-rectangular geometry. (Default: BoundingRectangle). Choose
from the following.
* BoundingRectangle
* SimpleArea
* SimpleAreaForNonRectOnly
explicit_holes: Boolean to note whether holes in Surfaces should be
represented explicitly with their own PolyLoop or the hole and boundary
should be collapsed into a single PolyLoop that winds inwards to
cut out the holes. (Default: False).
total_ventilation: Boolean to note whether outdoor air ventilation values
in the gbXML are written as a single total OAFlowPerZone (True)
or ventilation criteria are written as separate criteria (False).
That is, separate specifications for OAFlowPerPerson, OAFlowPerArea,
etc. Note that the total ventilation accounts for the ventilation
effectiveness while the individual flows do not. (Default: True).
program_name: Optional text to set the name of the software that will
appear under the programId and ProductName tags of the DocumentHistory
section. This can be set things like "Ladybug Tools" or "Pollination"
or some other software in which this gbXML export capability is being
run. If None, the "OpenStudio" will be used. (Default: None).
program_version: Optional text to set the version of the software that
will appear under the DocumentHistory section. If None, and the
program_name is also unspecified, only the version of OpenStudio will
appear. Otherwise, this will default to "0.0.0" given that the version
field is required. (Default: None).
gbxml_schema_version: Optional text to set the version of the gbXML schema
that is specified in the XML header (eg. "5.00"). If None, this
will default to the latest version.
"""
# create the XML string
xml_root = model_to_gbxml_element(
model, ip_units, include_shell_geometry, include_space_boundaries,
interior_face_type, ground_face_type, face_rename_format, subface_rename_format,
reset_geometry_ids, reset_resource_ids,
triangulate_subfaces, triangulate_non_planar, rect_geo_format, explicit_holes,
total_ventilation, program_name, program_version, gbxml_schema_version
)
try: # try to indent the XML to make it read-able
ET.indent(xml_root)
return ET.tostring(xml_root, encoding='unicode', xml_declaration=True)
except AttributeError: # we are in Python 2 and no indent is available
return ET.tostring(xml_root, xml_declaration=True)
[docs]
def shade_to_gbxml(
shade, tolerance=0.001, rect_geo_format='BoundingRectangle', explicit_holes=False
):
"""Get a gbXML Surface string from a honeybee Shade.
Args:
shade: A honeybee Shade for which an gbXML Surface string will be returned.
tolerance: The minimum difference in coordinate values below which
vertices are considered to be identical. (Default: 0.001, suitable
for objects in Meters or Feet).
rect_geo_format: Text string to note how the rectangular geometry for
all Surfaces is written into the gbXML. BoundingRectangle sets the
width and height of the rectangular geometry using the bounding
rectangle around the geometry, which results in an overestimated
area for non-rectangular geo. SimpleArea will set the rectangle width
always equal to geometry area and the height always equal to one,
ensuring accurate areas and making it easy to check the geometry
area in the gbXML. SimpleAreaForNonRectOnly will report the width and
height of rectangular Face3D correctly but use simpler areas
for non-rectangular geometry. (Default: BoundingRectangle). Choose
from the following.
* BoundingRectangle
* SimpleArea
* SimpleAreaForNonRectOnly
explicit_holes: Boolean to note whether holes in the Face3D should be
represented explicitly with their own PolyLoop or the hole and boundary
should be collapsed into a single PolyLoop that winds inwards to
cut out the holes. (Default: False).
"""
xml_root = shade_to_gbxml_element(
shade, tolerance, rect_geo_format, explicit_holes
)
try: # try to indent the XML to make it read-able
ET.indent(xml_root)
return ET.tostring(xml_root, encoding='unicode')
except AttributeError: # we are in Python 2 and no indent is available
return ET.tostring(xml_root)
[docs]
def shade_mesh_to_gbxml(
shade_mesh, tolerance=0.001, rect_geo_format='BoundingRectangle', explicit_holes=False
):
"""Get a gbXML string from a honeybee ShadeMesh.
Args:
shade_mesh: A honeybee ShadeMesh for which a gbXML Surface string
will be returned.
tolerance: The minimum difference in coordinate values below which
vertices are considered to be identical. (Default: 0.001, suitable
for objects in Meters or Feet).
rect_geo_format: Text string to note how the rectangular geometry for
all Surfaces is written into the gbXML. BoundingRectangle sets the
width and height of the rectangular geometry using the bounding
rectangle around the geometry, which results in an overestimated
area for non-rectangular geo. SimpleArea will set the rectangle width
always equal to geometry area and the height always equal to one,
ensuring accurate areas and making it easy to check the geometry
area in the gbXML. SimpleAreaForNonRectOnly will report the width and
height of rectangular Face3D correctly but use simpler areas
for non-rectangular geometry. (Default: BoundingRectangle). Choose
from the following.
* BoundingRectangle
* SimpleArea
* SimpleAreaForNonRectOnly
explicit_holes: Boolean to note whether holes in the Face3D should be
represented explicitly with their own PolyLoop or the hole and boundary
should be collapsed into a single PolyLoop that winds inwards to
cut out the holes. (Default: False).
"""
xml_roots = shade_mesh_to_gbxml_element(
shade_mesh, tolerance, rect_geo_format, explicit_holes
)
xml_strs = []
for xml_root in xml_roots:
try: # try to indent the XML to make it read-able
ET.indent(xml_root)
xml_strs.append(ET.tostring(xml_root, encoding='unicode'))
except AttributeError: # we are in Python 2 and no indent is available
xml_strs.append(ET.tostring(xml_root))
return '\n'.join(xml_strs)
[docs]
def sub_face_to_gbxml(
sub_face, tolerance=0.001, rect_geo_format='BoundingRectangle', explicit_holes=False
):
"""Get a gbXML Opening string from a honeybee Aperture or Door.
Args:
sub_face: A honeybee Aperture or Door for which a gbXML Opening string
object will be returned.
tolerance: The minimum difference in coordinate values below which
vertices are considered to be identical. (Default: 0.001, suitable
for objects in Meters or Feet).
rect_geo_format: Text string to note how the rectangular geometry for
all Surfaces is written into the gbXML. BoundingRectangle sets the
width and height of the rectangular geometry using the bounding
rectangle around the geometry, which results in an overestimated
area for non-rectangular geo. SimpleArea will set the rectangle width
always equal to geometry area and the height always equal to one,
ensuring accurate areas and making it easy to check the geometry
area in the gbXML. SimpleAreaForNonRectOnly will report the width and
height of rectangular Face3D correctly but use simpler areas
for non-rectangular geometry. (Default: BoundingRectangle). Choose
from the following.
* BoundingRectangle
* SimpleArea
* SimpleAreaForNonRectOnly
explicit_holes: Boolean to note whether holes in the Face3D should be
represented explicitly with their own PolyLoop or the hole and boundary
should be collapsed into a single PolyLoop that winds inwards to
cut out the holes. (Default: False).
"""
xml_root = sub_face_to_gbxml_element(
sub_face, tolerance, rect_geo_format, explicit_holes
)
try: # try to indent the XML to make it read-able
ET.indent(xml_root)
return ET.tostring(xml_root, encoding='unicode')
except AttributeError: # we are in Python 2 and no indent is available
return ET.tostring(xml_root)
[docs]
def face_to_gbxml(
face, tolerance=0.001, rect_geo_format='BoundingRectangle', explicit_holes=False
):
"""Get a gbXML Surface string from a honeybee Face.
Note that the resulting Surface element includes all Apertures and Doors
assigned to the Face as gbXML Openings.
Args:
face: A honeybee Face for which an gbXML Surface string will be returned.
tolerance: The minimum difference in coordinate values below which
vertices are considered to be identical. (Default: 0.001, suitable
for objects in Meters or Feet).
rect_geo_format: Text string to note how the rectangular geometry for
all Surfaces is written into the gbXML. BoundingRectangle sets the
width and height of the rectangular geometry using the bounding
rectangle around the geometry, which results in an overestimated
area for non-rectangular geo. SimpleArea will set the rectangle width
always equal to geometry area and the height always equal to one,
ensuring accurate areas and making it easy to check the geometry
area in the gbXML. SimpleAreaForNonRectOnly will report the width and
height of rectangular Face3D correctly but use simpler areas
for non-rectangular geometry. (Default: BoundingRectangle). Choose
from the following.
* BoundingRectangle
* SimpleArea
* SimpleAreaForNonRectOnly
explicit_holes: Boolean to note whether holes in the Face3D should be
represented explicitly with their own PolyLoop or the hole and boundary
should be collapsed into a single PolyLoop that winds inwards to
cut out the holes. (Default: False).
"""
xml_root = face_to_gbxml_element(
face, tolerance, rect_geo_format, explicit_holes
)
try: # try to indent the XML to make it read-able
ET.indent(xml_root)
return ET.tostring(xml_root, encoding='unicode')
except AttributeError: # we are in Python 2 and no indent is available
return ET.tostring(xml_root)
[docs]
def room_to_gbxml(
room, ip_units=False, include_shell_geometry=False, include_space_boundaries=False,
tolerance=0.001, explicit_holes=False
):
"""Get a gbXML Space string from a honeybee Room.
Note that the Space elements of gbXML do not contain any geometry given that
all geometry is specified with Surface elements.
Args:
room: A honeybee Room for which an gbXML Space string will be returned.
ip_units: A boolean to note whether the space loads should be reported
in IP units (True) or SI units (False). (Default: False).
include_shell_geometry: Boolean for whether shell geometry should be
included in the Space definition. (Default: False).
include_space_boundaries: Boolean for whether space boundaries should be
included in the Space definition. (Default: False).
tolerance: The minimum difference in coordinate values below which
vertices are considered to be identical. (Default: 0.001, suitable
for objects in Meters or Feet).
explicit_holes: Boolean to note whether holes in Face3Ds should be
represented explicitly with their own PolyLoop or the hole and boundary
should be collapsed into a single PolyLoop that winds inwards to
cut out the holes. (Default: False).
"""
xml_root = room_to_gbxml_element(
room, ip_units, include_shell_geometry, include_space_boundaries,
tolerance, explicit_holes
)
try: # try to indent the XML to make it read-able
ET.indent(xml_root)
return ET.tostring(xml_root, encoding='unicode')
except AttributeError: # we are in Python 2 and no indent is available
return ET.tostring(xml_root)
def _preprocess_model_for_trace_3dplus(
model, single_window=True, rect_sub_distance='0.15m',
frame_merge_distance='0.2m'):
"""Pre-process a Honeybee Model to be written to TRANE TRACE as a gbXML.
Args:
model: A Honeybee Model to be converted to a TRACE-compatible gbXML.
single_window: A boolean for whether all windows within walls should be
converted to a single window with an area that matches the original
geometry. (Default: True).
rect_sub_distance: Text string of a number for the resolution at which
non-rectangular Apertures will be subdivided into smaller rectangular
units. This is required as TRACE 3D plus cannot model non-rectangular
geometries. This can include the units of the distance (eg. 0.5ft) or,
if no units are provided, the value will be interpreted in the
honeybee model units. (Default: 0.15m).
frame_merge_distance: Text string of a number for the maximum distance
between non-rectangular Apertures at which point the Apertures will
be merged into a single rectangular geometry. This is often helpful
when there are several triangular Apertures that together make a
rectangle when they are merged across their frames. This can include
the units of the distance (eg. 0.5ft) or, if no units are provided,
the value will be interpreted in the honeybee model units. (Default: 0.2m).
Returns:
The input Model modified such that it can import to TRACE as a gbXML
without issues.
"""
# make sure there are rooms and remove all shades and orphaned objects
assert len(model.rooms) != 0, \
'Model contains no Rooms and therefore cannot be simulated in TRACE.'
model.remove_all_shades()
model.remove_faces()
model.remove_apertures()
model.remove_doors()
# remove degenerate geometry within native E+ tolerance of 0.01 meters
original_units = model.units
model.convert_to_units('Meters')
try:
model.remove_degenerate_geometry(0.01)
except ValueError:
error = 'Failed to remove degenerate Rooms.\nYour Model units system is: {}. ' \
'Is this correct?'.format(original_units)
raise ValueError(error)
rect_sub_distance = parse_distance_string(rect_sub_distance, original_units)
frame_merge_distance = parse_distance_string(frame_merge_distance, original_units)
if original_units != 'Meters':
c_factor = conversion_factor_to_meters(original_units)
rect_sub_distance = rect_sub_distance * c_factor
frame_merge_distance = frame_merge_distance * c_factor
# remove all interior windows in the model
for room in model.rooms:
for face in room.faces:
if isinstance(face.boundary_condition, Surface):
face.remove_sub_faces()
# convert all rooms to extrusions and patch the resulting missing adjacencies
model.rooms_to_extrusions()
model.properties.energy.missing_adjacencies_to_adiabatic()
# convert windows in walls to a single geometry
if single_window:
for room in model.rooms:
for face in room.faces:
if isinstance(face.type, Wall) and face.has_sub_faces:
face.boundary_condition = boundary_conditions.outdoors
face.apertures_by_ratio(face.aperture_ratio, 0.01, rect_split=False)
# convert all of the Aperture geometries to rectangles so they can be translated
model.rectangularize_apertures(
subdivision_distance=rect_sub_distance, max_separation=frame_merge_distance,
merge_all=True, resolve_adjacency=False
)
# if there are still multiple windows in a given Face, ensure they do not touch
for room in model.rooms:
for face in room.faces:
if len(face.apertures) > 1:
face.offset_aperture_edges(-0.01, 0.01)
# re-solve adjacency given that all of the previous operations have messed with it
model.solve_adjacency(merge_coplanar=True, intersect=True, overwrite=True)
# reset all display_names so that they are unique (derived from reset identifiers)
model.reset_ids() # sets the identifiers based on the display_name
for room in model.rooms:
room.display_name = room.identifier.replace('_', ' ')
if room.story is not None and room.story.startswith('-'):
room.story = 'neg{}'.format(room.story[1:])
# remove the HVAC from any Rooms lacking setpoints
model.properties.energy.remove_hvac_from_no_setpoints()
# rename all face geometry so that it is easy to identify in TRACE 700
for room in model.rooms:
room.rename_faces_by_attribute()
room.rename_apertures_by_attribute()
room.rename_doors_by_attribute()
model.reset_ids()
return model