2020-06-16 15:12:18 -04:00
|
|
|
"""
|
2020-06-16 15:26:55 -04:00
|
|
|
Building module
|
2020-06-16 15:12:18 -04:00
|
|
|
SPDX - License - Identifier: LGPL - 3.0 - or -later
|
|
|
|
Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca
|
2020-10-26 08:26:31 -04:00
|
|
|
contributors: Pilar Monsalvete pilar_monsalvete@yahoo.es
|
2020-06-16 15:12:18 -04:00
|
|
|
"""
|
2020-10-30 13:47:59 -04:00
|
|
|
|
2021-03-02 18:57:09 -05:00
|
|
|
|
2021-03-31 14:17:53 -04:00
|
|
|
import sys
|
2020-06-16 16:19:14 -04:00
|
|
|
from typing import List
|
2021-03-25 09:11:30 -04:00
|
|
|
import numpy as np
|
2021-03-31 14:17:53 -04:00
|
|
|
import math
|
2020-06-16 15:12:18 -04:00
|
|
|
|
2020-10-28 13:14:05 -04:00
|
|
|
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
|
2020-06-16 15:12:18 -04:00
|
|
|
from city_model_structure.city_object import CityObject
|
2021-03-01 16:42:03 -05:00
|
|
|
from helpers.geometry_helper import GeometryHelper as gh
|
2021-03-25 09:11:42 -04:00
|
|
|
from helpers.configuration_helper import ConfigurationHelper
|
2021-03-30 17:57:38 -04:00
|
|
|
from trimesh import Trimesh
|
2021-03-31 14:17:53 -04:00
|
|
|
|
2020-10-21 15:23:06 -04:00
|
|
|
|
2020-06-16 15:12:18 -04:00
|
|
|
class Building(CityObject):
|
|
|
|
"""
|
2020-06-16 15:26:55 -04:00
|
|
|
Building(CityObject) class
|
2020-06-16 15:12:18 -04:00
|
|
|
"""
|
2021-03-16 20:14:40 -04:00
|
|
|
def __init__(self, name, lod, surfaces, terrains, year_of_construction, function, city_lower_corner):
|
2020-06-16 16:19:14 -04:00
|
|
|
super().__init__(lod, surfaces, name)
|
2021-03-02 18:57:09 -05:00
|
|
|
self._basement_heated = None
|
|
|
|
self._attic_heated = None
|
2020-06-16 15:12:18 -04:00
|
|
|
self._terrains = terrains
|
|
|
|
self._year_of_construction = year_of_construction
|
|
|
|
self._function = function
|
2021-03-25 09:11:42 -04:00
|
|
|
self._city_lower_corner = city_lower_corner
|
|
|
|
self._building_lower_corner = None
|
2020-06-16 15:12:18 -04:00
|
|
|
self._average_storey_height = None
|
2020-10-28 12:20:13 -04:00
|
|
|
self._storeys_above_ground = None
|
2021-03-11 14:31:28 -05:00
|
|
|
self._floor_area = None
|
2021-03-25 09:11:30 -04:00
|
|
|
self._roof_type = None
|
2020-06-16 15:12:18 -04:00
|
|
|
self._usage_zones = []
|
|
|
|
self._type = 'building'
|
2020-10-30 08:32:38 -04:00
|
|
|
self._heating = dict()
|
|
|
|
self._cooling = dict()
|
2020-10-30 08:25:36 -04:00
|
|
|
self._external_temperature = dict()
|
|
|
|
self._global_horizontal = dict()
|
|
|
|
self._diffuse = dict()
|
|
|
|
self._beam = dict()
|
2021-03-25 09:11:42 -04:00
|
|
|
self._min_x = ConfigurationHelper().max_coordinate
|
|
|
|
self._min_y = ConfigurationHelper().max_coordinate
|
|
|
|
self._min_z = ConfigurationHelper().max_coordinate
|
|
|
|
self._centroid = None
|
2021-03-31 14:17:53 -04:00
|
|
|
self._eave_height = None
|
2020-06-16 15:12:18 -04:00
|
|
|
|
2021-03-25 09:11:30 -04:00
|
|
|
self._grounds = []
|
|
|
|
self._roofs = []
|
|
|
|
self._walls = []
|
2020-06-16 15:12:18 -04:00
|
|
|
# ToDo: Check this for LOD4
|
|
|
|
self._thermal_zones = []
|
2021-03-01 16:42:03 -05:00
|
|
|
if self.lod < 4:
|
2020-06-16 15:12:18 -04:00
|
|
|
# for lod under 4 is just one thermal zone
|
2021-03-26 12:53:27 -04:00
|
|
|
self._thermal_zones.append(ThermalZone(self.surfaces))
|
2020-06-16 15:12:18 -04:00
|
|
|
|
|
|
|
for t_zones in self._thermal_zones:
|
2021-03-02 18:57:09 -05:00
|
|
|
t_zones.bounded = [ThermalBoundary(s, [t_zones]) for s in t_zones.surfaces]
|
2021-03-11 14:31:28 -05:00
|
|
|
for surface in self.surfaces:
|
2021-03-25 09:11:42 -04:00
|
|
|
self._min_x = min(self._min_x, surface.min_x)
|
|
|
|
self._min_y = min(self._min_y, surface.min_y)
|
|
|
|
self._min_z = min(self._min_z, surface.min_z)
|
2021-03-25 09:11:30 -04:00
|
|
|
if surface.type == 'Ground':
|
|
|
|
self._grounds.append(surface)
|
|
|
|
elif surface.type == 'Wall':
|
|
|
|
self._walls.append(surface)
|
|
|
|
else:
|
|
|
|
self._roofs.append(surface)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def grounds(self) -> [Surface]:
|
|
|
|
"""
|
|
|
|
Building ground surfaces
|
|
|
|
"""
|
|
|
|
return self._grounds
|
|
|
|
|
2021-03-26 12:53:27 -04:00
|
|
|
@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
|
|
|
|
|
2021-03-25 09:11:30 -04:00
|
|
|
@property
|
|
|
|
def roofs(self) -> [Surface]:
|
|
|
|
"""
|
|
|
|
Building roof surfaces
|
|
|
|
"""
|
|
|
|
return self._roofs
|
|
|
|
|
|
|
|
@property
|
|
|
|
def walls(self) -> [Surface]:
|
|
|
|
"""
|
|
|
|
Building wall surfaces
|
|
|
|
"""
|
|
|
|
return self._walls
|
2020-06-16 15:12:18 -04:00
|
|
|
|
|
|
|
@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
|
|
|
|
"""
|
|
|
|
self._usage_zones = values
|
|
|
|
for thermal_zone in self.thermal_zones:
|
2021-03-26 12:53:27 -04:00
|
|
|
thermal_zone.usage_zones = values
|
2020-06-16 15:12:18 -04:00
|
|
|
|
|
|
|
@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
|
|
|
|
"""
|
2021-03-01 16:42:03 -05:00
|
|
|
self._basement_heated = value
|
2020-06-16 15:12:18 -04:00
|
|
|
|
|
|
|
@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
|
|
|
|
"""
|
2020-11-26 09:26:55 -05:00
|
|
|
# ToDo: this need to be calculated based on the basement and attic heated values
|
2021-03-15 11:47:30 -04:00
|
|
|
raise NotImplementedError
|
2020-06-16 15:12:18 -04:00
|
|
|
|
|
|
|
@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]
|
|
|
|
|
|
|
|
@property
|
|
|
|
def type(self):
|
|
|
|
"""
|
2020-10-13 14:47:01 -04:00
|
|
|
building type
|
2020-06-16 15:12:18 -04:00
|
|
|
:return: str
|
|
|
|
"""
|
|
|
|
return self._type
|
2020-10-13 14:47:01 -04:00
|
|
|
|
2020-10-16 06:54:49 -04:00
|
|
|
@property
|
2020-10-30 08:32:38 -04:00
|
|
|
def heating(self) -> dict:
|
2020-10-16 06:54:49 -04:00
|
|
|
"""
|
2020-10-30 08:32:38 -04:00
|
|
|
heating demand in Wh
|
|
|
|
:return: dict{DataFrame(float)}
|
2020-10-28 13:14:05 -04:00
|
|
|
"""
|
|
|
|
return self._heating
|
|
|
|
|
2020-10-30 08:32:38 -04:00
|
|
|
@heating.setter
|
|
|
|
def heating(self, value):
|
2020-10-16 06:54:49 -04:00
|
|
|
"""
|
2020-10-30 08:32:38 -04:00
|
|
|
heating demand in Wh
|
|
|
|
:param value: dict{DataFrame(float)}
|
2020-10-16 08:05:17 -04:00
|
|
|
"""
|
2020-10-30 08:32:38 -04:00
|
|
|
self._heating = value
|
2020-10-16 08:05:17 -04:00
|
|
|
|
|
|
|
@property
|
2020-10-30 08:32:38 -04:00
|
|
|
def cooling(self) -> dict:
|
2020-10-16 08:05:17 -04:00
|
|
|
"""
|
2020-10-30 08:32:38 -04:00
|
|
|
cooling demand in Wh
|
|
|
|
:return: dict{DataFrame(float)}
|
2020-10-28 13:14:05 -04:00
|
|
|
"""
|
2020-10-30 08:32:38 -04:00
|
|
|
return self._cooling
|
2020-10-28 13:14:05 -04:00
|
|
|
|
2020-10-30 08:32:38 -04:00
|
|
|
@cooling.setter
|
|
|
|
def cooling(self, value):
|
2020-10-28 13:14:05 -04:00
|
|
|
"""
|
2020-10-30 08:32:38 -04:00
|
|
|
cooling demand in Wh
|
|
|
|
:param value: dict{DataFrame(float)}
|
2020-10-16 08:05:17 -04:00
|
|
|
"""
|
2020-10-30 08:32:38 -04:00
|
|
|
self._cooling = value
|
2020-10-16 08:05:17 -04:00
|
|
|
|
|
|
|
@property
|
2020-10-30 08:25:36 -04:00
|
|
|
def external_temperature(self) -> dict:
|
2020-10-16 08:05:17 -04:00
|
|
|
"""
|
2020-10-30 08:25:36 -04:00
|
|
|
external temperature surrounding the building in grads Celsius
|
|
|
|
:return: dict{DataFrame(float)}
|
2020-10-16 06:54:49 -04:00
|
|
|
"""
|
2020-10-28 13:14:05 -04:00
|
|
|
return self._external_temperature
|
2020-10-29 07:40:40 -04:00
|
|
|
|
2020-10-30 08:25:36 -04:00
|
|
|
@external_temperature.setter
|
|
|
|
def external_temperature(self, value):
|
2020-10-29 07:40:40 -04:00
|
|
|
"""
|
2020-10-30 08:25:36 -04:00
|
|
|
external temperature surrounding the building in grads Celsius
|
2020-10-30 13:47:59 -04:00
|
|
|
:param value: dict{DataFrame(float)}
|
2020-10-29 07:40:40 -04:00
|
|
|
"""
|
2020-10-30 08:25:36 -04:00
|
|
|
self._external_temperature = value
|
2020-10-29 07:40:40 -04:00
|
|
|
|
|
|
|
@property
|
2020-10-30 08:25:36 -04:00
|
|
|
def global_horizontal(self) -> dict:
|
2020-10-29 07:40:40 -04:00
|
|
|
"""
|
2020-10-30 08:25:36 -04:00
|
|
|
global horizontal radiation surrounding the building in W/m2
|
|
|
|
:return: dict{DataFrame(float)}
|
2020-10-29 07:40:40 -04:00
|
|
|
"""
|
2020-10-30 08:25:36 -04:00
|
|
|
return self._global_horizontal
|
2020-10-29 07:40:40 -04:00
|
|
|
|
2020-10-30 08:25:36 -04:00
|
|
|
@global_horizontal.setter
|
|
|
|
def global_horizontal(self, value):
|
2020-10-29 07:40:40 -04:00
|
|
|
"""
|
2020-10-30 08:25:36 -04:00
|
|
|
global horizontal radiation surrounding the building in W/m2
|
2020-10-30 13:47:59 -04:00
|
|
|
:param value: dict{DataFrame(float)}
|
2020-10-29 07:40:40 -04:00
|
|
|
"""
|
2020-10-30 08:25:36 -04:00
|
|
|
self._global_horizontal = value
|
2020-10-29 07:40:40 -04:00
|
|
|
|
2020-10-30 08:25:36 -04:00
|
|
|
@property
|
|
|
|
def diffuse(self) -> dict:
|
2020-10-29 07:40:40 -04:00
|
|
|
"""
|
2020-10-30 08:25:36 -04:00
|
|
|
diffuse radiation surrounding the building in W/m2
|
|
|
|
:return: dict{DataFrame(float)}
|
2020-10-29 07:40:40 -04:00
|
|
|
"""
|
|
|
|
return self._diffuse
|
|
|
|
|
2020-10-30 08:25:36 -04:00
|
|
|
@diffuse.setter
|
|
|
|
def diffuse(self, value):
|
2020-10-29 07:40:40 -04:00
|
|
|
"""
|
2020-10-30 08:25:36 -04:00
|
|
|
diffuse radiation surrounding the building in W/m2
|
2020-10-30 13:47:59 -04:00
|
|
|
:param value: dict{DataFrame(float)}
|
2020-10-29 07:40:40 -04:00
|
|
|
"""
|
2020-10-30 08:25:36 -04:00
|
|
|
self._diffuse = value
|
2020-10-29 07:40:40 -04:00
|
|
|
|
2020-10-30 08:25:36 -04:00
|
|
|
@property
|
|
|
|
def beam(self) -> dict:
|
2020-10-29 07:40:40 -04:00
|
|
|
"""
|
2020-10-30 08:25:36 -04:00
|
|
|
beam radiation surrounding the building in W/m2
|
|
|
|
:return: dict{DataFrame(float)}
|
2020-10-29 07:40:40 -04:00
|
|
|
"""
|
2020-10-30 08:25:36 -04:00
|
|
|
return self._beam
|
2020-10-29 07:40:40 -04:00
|
|
|
|
2020-10-30 08:25:36 -04:00
|
|
|
@beam.setter
|
|
|
|
def beam(self, value):
|
2020-10-29 07:40:40 -04:00
|
|
|
"""
|
2020-10-30 08:25:36 -04:00
|
|
|
beam radiation surrounding the building in W/m2
|
2020-10-30 13:47:59 -04:00
|
|
|
:param value: dict{DataFrame(float)}
|
2020-10-29 07:40:40 -04:00
|
|
|
"""
|
2020-10-30 08:25:36 -04:00
|
|
|
self._beam = value
|
2021-03-01 16:42:03 -05:00
|
|
|
|
2021-03-25 09:11:42 -04:00
|
|
|
@property
|
|
|
|
def building_lower_corner(self):
|
|
|
|
if self._building_lower_corner is None:
|
|
|
|
self._building_lower_corner = [self._min_x, self._min_y, self._min_z]
|
|
|
|
return self._building_lower_corner
|
|
|
|
|
2021-03-01 16:42:03 -05:00
|
|
|
@property
|
2021-03-31 14:17:53 -04:00
|
|
|
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.max_z)
|
|
|
|
return self._eave_height
|
|
|
|
|
|
|
|
@property
|
|
|
|
def storeys(self) -> [Trimesh]:
|
|
|
|
"""
|
|
|
|
subsections of building trimesh by storage in case of no interiors defined
|
|
|
|
:return: [Trimesh]
|
|
|
|
"""
|
2021-03-16 12:33:22 -04:00
|
|
|
trimesh = self.simplified_polyhedron.trimesh
|
2021-03-31 14:17:53 -04:00
|
|
|
if self.average_storey_height is None:
|
|
|
|
if self.storeys_above_ground is None or self.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 [trimesh]
|
|
|
|
else:
|
|
|
|
number_of_storeys = int(self.storeys_above_ground)
|
|
|
|
height = self.eave_height / number_of_storeys
|
|
|
|
else:
|
|
|
|
height = self.average_storey_height
|
|
|
|
if self.storeys_above_ground is not None:
|
|
|
|
number_of_storeys = int(self.storeys_above_ground)
|
|
|
|
else:
|
|
|
|
number_of_storeys = math.floor(float(self.eave_height) / height) + 1
|
|
|
|
last_storey_height = float(self.eave_height) - height*(number_of_storeys-1)
|
|
|
|
if last_storey_height < 0.3*height:
|
|
|
|
number_of_storeys -= 1
|
|
|
|
|
|
|
|
storeys = []
|
2021-03-01 16:42:03 -05:00
|
|
|
for n in range(0, number_of_storeys - 1):
|
2021-03-25 09:11:42 -04:00
|
|
|
point_plane = [self.building_lower_corner[0], self.building_lower_corner[1],
|
2021-03-31 14:17:53 -04:00
|
|
|
self.building_lower_corner[2] + height * (n + 1)]
|
|
|
|
normal = [0, 0, -1]
|
|
|
|
storey, trimesh = gh.divide_mesh_by_plane(trimesh, normal, point_plane)
|
2021-03-01 16:42:03 -05:00
|
|
|
storeys.append(storey)
|
2021-03-31 14:17:53 -04:00
|
|
|
storeys.append(trimesh)
|
2021-03-01 16:42:03 -05:00
|
|
|
return storeys
|
2021-03-11 14:31:28 -05:00
|
|
|
|
2021-03-25 09:11:30 -04:00
|
|
|
@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
|
|
|
|
|
2021-03-11 14:31:28 -05:00
|
|
|
@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
|
|
|
|
|
2021-03-25 09:11:42 -04:00
|
|
|
@property
|
|
|
|
def centroid(self):
|
|
|
|
if self._centroid is None:
|
|
|
|
self._centroid = self.simplified_polyhedron.centroid
|
|
|
|
return self._centroid
|