From a78cb879a0246555367ad434297fd51081c804de Mon Sep 17 00:00:00 2001 From: Pilar Date: Fri, 27 Aug 2021 17:20:24 -0400 Subject: [PATCH] First working version of dynamic building simulation --- .gitignore | 4 + city_model_structure/building.py | 143 ++++-------------- .../building_demand/storey.py | 21 +-- .../building_demand/surface.py | 2 +- .../building_demand/thermal_boundary.py | 21 ++- .../building_demand/thermal_zone.py | 17 +++ imports/construction/ca_physics_parameters.py | 14 +- imports/construction/us_physics_parameters.py | 14 +- imports/geometry/citygml.py | 3 +- imports/schedules_factory.py | 5 + imports/usage_factory.py | 5 + 11 files changed, 106 insertions(+), 143 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..c321b771 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +!.gitignore +/venv/ +.idea/ +/development_tests/ diff --git a/city_model_structure/building.py b/city_model_structure/building.py index 84656713..c04acb42 100644 --- a/city_model_structure/building.py +++ b/city_model_structure/building.py @@ -5,19 +5,14 @@ Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@conc contributors: Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca """ - -import sys from typing import List -import math import numpy as np 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.thermal_boundary import ThermalBoundary 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): @@ -25,7 +20,7 @@ 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): + city_lower_corner, terrains=None): super().__init__(name, lod, surfaces, city_lower_corner) self._basement_heated = None self._attic_heated = None @@ -36,13 +31,14 @@ class Building(CityObject): self._storeys_above_ground = None self._floor_area = None self._roof_type = None + self._storeys = None self._thermal_zones = [] + self._thermal_boundaries = None 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 = [] @@ -298,86 +294,18 @@ class Building(CityObject): @property def storeys(self) -> [Storey]: """ - subsections of building trimesh by storey in case of no interiors defined + Storeys inside the building :return: [Storey] """ - number_of_storeys, height = self._calculate_number_storeys_and_height(self.average_storey_height, self.eave_height, - self.storeys_above_ground) - number_of_storeys = 1 - if not self._divide_in_storeys or number_of_storeys == 1: - return [Storey('storey_0', self.surfaces, [None, None], self.volume)] + return self._storeys - if number_of_storeys == 0: - raise Exception('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 - 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 + @storeys.setter + def storeys(self, value): + """ + Storeys inside the building + :param value: [Storey] + """ + self._storeys = value @property def roof_type(self): @@ -406,31 +334,20 @@ class Building(CityObject): self._floor_area += surface.perimeter_polygon.area return self._floor_area - @staticmethod - def _intersections_to_coordinates(edges_list): - # todo: this method is too complex, 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) + @property + def thermal_boundaries(self) -> List[ThermalBoundary]: + """ + List of all thermal boundaries associated to the building's thermal zones + :return: [ThermalBoundary] + """ + if self._thermal_boundaries is None: + self._thermal_boundaries = [] + for thermal_zone in self.thermal_zones: + _thermal_boundary_duplicated = False + for thermal_boundary in thermal_zone.thermal_boundaries: + if len(thermal_boundary.thermal_zones) > 1: + if thermal_zone != thermal_boundary.thermal_zones[1]: + self._thermal_boundaries.append(thermal_boundary) + else: + self._thermal_boundaries.append(thermal_boundary) + return self._thermal_boundaries diff --git a/city_model_structure/building_demand/storey.py b/city_model_structure/building_demand/storey.py index d3e7d8f6..ad3e841e 100644 --- a/city_model_structure/building_demand/storey.py +++ b/city_model_structure/building_demand/storey.py @@ -6,11 +6,9 @@ Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons from __future__ import annotations from typing import List -import numpy as np from city_model_structure.building_demand.surface import Surface from city_model_structure.building_demand.thermal_boundary import ThermalBoundary from city_model_structure.building_demand.thermal_zone import ThermalZone -import helpers.constants as cte class Storey: @@ -18,10 +16,10 @@ class Storey: """ Storey class """ - def __init__(self, name, surfaces, neighbours, volume): + def __init__(self, name, storey_surfaces, neighbours, volume): # todo: the information of the parent surface is lost -> need to recover it self._name = name - self._surfaces = surfaces + self._storey_surfaces = storey_surfaces self._thermal_boundaries = None self._virtual_surfaces = None self._thermal_zone = None @@ -42,7 +40,7 @@ class Storey: External surfaces enclosing the storey :return: [Surface] """ - return self._surfaces + return self._storey_surfaces @property def neighbours(self): @@ -58,21 +56,10 @@ class Storey: Thermal boundaries bounding the thermal zone :return: [ThermalBoundary] """ - # todo: it cannot be, it creates a loop between thermal boundaries and thermal zones if self._thermal_boundaries is None: self._thermal_boundaries = [] for surface in self.surfaces: - if surface.type != cte.INTERIOR_WALL or surface.type != cte.INTERIOR_SLAB: - # external thermal boundary -> only one thermal zone - delimits = [self.thermal_zone] - else: - # internal thermal boundary -> two thermal zones - grad = np.rad2deg(surface.inclination) - if grad >= 170: - delimits = [self.thermal_zone, self._neighbours[0]] - else: - delimits = [self._neighbours[1], self.thermal_zone] - self._thermal_boundaries.append(ThermalBoundary(surface, delimits)) + self._thermal_boundaries.append(ThermalBoundary(surface)) return self._thermal_boundaries @property diff --git a/city_model_structure/building_demand/surface.py b/city_model_structure/building_demand/surface.py index 8ebd1b38..3c18f2fc 100644 --- a/city_model_structure/building_demand/surface.py +++ b/city_model_structure/building_demand/surface.py @@ -38,7 +38,7 @@ class Surface: self._pv_system_installed = None self._inverse = None # todo: do I need it??? - self._associated_thermal_boundary = None + self._associated_thermal_boundaries = None @property def name(self): diff --git a/city_model_structure/building_demand/thermal_boundary.py b/city_model_structure/building_demand/thermal_boundary.py index 0e1d8ad2..878a9124 100644 --- a/city_model_structure/building_demand/thermal_boundary.py +++ b/city_model_structure/building_demand/thermal_boundary.py @@ -7,18 +7,19 @@ Contributors Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca from typing import List, TypeVar, Union from city_model_structure.building_demand.layer import Layer from city_model_structure.building_demand.thermal_opening import ThermalOpening -ThermalZone = TypeVar('ThermalZone') +from city_model_structure.building_demand.thermal_zone import ThermalZone +from city_model_structure.building_demand.surface import Surface + Polygon = TypeVar('Polygon') -Surface = TypeVar('Surface') class ThermalBoundary: """ ThermalBoundary class """ - def __init__(self, surface, thermal_zones): + def __init__(self, surface): self._surface = surface - self._thermal_zones = thermal_zones + self._thermal_zones = None # ToDo: up to at least LOD2 will be just one thermal opening per Thermal boundary only if window_ratio > 0, # review for LOD3 and LOD4 self._thermal_openings = None @@ -29,8 +30,8 @@ class ThermalBoundary: self._u_value = None self._shortwave_reflectance = None self._construction_name = None - self._hi = None - self._he = None + self._hi = 3.5 + self._he = 20 self._window_ratio = None self._refurbishment_measure = None self._surface_geometry = None @@ -57,6 +58,14 @@ class ThermalBoundary: """ return self._thermal_zones + @thermal_zones.setter + def thermal_zones(self, value): + """ + Thermal zones delimited by the thermal boundary + :param value: [ThermalZone] + """ + self._thermal_zones = value + @property def azimuth(self): """ diff --git a/city_model_structure/building_demand/thermal_zone.py b/city_model_structure/building_demand/thermal_zone.py index 3ca2a33e..ddf23f4d 100644 --- a/city_model_structure/building_demand/thermal_zone.py +++ b/city_model_structure/building_demand/thermal_zone.py @@ -29,6 +29,7 @@ class ThermalZone: self._volume = volume self._volume_geometry = None self._id = None + self._ordinate_number = None @property def id(self): @@ -181,3 +182,19 @@ class ThermalZone: :return: Polyhedron """ return self._volume_geometry + + @property + def ordinate_number(self): + """ + In case the thermal_zones need to be enumerated and their order saved, this property saves that order + :return: int + """ + return self._ordinate_number + + @ordinate_number.setter + def ordinate_number(self, value): + """ + Sets an specific order of the zones to be called + :param value: int + """ + self._ordinate_number = value diff --git a/imports/construction/ca_physics_parameters.py b/imports/construction/ca_physics_parameters.py index 3e34b4f3..684b0e1e 100644 --- a/imports/construction/ca_physics_parameters.py +++ b/imports/construction/ca_physics_parameters.py @@ -6,6 +6,7 @@ Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.mons import sys from imports.construction.helpers.construction_helper import ConstructionHelper from imports.construction.nrel_physics_interface import NrelPhysicsInterface +from imports.construction.helpers.storeys_generation import StoreysGeneration class CaPhysicsParameters(NrelPhysicsInterface): @@ -31,6 +32,8 @@ class CaPhysicsParameters(NrelPhysicsInterface): f'{ConstructionHelper.nrcan_from_function(building.function)} ' f'and building year of construction: {building.year_of_construction}\n') continue + + self._create_storeys(building, archetype) self._assign_values(building, archetype) def _search_archetype(self, function, year_of_construction): @@ -45,8 +48,6 @@ class CaPhysicsParameters(NrelPhysicsInterface): return None def _assign_values(self, building, archetype): - building.average_storey_height = archetype.average_storey_height - building.storeys_above_ground = archetype.storeys_above_ground for thermal_zone in building.thermal_zones: thermal_zone.additional_thermal_bridge_u_value = archetype.additional_thermal_bridge_u_value thermal_zone.effective_thermal_capacity = archetype.effective_thermal_capacity @@ -67,3 +68,12 @@ class CaPhysicsParameters(NrelPhysicsInterface): thermal_opening.frame_ratio = thermal_opening_archetype.frame_ratio thermal_opening.g_value = thermal_opening_archetype.g_value thermal_opening.overall_u_value = thermal_opening_archetype.overall_u_value + + @staticmethod + def _create_storeys(building, archetype): + building.average_storey_height = archetype.average_storey_height + building.storeys_above_ground = archetype.storeys_above_ground + storeys_generation = StoreysGeneration(building) + storeys = storeys_generation.storeys + building.storeys = storeys + storeys_generation.assign_thermal_zones_delimited_by_thermal_boundaries() diff --git a/imports/construction/us_physics_parameters.py b/imports/construction/us_physics_parameters.py index b37ecc73..7d0de632 100644 --- a/imports/construction/us_physics_parameters.py +++ b/imports/construction/us_physics_parameters.py @@ -10,6 +10,7 @@ from imports.construction.nrel_physics_interface import NrelPhysicsInterface from imports.construction.helpers.construction_helper import ConstructionHelper from city_model_structure.building_demand.layer import Layer from city_model_structure.building_demand.material import Material +from imports.construction.helpers.storeys_generation import StoreysGeneration class UsPhysicsParameters(NrelPhysicsInterface): @@ -39,6 +40,8 @@ class UsPhysicsParameters(NrelPhysicsInterface): sys.stderr.write(f'Building {building.name} has unknown archetype for building function: {building.function} ' f'and building year of construction: {building.year_of_construction}\n') continue + + self._create_storeys(building, archetype) self._assign_values(building, archetype) def _search_archetype(self, building_type, standard, climate_zone): @@ -51,8 +54,6 @@ class UsPhysicsParameters(NrelPhysicsInterface): return None def _assign_values(self, building, archetype): - building.average_storey_height = archetype.average_storey_height - building.storeys_above_ground = archetype.storeys_above_ground for thermal_zone in building.thermal_zones: thermal_zone.additional_thermal_bridge_u_value = archetype.additional_thermal_bridge_u_value thermal_zone.effective_thermal_capacity = archetype.effective_thermal_capacity @@ -95,3 +96,12 @@ class UsPhysicsParameters(NrelPhysicsInterface): thermal_opening_archetype.back_side_solar_transmittance_at_normal_incidence thermal_opening.front_side_solar_transmittance_at_normal_incidence = \ thermal_opening_archetype.front_side_solar_transmittance_at_normal_incidence + + @staticmethod + def _create_storeys(building, archetype): + building.average_storey_height = archetype.average_storey_height + building.storeys_above_ground = archetype.storeys_above_ground + storeys_generation = StoreysGeneration(building) + storeys = storeys_generation.storeys + building.storeys = storeys + storeys_generation.assign_thermal_zones_delimited_by_thermal_boundaries() diff --git a/imports/geometry/citygml.py b/imports/geometry/citygml.py index 10503e32..2f4c0d7a 100644 --- a/imports/geometry/citygml.py +++ b/imports/geometry/citygml.py @@ -83,8 +83,7 @@ class CityGml: surfaces = CityGmlLod2(city_object).surfaces else: raise NotImplementedError("Not supported level of detail") - return Building(name, lod, surfaces, year_of_construction, function, self._lower_corner, [], - divide_in_storeys=True) + return Building(name, lod, surfaces, year_of_construction, function, self._lower_corner, []) def _create_parts_consisting_building(self, city_object): name = city_object['@id'] diff --git a/imports/schedules_factory.py b/imports/schedules_factory.py index 42b3d986..ea1634ac 100644 --- a/imports/schedules_factory.py +++ b/imports/schedules_factory.py @@ -1,5 +1,6 @@ """ SchedulesFactory retrieve the specific schedules module for the given standard +This factory can only be called after calling the usage factory so the usage zones are created. SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca """ @@ -17,6 +18,10 @@ class SchedulesFactory: self._handler = '_' + handler.lower().replace(' ', '_') self._city = city self._base_path = base_path + for building in city.buildings: + if len(building.usage_zones) == 0: + raise Exception('It seems that the schedule factory is being called before the usage factory. ' + 'Please ensure that the usage factory is called first.') def _comnet(self): ComnetSchedules(self._city, self._base_path) diff --git a/imports/usage_factory.py b/imports/usage_factory.py index d302b872..d748dc0d 100644 --- a/imports/usage_factory.py +++ b/imports/usage_factory.py @@ -1,5 +1,6 @@ """ UsageFactory retrieve the specific usage module for the given region +This factory can only be called after calling the construction factory so the thermal zones are created. SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Guille Gutierrez guillermo.gutierrezmorote@concordia.ca """ @@ -18,6 +19,10 @@ class UsageFactory: self._handler = '_' + handler.lower().replace(' ', '_') self._city = city self._base_path = base_path + for building in city.buildings: + if len(building.thermal_zones) == 0: + raise Exception('It seems that the usage factory is being called before the construction factory. ' + 'Please ensure that the construction factory is called first.') def _hft(self): return HftUsageParameters(self._city, self._base_path).enrich_buildings()