Source code for archetypal.template.zonedefinition

"""archetypal ZoneDefinition module."""

import collections
import sqlite3
import time

from eppy.bunch_subclass import BadEPFieldError
from sigfig import round
from validator_collection import validators

from archetypal.template.conditioning import ZoneConditioning
from archetypal.template.constructions.internal_mass import InternalMass
from archetypal.template.constructions.opaque_construction import OpaqueConstruction
from archetypal.template.dhw import DomesticHotWaterSetting
from archetypal.template.load import ZoneLoad
from archetypal.template.umi_base import UmiBase
from archetypal.template.ventilation import VentilationSetting
from archetypal.template.window_setting import WindowSetting
from archetypal.template.zone_construction_set import ZoneConstructionSet
from archetypal.utils import log, settings


[docs]class ZoneDefinition(UmiBase): """Zone settings class. .. image:: ../images/template/zoneinfo-zone.png """ __slots__ = ( "_internal_mass_exposed_per_floor_area", "_constructions", "_loads", "_conditioning", "_ventilation", "_domestic_hot_water", "_windows", "_occupants", "_daylight_mesh_resolution", "_daylight_workplane_height", "_internal_mass_construction", "_is_part_of_conditioned_floor_area", "_is_part_of_total_floor_area", "_zone_surfaces", "_volume", "_multiplier", "_area", "_is_core", ) def __init__( self, Name, Constructions=None, Loads=None, Conditioning=None, Ventilation=None, DomesticHotWater=None, DaylightMeshResolution=1, DaylightWorkplaneHeight=0.8, InternalMassConstruction=None, InternalMassExposedPerFloorArea=1.05, Windows=None, area=1, volume=1, occupants=1, is_part_of_conditioned_floor_area=True, is_part_of_total_floor_area=True, multiplier=1, zone_surfaces=None, is_core=False, **kwargs, ): """Initialize :class:`Zone` object. Args: Name (str): Name of the object. Must be Unique. Constructions (ZoneConstructionSet): Loads (ZoneLoad): Loads of the zone defined with the lights, equipment and occupancy parameters (see :class:`ZoneLoad`) Conditioning (ZoneConditioning): Conditioning of the zone defined with heating/cooling and mechanical ventilation parameters (see :class:`ZoneConditioning`) Ventilation (VentilationSetting): Ventilation settings of the zone defined with the infiltration rate and natural ventilation parameters (see :class:`VentilationSetting`) DomesticHotWater (archetypal.template.dhw.DomesticHotWaterSetting): DaylightMeshResolution (float): DaylightWorkplaneHeight (float): InternalMassConstruction (archetypal.OpaqueConstruction): InternalMassExposedPerFloorArea: Windows (WindowSetting): The WindowSetting object associated with this zone. area (float): volume (float): occupants (float): **kwargs: """ super(ZoneDefinition, self).__init__(Name, **kwargs) self.Ventilation = Ventilation self.Loads = Loads self.Conditioning = Conditioning self.Constructions = Constructions self.DaylightMeshResolution = DaylightMeshResolution self.DaylightWorkplaneHeight = DaylightWorkplaneHeight self.DomesticHotWater = DomesticHotWater self.InternalMassConstruction = InternalMassConstruction self.InternalMassExposedPerFloorArea = InternalMassExposedPerFloorArea self.Windows = Windows # This is not used in to_dict() if zone_surfaces is None: zone_surfaces = [] self.zone_surfaces = zone_surfaces self.area = area self.volume = volume self.occupants = occupants self.is_part_of_conditioned_floor_area = is_part_of_conditioned_floor_area self.is_part_of_total_floor_area = is_part_of_total_floor_area self.multiplier = multiplier self.is_core = is_core @property def Constructions(self): """Get or set the ZoneConstructionSet object.""" return self._constructions @Constructions.setter def Constructions(self, value): if value is not None: assert isinstance(value, ZoneConstructionSet), ( f"Input value error. Constructions must be of " f"type {ZoneConstructionSet}, not {type(value)}." ) self._constructions = value @property def Loads(self): """Get or set the ZoneLoad object.""" return self._loads @Loads.setter def Loads(self, value): if value is not None: assert isinstance(value, ZoneLoad), ( f"Input value error. Loads must be of " f"type {ZoneLoad}, not {type(value)}." ) self._loads = value @property def Conditioning(self): """Get or set the ZoneConditioning object.""" return self._conditioning @Conditioning.setter def Conditioning(self, value): if value is not None: assert isinstance(value, ZoneConditioning), ( f"Input value error. Conditioning must be of " f"type {ZoneConditioning}, not {type(value)}." ) self._conditioning = value @property def Ventilation(self): """Get or set the VentilationSetting object.""" return self._ventilation @Ventilation.setter def Ventilation(self, value): if value is not None: assert isinstance(value, VentilationSetting), ( f"Input value error. Ventilation must be of " f"type {VentilationSetting}, not {type(value)}." ) self._ventilation = value @property def DomesticHotWater(self): """Get or set the DomesticHotWaterSetting object.""" return self._domestic_hot_water @DomesticHotWater.setter def DomesticHotWater(self, value): if value is not None: assert isinstance(value, DomesticHotWaterSetting), ( f"Input value error. DomesticHotWater must be of " f"type {DomesticHotWaterSetting}, not {type(value)}." ) self._domestic_hot_water = value @property def DaylightMeshResolution(self): """Get or set the daylight mesh resolution [m].""" return self._daylight_mesh_resolution @DaylightMeshResolution.setter def DaylightMeshResolution(self, value): self._daylight_mesh_resolution = validators.float(value, minimum=0) @property def DaylightWorkplaneHeight(self): """Get or set the DaylightWorkplaneHeight [m].""" return self._daylight_workplane_height @DaylightWorkplaneHeight.setter def DaylightWorkplaneHeight(self, value): self._daylight_workplane_height = validators.float(value, minimum=0) @property def InternalMassConstruction(self): """Get or set the internal mass construction object.""" return self._internal_mass_construction @InternalMassConstruction.setter def InternalMassConstruction(self, value): if value is not None: assert isinstance(value, OpaqueConstruction), ( f"Input value error. InternalMassConstruction must be of " f"type {OpaqueConstruction}, not {type(value)}." ) self._internal_mass_construction = value @property def InternalMassExposedPerFloorArea(self): """Get or set the internal mass exposed per floor area [-].""" return self._internal_mass_exposed_per_floor_area @InternalMassExposedPerFloorArea.setter def InternalMassExposedPerFloorArea(self, value): self._internal_mass_exposed_per_floor_area = validators.float(value, minimum=0) @property def Windows(self): """Get or set the WindowSetting object.""" return self._windows @Windows.setter def Windows(self, value): if value is not None: assert isinstance(value, WindowSetting), ( f"Input value error. Windows must be of " f"type {WindowSetting}, not {type(value)}." ) self._windows = value @property def occupants(self): """Get or set the number of occupants in the zone.""" return self._occupants @occupants.setter def occupants(self, value): self._occupants = value @property def area(self): """Get or set the area of the zone [m²].""" return self._area @area.setter def area(self, value): self._area = validators.float(value, minimum=0) @property def volume(self): """Get or set the volume of the zone [m³].""" return self._volume @volume.setter def volume(self, value): self._volume = validators.float(value, minimum=0) @property def is_core(self): """Get or set if the zone is a core zone [bool].""" return self._is_core @is_core.setter def is_core(self, value): assert isinstance(value, bool), value self._is_core = value @property def multiplier(self): """Get or set the zone multiplier. Note: Zone multiplier is designed as a “multiplier” for floor area, zone loads, and energy consumed by internal gains. """ return self._multiplier @multiplier.setter def multiplier(self, value): self._multiplier = validators.integer(value, minimum=1) @property def is_part_of_conditioned_floor_area(self): """Get or set is part of conditioned area [bool].""" return self._is_part_of_conditioned_floor_area @is_part_of_conditioned_floor_area.setter def is_part_of_conditioned_floor_area(self, value): assert isinstance(value, bool) self._is_part_of_conditioned_floor_area = value @property def is_part_of_total_floor_area(self): """Get or set is part od the total building floor area [bool].""" return self._is_part_of_total_floor_area @is_part_of_total_floor_area.setter def is_part_of_total_floor_area(self, value): assert isinstance(value, bool) self._is_part_of_total_floor_area = value @property def zone_surfaces(self): """Get or set the list of surfaces for this zone.""" return self._zone_surfaces @zone_surfaces.setter def zone_surfaces(self, value): self._zone_surfaces = validators.iterable(value, allow_empty=True)
[docs] def to_dict(self): """Return ZoneDefinition dictionary representation.""" self.validate() # Validate object before trying to get json format data_dict = collections.OrderedDict() data_dict["$id"] = str(self.id) data_dict["Conditioning"] = self.Conditioning.to_ref() data_dict["Constructions"] = self.Constructions.to_ref() data_dict["DaylightMeshResolution"] = round(self.DaylightMeshResolution, 2) data_dict["DaylightWorkplaneHeight"] = round(self.DaylightWorkplaneHeight, 2) data_dict["DomesticHotWater"] = self.DomesticHotWater.to_ref() data_dict["InternalMassConstruction"] = self.InternalMassConstruction.to_ref() data_dict["InternalMassExposedPerFloorArea"] = round( self.InternalMassExposedPerFloorArea, 3 ) data_dict["Loads"] = self.Loads.to_ref() data_dict["Ventilation"] = self.Ventilation.to_ref() data_dict["Category"] = self.Category data_dict["Comments"] = validators.string(self.Comments, allow_empty=True) data_dict["DataSource"] = self.DataSource data_dict["Name"] = self.Name return data_dict
[docs] @classmethod def from_dict( cls, data, zone_conditionings, zone_construction_sets, domestic_hot_water_settings, opaque_constructions, zone_loads, ventilation_settings, **kwargs, ): """Create a ZoneDefinition from a dictionary. Args: data (dict): The python dictionary. zone_conditionings (dict): A dictionary of ZoneConditioning objects with their id as keys. zone_construction_sets (dict): A dictionary of ZoneConstructionSet objects with their id as keys. domestic_hot_water_settings (dict): A dictionary of DomesticHotWaterSetting objects with their id as keys. opaque_constructions (dict): A dictionary of OpaqueConstruction objects with their id as keys. zone_loads (dict): A dictionary of ZoneLoad objects with their id as keys. ventilation_settings (dict): A dictionary of ZoneConditioning objects with their id as keys. **kwargs: keywords passed to the constructor. .. code-block:: python { "$id": "175", "Conditioning": { "$ref": "165" }, "Constructions": { "$ref": "168" }, "DaylightMeshResolution": 1.0, "DaylightWorkplaneHeight": 0.8, "DomesticHotWater": { "$ref": "159" }, "InternalMassConstruction": { "$ref": "54" }, "InternalMassExposedPerFloorArea": 1.05, "Loads": { "$ref": "172" }, "Ventilation": { "$ref": "162" }, "Category": "Office Spaces", "Comments": null, "DataSource": "MIT_SDL", "Name": "B_Off_0" } """ _id = data.pop("$id") conditioning = zone_conditionings[data.pop("Conditioning")["$ref"]] construction_set = zone_construction_sets[data.pop("Constructions")["$ref"]] domestic_hot_water_setting = domestic_hot_water_settings[ data.pop("DomesticHotWater")["$ref"] ] internal_mass_construction = opaque_constructions[ data.pop("InternalMassConstruction")["$ref"] ] zone_load = zone_loads[data.pop("Loads")["$ref"]] ventilation_setting = ventilation_settings[data.pop("Ventilation")["$ref"]] return cls( id=_id, Conditioning=conditioning, Constructions=construction_set, DomesticHotWater=domestic_hot_water_setting, InternalMassConstruction=internal_mass_construction, Loads=zone_load, Ventilation=ventilation_setting, **data, **kwargs, )
[docs] @classmethod def from_epbunch(cls, ep_bunch, construct_parents=True, **kwargs): """Create a Zone object from an eppy 'ZONE' epbunch. Args: ep_bunch (eppy.bunch_subclass.EpBunch): The Zone EpBunch. construct_parents (bool): If False, skips construction of parents objects such as Constructions, Conditioning, etc. """ assert ( ep_bunch.key.lower() == "zone" ), f"Expected a `ZONE` epbunch, got {ep_bunch.key}" start_time = time.time() log('Constructing :class:`Zone` for zone "{}"'.format(ep_bunch.Name)) def calc_zone_area(zone_ep): """Get zone area from simulation sql file.""" with sqlite3.connect(zone_ep.theidf.sql_file) as conn: sql_query = """ SELECT t.Value FROM TabularDataWithStrings t WHERE TableName='Zone Summary' and ColumnName='Area' and RowName=? """ (res,) = conn.execute(sql_query, (zone_ep.Name.upper(),)).fetchone() return float(res) def calc_zone_volume(zone_ep): """Get zone volume from simulation sql file.""" with sqlite3.connect(zone_ep.theidf.sql_file) as conn: sql_query = ( "SELECT t.Value FROM TabularDataWithStrings t " "WHERE TableName='Zone Summary' and ColumnName='Volume' and " "RowName=?" ) (res,) = conn.execute(sql_query, (zone_ep.Name.upper(),)).fetchone() return float(res) def calc_zone_occupants(zone_ep): """Get zone occupants from simulation sql file.""" with sqlite3.connect(zone_ep.theidf.sql_file) as conn: sql_query = ( "SELECT t.Value FROM TabularDataWithStrings t " "WHERE TableName='Average Outdoor Air During Occupied Hours' and ColumnName='Nominal Number of Occupants' and RowName=?" ) fetchone = conn.execute(sql_query, (zone_ep.Name.upper(),)).fetchone() (res,) = fetchone or (0,) return float(res) def calc_is_part_of_conditioned_floor_area(zone_ep): """Return True if zone is part of the conditioned floor area.""" with sqlite3.connect(zone_ep.theidf.sql_file) as conn: sql_query = ( "SELECT t.Value FROM TabularDataWithStrings t WHERE " "TableName='Zone Summary' and ColumnName='Conditioned (Y/N)' " "and RowName=?" "" ) res = conn.execute(sql_query, (zone_ep.Name.upper(),)).fetchone() return "Yes" in res def calc_is_part_of_total_floor_area(zone_ep): """Return True if zone is part of the total floor area.""" with sqlite3.connect(zone_ep.theidf.sql_file) as conn: sql_query = ( "SELECT t.Value FROM TabularDataWithStrings t WHERE " "TableName='Zone Summary' and ColumnName='Part of " "Total Floor Area (Y/N)' and RowName=?" ) res = conn.execute(sql_query, (zone_ep.Name.upper(),)).fetchone() return "Yes" in res def calc_multiplier(zone_ep): """Get the zone multiplier from simulation sql.""" with sqlite3.connect(zone_ep.theidf.sql_file) as conn: sql_query = ( "SELECT t.Value FROM TabularDataWithStrings t WHERE " "TableName='Zone Summary' and " "ColumnName='Multipliers' and RowName=?" ) (res,) = conn.execute(sql_query, (zone_ep.Name.upper(),)).fetchone() return int(float(res)) def is_core(zone_ep): # if all surfaces don't have boundary condition == "Outdoors" iscore = True for s in zone_ep.zonesurfaces: try: if (abs(int(s.tilt)) < 180) & (abs(int(s.tilt)) > 0): obc = s.Outside_Boundary_Condition.lower() if obc in ["outdoors", "ground"]: iscore = False break except BadEPFieldError: pass # pass surfaces that don't have an OBC, # eg. InternalMass return iscore name = ep_bunch.Name zone = cls( Name=name, Category=ep_bunch.theidf.name, area=calc_zone_area(ep_bunch), volume=calc_zone_volume(ep_bunch), occupants=calc_zone_occupants(ep_bunch), is_part_of_conditioned_floor_area=calc_is_part_of_conditioned_floor_area( ep_bunch ), is_part_of_total_floor_area=calc_is_part_of_total_floor_area(ep_bunch), multiplier=calc_multiplier(ep_bunch), zone_surfaces=ep_bunch.zonesurfaces, is_core=is_core(ep_bunch), **kwargs, ) if construct_parents: zone.Constructions = ZoneConstructionSet.from_zone(zone, **kwargs) zone.Conditioning = ZoneConditioning.from_zone(zone, ep_bunch, **kwargs) zone.Ventilation = VentilationSetting.from_zone(zone, ep_bunch, **kwargs) zone.DomesticHotWater = DomesticHotWaterSetting.from_zone( ep_bunch, **kwargs ) zone.Loads = ZoneLoad.from_zone(zone, ep_bunch, **kwargs) internal_mass_from_zone = InternalMass.from_zone(ep_bunch) zone.InternalMassConstruction = internal_mass_from_zone.construction zone.InternalMassExposedPerFloorArea = ( internal_mass_from_zone.total_area_exposed_to_zone ) zone.Windows = WindowSetting.from_zone(zone, **kwargs) log( 'completed Zone "{}" constructor in {:,.2f} seconds'.format( ep_bunch.Name, time.time() - start_time ) ) return zone
[docs] def combine(self, other, weights=None, allow_duplicates=False): """Combine two ZoneDefinition objects together. Args: other (ZoneDefinition): The other object. weights (list-like, optional): A list-like object of len 2. If None, the volume of the zones for which self and other belongs is used. Todo: Create Equivalent InternalMassConstruction from partitions when combining zones. Returns: (ZoneDefinition): the combined Zone object. """ # Check if other is None. Simply return self if not self and not other: return None elif self == other: area = 1 if self.area + other.area == 2 else self.area + other.area volume = ( 1 if self.volume + other.volume == 2 else self.volume + other.volume ) new_obj = self.duplicate() new_obj.area = area new_obj.volume = volume return new_obj elif not self or not other: new_obj = (self or other).duplicate() return new_obj # Check if other is the same type as self if not isinstance(other, self.__class__): msg = "Cannot combine %s with %s" % ( self.__class__.__name__, other.__class__.__name__, ) raise NotImplementedError(msg) meta = self._get_predecessors_meta(other) if not weights: zone_weight = settings.zone_weight weights = [ getattr(self, str(zone_weight)), getattr(other, str(zone_weight)), ] log( 'using zone {} "{}" as weighting factor in "{}" ' "combine.".format( zone_weight, " & ".join(list(map(str, map(int, weights)))), self.__class__.__name__, ) ) new_attr = dict( Conditioning=ZoneConditioning.combine( self.Conditioning, other.Conditioning, weights ), Constructions=ZoneConstructionSet.combine( self.Constructions, other.Constructions, weights ), Ventilation=VentilationSetting.combine(self.Ventilation, other.Ventilation), Windows=WindowSetting.combine(self.Windows, other.Windows, weights), DaylightMeshResolution=self.float_mean( other, "DaylightMeshResolution", weights=weights ), DaylightWorkplaneHeight=self.float_mean( other, "DaylightWorkplaneHeight", weights ), DomesticHotWater=DomesticHotWaterSetting.combine( self.DomesticHotWater, other.DomesticHotWater ), InternalMassConstruction=OpaqueConstruction.combine( self.InternalMassConstruction, other.InternalMassConstruction ), InternalMassExposedPerFloorArea=self.float_mean( other, "InternalMassExposedPerFloorArea", weights ), Loads=ZoneLoad.combine(self.Loads, other.Loads, weights), ) new_obj = ZoneDefinition(**meta, **new_attr) # transfer aggregated values [volume, area, occupants] to new combined zone new_obj.volume = self.volume + other.volume new_obj.area = self.area + other.area new_obj.occupants = self.occupants + other.occupants if new_attr["Windows"]: # Could be None new_attr["Windows"].area = new_obj.area new_obj.predecessors.update(self.predecessors + other.predecessors) return new_obj
[docs] def validate(self): """Validate object and fill in missing values.""" if not self.InternalMassConstruction: internal_mass = InternalMass.generic_internalmass_from_zone(self) self.InternalMassConstruction = internal_mass.construction self.InternalMassExposedPerFloorArea = ( internal_mass.total_area_exposed_to_zone ) log( f"While validating {self}, the required attribute " f"'InternalMassConstruction' was filled " f"with {self.InternalMassConstruction} and the " f"'InternalMassExposedPerFloorArea' set to" f" {self.InternalMassExposedPerFloorArea}" ) if self.Conditioning is None: self.Conditioning = ZoneConditioning(Name="Unconditioned Zone") return self
[docs] def mapping(self, validate=True): """Get a dict based on the object properties, useful for dict repr. Args: validate (bool): If True, try to validate object before returning the mapping. """ if validate: self.validate() return dict( Conditioning=self.Conditioning, Constructions=self.Constructions, DaylightMeshResolution=self.DaylightMeshResolution, DaylightWorkplaneHeight=self.DaylightWorkplaneHeight, DomesticHotWater=self.DomesticHotWater, InternalMassConstruction=self.InternalMassConstruction, InternalMassExposedPerFloorArea=self.InternalMassExposedPerFloorArea, Windows=self.Windows, Loads=self.Loads, Ventilation=self.Ventilation, Category=self.Category, Comments=self.Comments, DataSource=self.DataSource, Name=self.Name, area=self.area, volume=self.volume, occupants=self.occupants, is_part_of_conditioned_floor_area=self.is_part_of_conditioned_floor_area, is_part_of_total_floor_area=self.is_part_of_total_floor_area, multiplier=self.multiplier, zone_surfaces=self.zone_surfaces, is_core=self.is_core, )
def __add__(self, other): """Return a combination of self and other.""" return ZoneDefinition.combine(self, other) def __hash__(self): """Return the hash value of self.""" return hash( (self.__class__.__name__, getattr(self, "Name", None), self.DataSource) ) def __eq__(self, other): """Assert self is equivalent to other.""" if not isinstance(other, ZoneDefinition): return NotImplemented else: return all( [ self.Conditioning == other.Conditioning, self.Constructions == other.Constructions, self.DomesticHotWater == other.DomesticHotWater, self.Loads == other.Loads, self.Ventilation == other.Ventilation, self.Windows == other.Windows, self.InternalMassConstruction == other.InternalMassConstruction, self.InternalMassExposedPerFloorArea == other.InternalMassExposedPerFloorArea, self.DaylightMeshResolution == other.DaylightMeshResolution, self.DaylightWorkplaneHeight == other.DaylightWorkplaneHeight, ] ) def __copy__(self): """Return a copy of self.""" return self.__class__(**self.mapping(validate=False))
def resolve_obco(ep_bunch): """Resolve the outside boundary condition of a surface. Args: ep_bunch (EpBunch): The surface for which we are identifying the boundary object. Returns: (EpBunch, EpBunch): A tuple of: EpBunch: The other surface EpBunch: The other zone Notes: Info on the Outside Boundary Condition Object of a surface of type BuildingSurface:Detailed: Non-blank only if the field `Outside Boundary Condition` is *Surface*, *Zone*, *OtherSideCoefficients* or *OtherSideConditionsModel*. If Surface, specify name of corresponding surface in adjacent zone or specify current surface name for internal partition separating like zones. If Zone, specify the name of the corresponding zone and the program will generate the corresponding interzone surface. If Foundation, specify the name of the corresponding Foundation object and the program will calculate the heat transfer appropriately. If OtherSideCoefficients, specify name of SurfaceProperty:OtherSideCoefficients. If OtherSideConditionsModel, specify name of SurfaceProperty:OtherSideConditionsModel. """ obc = ep_bunch.Outside_Boundary_Condition if obc.upper() == "ZONE": name = ep_bunch.Outside_Boundary_Condition_Object adj_zone = ep_bunch.theidf.getobject("ZONE", name) return None, adj_zone elif obc.upper() == "SURFACE": obco = ep_bunch.get_referenced_object("Outside_Boundary_Condition_Object") adj_zone = obco.theidf.getobject("ZONE", obco.Zone_Name) return obco, adj_zone else: return None, None def is_core(zone): """Return true if zone is a core zone. Args: zone (eppy.bunch_subclass.EpBunch): The Zone object. Returns: (bool): Whether the zone is a core zone or not. """ # if all surfaces don't have boundary condition == "Outdoors" iscore = True for s in zone.zonesurfaces: try: if (abs(int(s.tilt)) < 180) & (abs(int(s.tilt)) > 0): obc = s.Outside_Boundary_Condition.lower() if obc in ["outdoors", "ground"]: iscore = False break except BadEPFieldError: pass # pass surfaces that don't have an OBC, # eg. InternalMass return iscore