""" Building module SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca contributors: Pilar Monsalvete pilar_monsalvete@yahoo.es """ from typing import List import matplotlib.patches as patches import numpy as np from matplotlib import pylab from shapely import ops from shapely.geometry import MultiPolygon import pandas as pd import helpers.constants as cte from city_model_structure.attributes.surface import Surface from city_model_structure.attributes.thermal_boundary import ThermalBoundary from city_model_structure.attributes.thermal_zone import ThermalZone from city_model_structure.attributes.usage_zone import UsageZone from city_model_structure.city_object import CityObject from city_model_structure.building_unit import BuildingUnit class Building(CityObject): """ Building(CityObject) class """ def __init__(self, name, lod, surfaces, terrains, year_of_construction, function, lower_corner, attic_heated=0, basement_heated=0): super().__init__(lod, surfaces, name) self._basement_heated = basement_heated self._attic_heated = attic_heated self._terrains = terrains self._year_of_construction = year_of_construction self._function = function self._lower_corner = lower_corner self._average_storey_height = None self._storeys_above_ground = None self._foot_print = None self._usage_zones = [] self._building_units = [] self._type = 'building' self._m_heating = pd.DataFrame() self._h_heating = pd.DataFrame() self._heating = pd.DataFrame() self._m_cooling = pd.DataFrame() self._h_cooling = pd.DataFrame() self._cooling = pd.DataFrame() self._external_temperature = dict() self._global_horizontal = dict() self._diffuse = dict() self._beam = dict() # ToDo: Check this for LOD4 self._thermal_zones = [] if self.lod < 8: # for lod under 4 is just one thermal zone self._thermal_zones.append(ThermalZone(self.surfaces)) for t_zones in self._thermal_zones: t_zones.bounded = [ThermalBoundary(s, [t_zones]) for s in t_zones.surfaces] surface_id = 0 for surface in self._surfaces: surface.lower_corner = self._lower_corner surface.parent(self, surface_id) surface_id += 1 @property def usage_zones(self) -> List[UsageZone]: """ Get city object usage zones :return: [UsageZone] """ return self._usage_zones @usage_zones.setter def usage_zones(self, values): """ Set city objects usage zones :param values: [UsageZones] :return: None """ # ToDo: this is only valid for one usage zone need to be revised for multiple usage zones. self._usage_zones = values for thermal_zone in self.thermal_zones: thermal_zone.usage_zones = [(100, usage_zone) for usage_zone in values] @property def terrains(self) -> List[Surface]: """ Get city object terrain surfaces :return: [Surface] """ return self._terrains @property def attic_heated(self): """ Get if the city object attic is heated :return: Boolean """ return self._attic_heated @attic_heated.setter def attic_heated(self, value): """ Set if the city object attic is heated :param value: Boolean :return: None """ self._attic_heated = value @property def basement_heated(self): """ Get if the city object basement is heated :return: Boolean """ return self._basement_heated @basement_heated.setter def basement_heated(self, value): """ Set if the city object basement is heated :param value: Boolean :return: None """ self._attic_heated = value @property def name(self): """ City object name :return: str """ return self._name @property def thermal_zones(self) -> List[ThermalZone]: """ City object thermal zones :return: [ThermalZone] """ return self._thermal_zones @property def heated_volume(self): """ City object heated volume in cubic meters :return: float """ # ToDo: this need to be the calculated based on the basement and attic heated values return self.volume @property def year_of_construction(self): """ City object year of construction :return: int """ return self._year_of_construction @property def function(self): """ City object function :return: str """ return self._function @property def average_storey_height(self): """ Get city object average storey height in meters :return: float """ return self._average_storey_height @average_storey_height.setter def average_storey_height(self, value): """ Set city object average storey height in meters :param value: float :return: None """ self._average_storey_height = value @property def storeys_above_ground(self): """ Get city object storeys number above ground :return: int """ return self._storeys_above_ground @storeys_above_ground.setter def storeys_above_ground(self, value): """ Set city object storeys number above ground :param value: int :return: """ self._storeys_above_ground = value @staticmethod def _tuple_to_point(xy_tuple): return [xy_tuple[0], xy_tuple[1], 0.0] def _plot(self, polygon): points = () for point_tuple in polygon.exterior.coords: almost_equal = False for point in points: point_1 = Building._tuple_to_point(point) point_2 = Building._tuple_to_point(point_tuple) if self._geometry.almost_equal(point_1, point_2): almost_equal = True break if not almost_equal: points = points + (point_tuple,) points = points + (points[0],) pylab.scatter([point[0] for point in points], [point[1] for point in points]) pylab.gca().add_patch(patches.Polygon(points, closed=True, fill=True)) pylab.grid() pylab.show() @property def foot_print(self) -> Surface: """ City object foot print surface :return: Surface """ if self._foot_print is None: shapelys = [] union = None for surface in self.surfaces: if surface.shapely.is_empty or not surface.shapely.is_valid: continue shapelys.append(surface.shapely) union = ops.unary_union(shapelys) shapelys = [union] if isinstance(union, MultiPolygon): Exception('foot print returns a multipolygon') points_list = [] for point_tuple in union.exterior.coords: # ToDo: should be Z 0.0 or min Z? point = Building._tuple_to_point(point_tuple) almost_equal = False for existing_point in points_list: if self._geometry.almost_equal(point, existing_point): almost_equal = True break if not almost_equal: points_list.append(point) points_list = np.reshape(points_list, len(points_list) * 3) points = np.array_str(points_list).replace('[', '').replace(']', '') self._foot_print = Surface(points, remove_last=False, is_projected=True) return self._foot_print @property def type(self): """ building type :return: str """ return self._type @property def building_units(self) -> [BuildingUnit]: """ Building units :return: """ return self._building_units @building_units.setter def building_units(self, value): """ Building units :param value: [BuildingUnit] """ self._building_units = value @property def _monthly_heating(self) -> pd.DataFrame: """ building monthly heating values in Watts-hour :return: DataFrame with 12 values and a header with the source of those """ return self._m_heating @_monthly_heating.setter def _monthly_heating(self, value): """ building monthly heating values in Watts-hour and a header with the source :param value: DataFrame(heating demand) """ if self._m_heating.empty: self._m_heating = value else: self._m_heating = pd.concat([self._m_heating, value], axis=1) @property def _hourly_heating(self) -> pd.DataFrame: """ building hourly heating values in Watts-hour :return: DataFrame with 8760 values and a header with the source of those """ return self._h_heating @_hourly_heating.setter def _hourly_heating(self, value): """ building hourly heating values in Watts-hour and a header with the source :param value: DataFrame(heating demand) """ if self._h_heating.empty: self._h_heating = value else: self._h_heating = pd.concat([self._h_heating, value], axis=1) def heating(self, time_scale): """ Get heating demand in Wh in a defined time_scale :param time_scale: string. :return: DataFrame(float) """ if time_scale == cte.time_scale['hour']: self._heating = self._hourly_heating elif time_scale == cte.time_scale['month']: self._heating = self._monthly_heating else: raise NotImplementedError return self._heating @property def _monthly_cooling(self) -> pd.DataFrame: """ building monthly cooling values in Watts-hour :return: DataFrame with 12 values and a header with the source of those """ return self._m_cooling @_monthly_cooling.setter def _monthly_cooling(self, value): """ building monthly cooling values in Watts-hour and a header with the source :param value: DataFrame(cooling demand) """ if self._m_cooling.empty: self._m_cooling = value else: self._m_cooling = pd.concat([self._m_cooling, value], axis=1) @property def _hourly_cooling(self) -> pd.DataFrame: """ building hourly cooling values in Watts-hour :return: DataFrame with 8760 values and a header with the source of those """ return self._h_cooling @_hourly_cooling.setter def _hourly_cooling(self, value): """ building hourly cooling values in Watts-hour and a header with the source :param value: DataFrame(cooling demand) """ if self._h_cooling.empty: self._h_cooling = value else: self._h_cooling = pd.concat([self._h_cooling, value], axis=1) def cooling(self, time_scale): """ Get cooling demand in Wh in a defined time_scale :param time_scale: string. :return: DataFrame(float) """ if time_scale == cte.time_scale['hour']: self._cooling = self._hourly_cooling elif time_scale == cte.time_scale['month']: self._cooling = self._monthly_cooling else: raise NotImplementedError return self._cooling @property def external_temperature(self) -> dict: """ external temperature surrounding the building in grads Celsius :return: dict{DataFrame(float)} """ return self._external_temperature @external_temperature.setter def external_temperature(self, value): """ external temperature surrounding the building in grads Celsius :param value: dict{DataFrame(external temperature)} """ self._external_temperature = value @property def global_horizontal(self) -> dict: """ global horizontal radiation surrounding the building in W/m2 :return: dict{DataFrame(float)} """ return self._global_horizontal @global_horizontal.setter def global_horizontal(self, value): """ global horizontal radiation surrounding the building in W/m2 :param value: dict{DataFrame(global horizontal radiation)} """ self._global_horizontal = value @property def diffuse(self) -> dict: """ diffuse radiation surrounding the building in W/m2 :return: dict{DataFrame(float)} """ return self._diffuse @diffuse.setter def diffuse(self, value): """ diffuse radiation surrounding the building in W/m2 :param value: dict{DataFrame(diffuse radiation)} """ self._diffuse = value @property def beam(self) -> dict: """ beam radiation surrounding the building in W/m2 :return: dict{DataFrame(float)} """ return self._beam @beam.setter def beam(self, value): """ beam radiation surrounding the building in W/m2 :param value: dict{DataFrame(beam radiation)} """ self._beam = value