hub/city_model_structure/building.py

444 lines
12 KiB
Python

"""
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 thermal_zone in self.thermal_zones:
if thermal_zone.is_heated:
return thermal_zone.is_heated
return False
@property
def is_cooled(self):
"""
Get building cooled flag
:return: Boolean
"""
for thermal_zone in self.thermal_zones:
if thermal_zone.is_cooled:
return thermal_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)