""" Building module SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca contributors: Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ import sys from typing import List import numpy as np import math from city_model_structure.building_demand.surface import Surface from city_model_structure.building_demand.thermal_zone import ThermalZone from city_model_structure.building_demand.usage_zone import UsageZone from city_model_structure.building_demand.storey import Storey from city_model_structure.attributes.polygon import Polygon from city_model_structure.attributes.point import Point from city_model_structure.city_object import CityObject from helpers import constants as cte class Building(CityObject): """ Building(CityObject) class """ def __init__(self, name, lod, surfaces, year_of_construction, function, city_lower_corner, terrains=None, divide_in_storeys=False): super().__init__(name, lod, surfaces, city_lower_corner) self._basement_heated = None self._attic_heated = None self._terrains = terrains self._year_of_construction = year_of_construction self._function = function self._average_storey_height = None self._storeys_above_ground = None self._floor_area = None self._roof_type = None self._thermal_zones = [] self._usage_zones = [] self._type = 'building' self._heating = dict() self._cooling = dict() self._eave_height = None self._divide_in_storeys = divide_in_storeys self._grounds = [] self._roofs = [] self._walls = [] self._internal_walls = [] for surface_id, surface in enumerate(self.surfaces): self._min_x = min(self._min_x, surface.lower_corner[0]) self._min_y = min(self._min_y, surface.lower_corner[1]) self._min_z = min(self._min_z, surface.lower_corner[2]) surface.id = surface_id if surface.type == 'Ground': self._grounds.append(surface) elif surface.type == 'Wall': self._walls.append(surface) elif surface.type == 'Roof': self._roofs.append(surface) else: self._internal_walls.append(surface) self._pv_plus_hp_installation = None @property def grounds(self) -> [Surface]: """ Building ground surfaces """ return self._grounds @property def is_heated(self): """ Get building heated flag :return: Boolean """ for usage_zone in self.usage_zones: if usage_zone.is_heated: return usage_zone.is_heated return False @property def is_cooled(self): """ Get building cooled flag :return: Boolean """ for usage_zone in self.usage_zones: if usage_zone.is_cooled: return usage_zone.is_cooled return False @property def roofs(self) -> [Surface]: """ Building roof surfaces """ return self._roofs @property def walls(self) -> [Surface]: """ Building wall surfaces """ return self._walls @property def usage_zones(self) -> List[UsageZone]: """ Get city object usage zones :return: [UsageZone] """ if len(self._usage_zones) == 0: for thermal_zone in self.thermal_zones: self._usage_zones.extend(thermal_zone.usage_zones) return self._usage_zones @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._basement_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] """ if len(self._thermal_zones) == 0: for storey in self.storeys: self._thermal_zones.append(storey.thermal_zone) return self._thermal_zones @property def heated_volume(self): """ City object heated volume in cubic meters :return: float """ # ToDo: this need to be calculated based on the basement and attic heated values raise NotImplementedError @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 @function.setter def function(self, value): """ Set building function :param value: string :return: None """ self._function = value @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] @property def heating(self) -> dict: """ heating demand in Wh :return: dict{DataFrame(float)} """ return self._heating @heating.setter def heating(self, value): """ heating demand in Wh :param value: dict{DataFrame(float)} """ self._heating = value @property def cooling(self) -> dict: """ cooling demand in Wh :return: dict{DataFrame(float)} """ return self._cooling @cooling.setter def cooling(self, value): """ cooling demand in Wh :param value: dict{DataFrame(float)} """ self._cooling = value @property def eave_height(self): """ building eave height in meters :return: float """ if self._eave_height is None: self._eave_height = 0 for wall in self.walls: self._eave_height = max(self._eave_height, wall.upper_corner[2]) return self._eave_height @property def storeys(self) -> [Storey]: """ subsections of building trimesh by storey in case of no interiors defined :return: [Storey] """ number_of_storeys, height = self._calculate_number_storeys_and_height(self.average_storey_height, self.eave_height, self.storeys_above_ground) if not self._divide_in_storeys or number_of_storeys == 1: return [Storey('storey_0', self.surfaces, [None, None], self.volume)] if number_of_storeys == 0: raise Exception(f'Number of storeys cannot be 0') storeys = [] surfaces_child_last_storey = [] rest_surfaces = [] total_volume = 0 for i in range(0, number_of_storeys-1): name = 'storey_' + str(i) surfaces_child = [] if i == 0: neighbours = [None, 'storey_1'] for surface in self.surfaces: if surface.type == cte.GROUND: surfaces_child.append(surface) else: rest_surfaces.append(surface) else: neighbours = ['storey_' + str(i-1), 'storey_' + str(i+1)] height_division = self.lower_corner[2] + height*(i+1) intersections = [] for surface in rest_surfaces: if surface.type == cte.ROOF: if height_division >= surface.upper_corner[2] > height_division-height: surfaces_child.append(surface) else: surfaces_child_last_storey.append(surface) else: surface_child, rest_surface, intersection = surface.divide(height_division) surfaces_child.append(surface_child) intersections.extend(intersection) if i == number_of_storeys-2: surfaces_child_last_storey.append(rest_surface) points = [] for intersection in intersections: points.append(intersection[1]) coordinates = self._intersections_to_coordinates(intersections) polygon = Polygon(coordinates) ceiling = Surface(polygon, polygon, surface_type=cte.INTERIOR_SLAB) surfaces_child.append(ceiling) volume = ceiling.area_above_ground * height total_volume += volume storeys.append(Storey(name, surfaces_child, neighbours, volume)) name = 'storey_' + str(number_of_storeys-1) neighbours = ['storey_' + str(number_of_storeys-2), None] volume = self.volume - total_volume if volume < 0: raise Exception('Error in storeys creation, volume of last storey cannot be lower that 0') storeys.append(Storey(name, surfaces_child_last_storey, neighbours, volume)) return storeys @staticmethod def _calculate_number_storeys_and_height(average_storey_height, eave_height, storeys_above_ground): if average_storey_height is None: if storeys_above_ground is None or storeys_above_ground <= 0: sys.stderr.write('Warning: not enough information to divide building into storeys, ' 'either number of storeys or average storey height must be provided.\n') return 0, 0 else: number_of_storeys = int(storeys_above_ground) height = eave_height / number_of_storeys else: height = float(average_storey_height) if storeys_above_ground is not None: number_of_storeys = int(storeys_above_ground) else: number_of_storeys = math.floor(float(eave_height) / height) + 1 last_storey_height = float(eave_height) - height*(number_of_storeys-1) if last_storey_height < 0.3*height: number_of_storeys -= 1 return number_of_storeys, height @property def roof_type(self): """ Roof type for the building flat or pitch """ if self._roof_type is None: self._roof_type = 'flat' for roof in self.roofs: grads = np.rad2deg(roof.inclination) if 355 > grads > 5: self._roof_type = 'pitch' break return self._roof_type @property def floor_area(self): """ Floor area of the building m2 :return: float """ if self._floor_area is None: self._floor_area = 0 for surface in self.surfaces: if surface.type == 'Ground': self._floor_area += surface.perimeter_polygon.area return self._floor_area @property def pv_plus_hp_installation(self): return self._pv_plus_hp_installation @pv_plus_hp_installation.setter def pv_plus_hp_installation(self, value): self._pv_plus_hp_installation = value @staticmethod def _intersections_to_coordinates(edges_list): # todo: this method is horrible, the while loop needs to be improved points = [Point(edges_list[0][0]), Point(edges_list[0][1])] found_edges = [] j = 0 while j < len(points)-1: for i in range(1, len(edges_list)): if i not in found_edges: point_2 = points[len(points) - 1] point_1 = Point(edges_list[i][0]) found = False if point_1.distance_to_point(point_2) <= 1e-10: points.append(Point(edges_list[i][1])) found_edges.append(i) found = True if not found: point_1 = Point(edges_list[i][1]) if point_1.distance_to_point(point_2) <= 1e-10: points.append(Point(edges_list[i][0])) found_edges.append(i) j += 1 points.remove(points[len(points)-1]) array_points = [] for point in points: array_points.append(point.coordinates) return np.array(array_points)