# coding=utf-8
"""Building Energy Properties."""
import os
import json
from honeybee_energy.config import folders
from honeybee_energy.programtype import ProgramType
from honeybee_energy.constructionset import ConstructionSet
from honeybee_energy.hvac._base import _HVACSystem
from honeybee_energy.hvac.idealair import IdealAirSystem
from honeybee_energy.hvac import HVAC_TYPES_DICT
from honeybee_energy.shw import SHWSystem
from honeybee_energy.lib.constructionsets import generic_construction_set, \
construction_set_by_identifier
from honeybee_energy.lib.programtypes import building_program_type_by_identifier
[docs]class BuildingEnergyProperties(object):
"""Energy Properties for Dragonfly Building.
Args:
host: A dragonfly_core Building object that hosts these properties.
construction_set: A honeybee ConstructionSet object to specify all
default constructions for the Faces of the Building. If None, the
Building will use the honeybee default construction set, which is not
representative of a particular building code or climate zone.
Default: None.
Properties:
* host
* construction_set
"""
_HVAC_REGISTRY = None
_HVAC_TYPES_DICT = HVAC_TYPES_DICT
_VINTAGE_MAP = {
'DOE Ref Pre-1980': ('pre_1980', 'DOE_Ref_Pre_1980'),
'DOE Ref 1980-2004': ('1980_2004', 'DOE_Ref_1980_2004'),
'90.1-2004': ('2004', 'ASHRAE_2004'),
'90.1-2007': ('2007', 'ASHRAE_2007'),
'90.1-2010': ('2010', 'ASHRAE_2010'),
'90.1-2013': ('2013', 'ASHRAE_2013'),
'90.1-2016': ('2016', 'ASHRAE_2016'),
'90.1-2019': ('2019', 'ASHRAE_2019')
}
__slots__ = ('_host', '_construction_set')
def __init__(self, host, construction_set=None):
"""Initialize Building energy properties."""
self._host = host
self.construction_set = construction_set
@property
def host(self):
"""Get the Building object hosting these properties."""
return self._host
@property
def construction_set(self):
"""Get or set the Building ConstructionSet object.
If not set, it will be the Honeybee default generic ConstructionSet.
"""
if self._construction_set is not None: # set by the user
return self._construction_set
else:
return generic_construction_set
@construction_set.setter
def construction_set(self, value):
if value is not None:
assert isinstance(value, ConstructionSet), \
'Expected ConstructionSet. Got {}'.format(type(value))
value.lock() # lock in case construction set has multiple references
self._construction_set = value
[docs] def averaged_program_type(self, identifier=None, timestep_resolution=1):
"""Get a ProgramType that is averaged across all of the children Room2Ds.
The weights used in the averaging process are the floor area weights and they
account for the multipliers on the child Story objects.
Args:
identifier: A unique ID text string for the new averaged ProgramType.
Must be < 100 characters and not contain any EnergyPlus special
characters. This will be used to identify the object across a model
and in the exported IDF. If None, the resulting ProgramType will
use the identifier of the host Building. (Default: None)
timestep_resolution: An optional integer for the timestep resolution
at which the schedules will be averaged. Any schedule details
smaller than this timestep will be lost in the averaging process.
Default: 1.
"""
# get the default identifier of the ProgramType if None
identifier = identifier if identifier is not None else \
'{}_Program'.format(self.host.identifier)
# compute the floor area weights and programs
flr_areas = []
program_types = []
for story in self.host.unique_stories:
for room in story.room_2ds:
flr_areas.append(room.floor_area * story.multiplier)
program_types.append(room.properties.energy.program_type)
total_area = sum(flr_areas)
weights = [room_area / total_area for room_area in flr_areas]
# compute the averaged program
return ProgramType.average(
identifier, program_types, weights, timestep_resolution)
[docs] def set_all_room_2d_program_type(self, program_type):
"""Set all of the children Room2Ds of this Building to have the same ProgramType.
Args:
program_type: A ProgramType to assign to all children Room2Ds.
"""
assert isinstance(program_type, ProgramType), 'Expected ProgramType for ' \
'Building set_all_room_2d_program_type. Got {}'.format(type(program_type))
for room_2d in self.host.unique_room_2ds:
room_2d.properties.energy.program_type = program_type
[docs] def set_all_program_type_from_building_type(self, building_type):
"""Set the children Room2Ds to have a program mix from a building_type.
Args:
building_type: A text string for the type of building. This must appear
under the BUILDING_TYPES constant of the honeybee_energy.lib.programtypes
module to be successful.
"""
program = building_program_type_by_identifier(building_type)
self.set_all_room_2d_program_type(program)
[docs] def set_all_room_2d_hvac(self, hvac, conditioned_only=True):
"""Set all children Room2Ds of this Building to have the same HVAC system.
Args:
hvac: An HVAC system with properties that will be assigned to all
children Room2Ds.
conditioned_only: Boolean to note whether the input hvac should only
be applied to rooms that are already conditioned. If False, the
hvac will be applied to all rooms. (Default: True).
"""
assert isinstance(hvac, _HVACSystem), 'Expected HVACSystem for Building.' \
'set_all_room_2d_hvac. Got {}'.format(type(hvac))
new_hvac = hvac.duplicate()
new_hvac._identifier = '{}_{}'.format(hvac.identifier, self.host.identifier)
for room_2d in self.host.unique_room_2ds:
if not conditioned_only or room_2d.properties.energy.is_conditioned:
room_2d.properties.energy.hvac = new_hvac
[docs] def add_default_ideal_air(self):
"""Add a default IdealAirSystem to all children Room2Ds of this Story.
The identifier of the systems will be derived from the room identifiers.
"""
for room_2d in self.host.unique_room_2ds:
room_2d.properties.energy.add_default_ideal_air()
[docs] def set_all_room_2d_shw(self, shw):
"""Set all children Room2Ds of this Building to have the same SHW system.
Args:
shw: A Service Hot Water (SHW) system with properties that will be
assigned to all children Room2Ds.
"""
assert isinstance(shw, SHWSystem), 'Expected SHWSystem for Building.' \
'set_all_room_2d_shw. Got {}'.format(type(shw))
new_shw = shw.duplicate()
new_shw._identifier = '{}_{}'.format(shw.identifier, self.host.identifier)
for room_2d in self.host.unique_room_2ds:
room_2d.properties.energy.shw = new_shw
[docs] def diversify(self, occupancy_stdev=20, lighting_stdev=20,
electric_equip_stdev=20, gas_equip_stdev=20, hot_water_stdev=20,
infiltration_stdev=20, schedule_offset=1, timestep=1):
"""Diversify the ProgramTypes assigned to this Building's Room2Ds.
This method uses a random number generator and gaussian distribution to
generate loads that vary about the original "mean" programs. Note that the
randomly generated values can be set to something predictable by using the
native Python random.seed() method before running this method.
In addition to diversifying load values, approximately 2/3 of the schedules
in the resulting Room2Ds will be offset from the mean by the input
schedule_offset (1/3 ahead and another 1/3 behind).
Args:
occupancy_stdev: A number between 0 and 100 for the percent of the
occupancy people_per_area representing one standard deviation
of diversification from the mean. (Default 20 percent).
lighting_stdev: A number between 0 and 100 for the percent of the
lighting watts_per_area representing one standard deviation
of diversification from the mean. (Default 20 percent).
electric_equip_stdev: A number between 0 and 100 for the percent of the
electric equipment watts_per_area representing one standard deviation
of diversification from the mean. (Default 20 percent).
gas_equip_stdev: A number between 0 and 100 for the percent of the
gas equipment watts_per_area representing one standard deviation
of diversification from the mean. (Default 20 percent).
hot_water_stdev: A number between 0 and 100 for the percent of the
service hot water flow_per_area representing one standard deviation
of diversification from the mean. (Default 20 percent).
infiltration_stdev: A number between 0 and 100 for the percent of the
infiltration flow_per_exterior_area representing one standard deviation
of diversification from the mean. (Default 20 percent).
schedule_offset: A positive integer for the number of timesteps at which all
schedules of the resulting programs will be shifted - roughly 1/3 of
the programs ahead and another 1/3 behind. (Default: 1).
timestep: An integer for the number of timesteps per hour at which the
shifting is occurring. This must be a value between 1 and 60, which
is evenly divisible by 60. 1 indicates that each step is an hour
while 60 indicates that each step is a minute. (Default: 1).
"""
# build a dictionary with the unique ProgramTypes and their assigned rooms
program_dict = {}
for room_2d in self.host.unique_room_2ds:
p_type = room_2d.properties.energy.program_type
try: # see if we have already found the program
program_dict[p_type.identifier][1].append(room_2d)
except KeyError: # this is the firs time encountering the program
program_dict[p_type.identifier] = [p_type, [room_2d]]
# loop through the dictionary and generate + assign diversified programs
for prog_list in program_dict.values():
prog, rooms = prog_list[0], prog_list[1]
div_programs = prog.diversify(
len(rooms), occupancy_stdev, lighting_stdev, electric_equip_stdev,
gas_equip_stdev, hot_water_stdev, infiltration_stdev,
schedule_offset, timestep)
for room, d_prog in zip(rooms, div_programs):
room.properties.energy.program_type = d_prog
[docs] @classmethod
def from_dict(cls, data, host):
"""Create BuildingEnergyProperties from a dictionary.
Note that the dictionary must be a non-abridged version for this
classmethod to work.
Args:
data: A dictionary representation of BuildingEnergyProperties.
host: A Building object that hosts these properties.
"""
assert data['type'] == 'BuildingEnergyProperties', \
'Expected BuildingEnergyProperties. Got {}.'.format(data['type'])
new_prop = cls(host)
if 'construction_set' in data and data['construction_set'] is not None:
new_prop.construction_set = \
ConstructionSet.from_dict(data['construction_set'])
return new_prop
[docs] def apply_properties_from_dict(self, abridged_data, construction_sets):
"""Apply properties from a BuildingEnergyPropertiesAbridged dictionary.
Args:
abridged_data: A BuildingEnergyPropertiesAbridged dictionary (typically
coming from a Model).
construction_sets: A dictionary of ConstructionSets with identifiers
of the sets as keys, which will be used to re-assign construction_sets.
"""
if 'construction_set' in abridged_data and \
abridged_data['construction_set'] is not None:
self.construction_set = construction_sets[abridged_data['construction_set']]
[docs] def to_dict(self, abridged=False):
"""Return Building energy properties as a dictionary.
Args:
abridged: Boolean for whether the full dictionary of the Building should
be written (False) or just the identifier of the the individual
properties (True). Default: False.
"""
base = {'energy': {}}
base['energy']['type'] = 'BuildingEnergyProperties' if not \
abridged else 'BuildingEnergyPropertiesAbridged'
# write the ConstructionSet into the dictionary
if self._construction_set is not None:
base['energy']['construction_set'] = \
self._construction_set.identifier if abridged else \
self._construction_set.to_dict()
return base
[docs] def apply_properties_from_geojson_dict(self, data):
"""Apply properties from a geoJSON dictionary.
Args:
data: A dictionary representation of a geoJSON feature properties.
Specifically, this should be the "properties" key describing
a Polygon or MultiPolygon object.
"""
# determine the vintage of the building
template = data['template'] if 'template' in data else '90.1-2019'
vintage = self._VINTAGE_MAP[template]
# assign the construction set based on climate zone
if 'climate_zone' in data:
zone_int = str(data['climate_zone'])[0]
c_set_id = '{}::{}{}::SteelFramed'.format(
vintage[0], 'ClimateZone', zone_int)
try:
self.construction_set = construction_set_by_identifier(c_set_id)
except ValueError: # not a construction set in the library
pass
# assign the program based on the building type
if 'building_type' in data:
try:
self.set_all_program_type_from_building_type(data['building_type'])
except ValueError: # not a building type in the library
pass
# assign the HVAC based on the name
if 'system_type' in data:
hvac_instance = self._hvac_from_long_name(data['system_type'], vintage[1])
if hvac_instance is not None:
self.set_all_room_2d_hvac(hvac_instance, False)
[docs] def duplicate(self, new_host=None):
"""Get a copy of this object.
new_host: A new Building object that hosts these properties.
If None, the properties will be duplicated with the same host.
"""
_host = new_host or self._host
return BuildingEnergyProperties(_host, self._construction_set)
def _hvac_from_long_name(self, hvac_long_name, vintage='ASHRAE_2013'):
"""Get an HVAC class instance from it's long name (as found in a geoJSON)."""
hvac_reg = None
if BuildingEnergyProperties._HVAC_REGISTRY is None:
ext_folder = [f for f in folders.standards_extension_folders
if f.endswith('honeybee_energy_standards')]
if len(ext_folder) == 1:
hvac_reg = os.path.join(ext_folder[0], 'hvac_registry.json')
if os.path.isfile(hvac_reg):
with open(hvac_reg, 'r') as f:
BuildingEnergyProperties._HVAC_REGISTRY = json.load(f)
BuildingEnergyProperties._HVAC_REGISTRY['Ideal Air System'] = \
'IdealAirSystem'
hvac_reg = BuildingEnergyProperties._HVAC_REGISTRY
if hvac_reg is not None:
try:
hvac_class = self._HVAC_TYPES_DICT[hvac_reg[hvac_long_name]]
except KeyError: # HVAC type is not in the library
return None
if hvac_class is IdealAirSystem:
return IdealAirSystem('{} {}'.format(self.host.identifier, 'Ideal Air'))
else: # assume it is an HVAC template
hvac_id = '{} {}'.format(self.host.identifier, hvac_reg[hvac_long_name])
return hvac_class(hvac_id, vintage, hvac_reg[hvac_long_name])
[docs] def ToString(self):
return self.__repr__()
def __repr__(self):
return 'Building Energy Properties: {}'.format(self.host.identifier)