Source code for dragonfly_energy.properties.building

# coding=utf-8
"""Building Energy Properties."""
import os
import json

from ladybug.header import Header
from ladybug.datacollection import HourlyContinuousCollection
from ladybug.datatype.power import Power
from ladybug.datatype.time import Time

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 * des_cooling_load * des_heating_load * des_hot_water_load * has_des_loads """ _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', '_des_cooling_load', '_des_heating_load', '_des_hot_water_load' ) def __init__(self, host, construction_set=None): """Initialize Building energy properties.""" self._host = host self.construction_set = construction_set self._des_cooling_load = None # can be set later self._des_heating_load = None # can be set later self._des_hot_water_load = None # can be set later @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 @property def des_cooling_load(self): """Get or set an optional data collection for building cooling loads for a DES. Note that any data collection input here must be an HourlyContinuousCollection, it must be annual, and it must have a data type of Power in Watts. """ return self._des_cooling_load @des_cooling_load.setter def des_cooling_load(self, value): if value is not None: value = self._check_data_coll(value, 'DES Cooling') self._des_cooling_load = value @property def des_heating_load(self): """Get or set an optional data collection for building heating loads for a DES. Note that any data collection input here must be an HourlyContinuousCollection, it must be annual, and it must have a data type of Power in Watts. """ return self._des_heating_load @des_heating_load.setter def des_heating_load(self, value): if value is not None: value = self._check_data_coll(value, 'DES Heating') self._des_heating_load = value @property def des_hot_water_load(self): """Get or set an optional data collection for building hot water loads for a DES. Note that any data collection input here must be an HourlyContinuousCollection, it must be annual, and it must have a data type of Power in Watts. """ return self._des_hot_water_load @des_hot_water_load.setter def des_hot_water_load(self, value): if value is not None: value = self._check_data_coll(value, 'DES Hot Water') self._des_hot_water_load = value @property def has_des_loads(self): """Get a boolean for whether this Building has DES loads assigned to it.""" return self._des_cooling_load is not None or self._des_heating_load is not None \ or self._des_hot_water_load is not None
[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']) if 'des_cooling_load' in data and data['des_cooling_load'] is not None: new_prop.des_cooling_load = \ HourlyContinuousCollection.from_dict(data['des_cooling_load']) if 'des_heating_load' in data and data['des_heating_load'] is not None: new_prop.des_heating_load = \ HourlyContinuousCollection.from_dict(data['des_heating_load']) if 'des_hot_water_load' in data and data['des_hot_water_load'] is not None: new_prop.des_heating_load = \ HourlyContinuousCollection.from_dict(data['des_hot_water_load']) 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']] if 'des_cooling_load' in abridged_data and \ abridged_data['des_cooling_load'] is not None: self.des_cooling_load = \ HourlyContinuousCollection.from_dict(abridged_data['des_cooling_load']) if 'des_heating_load' in abridged_data and \ abridged_data['des_heating_load'] is not None: self.des_heating_load = \ HourlyContinuousCollection.from_dict(abridged_data['des_heating_load']) if 'des_hot_water_load' in abridged_data and \ abridged_data['des_hot_water_load'] is not None: self.des_heating_load = \ HourlyContinuousCollection.from_dict(abridged_data['des_hot_water_load'])
[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 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 properties 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() if self._des_cooling_load is not None: base['energy']['des_cooling_load'] = self._des_cooling_load.to_dict() if self._des_heating_load is not None: base['energy']['des_heating_load'] = self._des_heating_load.to_dict() if self._des_hot_water_load is not None: base['energy']['des_hot_water_load'] = self._des_hot_water_load.to_dict() return base
[docs] def to_building_load_csv(self): """Get a CSV file string of building loads for DES simulation.""" time_col, cool, heat, water = self._building_loads() total = cool + heat header = ( 'SecondsFromStart', 'TotalSensibleLoad', 'TotalCoolingSensibleLoad', 'TotalHeatingSensibleLoad', 'TotalWaterHeating' ) file_lines = [','.join(header)] for s, t, c, h, hw in zip(time_col, total, cool, heat, water): text_vals = [str(v) for v in (s, t, c, h, hw)] file_lines.append(','.join(text_vals)) return '\n'.join(file_lines)
[docs] def to_building_load_mos(self): """Get a MOS file string of building loads for DES simulation.""" time_col, cool, heat, water = self._building_loads() file_lines = [ '#1', '#Exported loads from Dragonfly', '\n', '#First column: Seconds in the year (loads are hourly)', '#Second column: cooling loads in Watts (as negative numbers).', '#Third column: space heating loads in Watts', '#Fourth column: water heating loads in Watts', '\n' ] file_lines.append('#Peak space cooling load = {} Watts'.format(cool.min)) file_lines.append('#Peak space heating load = {} Watts'.format(cool.max)) file_lines.append('#Peak water heating load = {} Watts'.format(water.max)) file_lines.append('double tab1({},4)'.format(len(time_col))) for s, c, h, hw in zip(time_col, cool, heat, water): text_vals = [str(v) for v in (s, c, h, hw)] file_lines.append(';'.join(text_vals)) return '\n'.join(file_lines)
[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 new_prop = BuildingEnergyProperties(_host, self._construction_set) new_prop._des_cooling_load = self._des_cooling_load new_prop._des_heating_load = self._des_heating_load new_prop._des_hot_water_load = self._des_hot_water_load return new_prop
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]) def _building_loads(self): """Get data collections for cooling, heating, and hot water.""" assert self.has_des_loads, 'Building "{}" has no building loads assigned ' \ 'to it for DES simulation.'.format(self.host.display_name) base_col = self._base_load_collection() a_per = base_col.header.analysis_period def_vals = [0] * len(base_col) def_col = HourlyContinuousCollection(Header(Power(), 'W', a_per), def_vals) cool = self._des_cooling_load if self._des_cooling_load is not None else def_col heat = self._des_heating_load if self._des_heating_load is not None else def_col water = self._des_hot_water_load \ if self._des_hot_water_load is not None else def_col # negate cooling as DES simulation needs it that way neg_cool_vals = [] for val in cool.values: v = -val if val != 0 else val neg_cool_vals.append(v) cool.values = neg_cool_vals # make a collection for time in seconds sec_step = int(3600.0 / a_per.timestep) time_vals = list(range(sec_step, sec_step * (len(base_col) + 1), sec_step)) time_col = HourlyContinuousCollection(Header(Time(), 'sec', a_per), time_vals) return time_col, cool, heat, water def _base_load_collection(self): """Get a data collection to serve as the basis for writing DES loads.""" if self._des_cooling_load is not None: return self._des_cooling_load if self._des_heating_load is not None: return self._des_heating_load if self._des_heating_load is not None: return self._des_hot_water_load @staticmethod def _check_data_coll(value, name): """Check the data type and units of a Data Collection.""" assert isinstance(value, HourlyContinuousCollection), 'Expected ' \ 'HourlyContinuousCollection for {}. Got {}'.format(name, type(value)) assert value.header.analysis_period.is_annual, '{} data analysis period ' \ 'is not annual. {}'.format(name, value.header.analysis_period) assert isinstance(value.header.data_type, Power), '{} must be Power in W. ' \ 'Got {} in {}'.format(name, value.header.data_type.name, value.header.unit) if value.header.unit != 'W': value = value.to_unit('W') return value
[docs] def ToString(self): return self.__repr__()
def __repr__(self): return 'Building Energy Properties: {}'.format(self.host.identifier)