modified structure to new concept (thermal zones created after building division). Thermal_zone still not created, all tests related to thermal zones don't pass.
This commit is contained in:
parent
6b7dac8123
commit
533e117b03
57
city_model_structure/attributes/plane.py
Normal file
57
city_model_structure/attributes/plane.py
Normal file
|
@ -0,0 +1,57 @@
|
|||
"""
|
||||
Plane module
|
||||
SPDX - License - Identifier: LGPL - 3.0 - or -later
|
||||
Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca
|
||||
"""
|
||||
|
||||
from typing import TypeVar
|
||||
import numpy as np
|
||||
|
||||
Point = TypeVar('Point')
|
||||
|
||||
|
||||
class Plane:
|
||||
"""
|
||||
Plane class
|
||||
"""
|
||||
|
||||
def __init__(self, origin=None, normal=None):
|
||||
# todo: other options to define the plane:
|
||||
# by two lines
|
||||
# by three points
|
||||
self._origin = origin
|
||||
self._normal = normal
|
||||
self._opposite_normal = None
|
||||
|
||||
@property
|
||||
def origin(self) -> Point:
|
||||
"""
|
||||
Point origin of the plane
|
||||
return Point
|
||||
"""
|
||||
if self._origin is None:
|
||||
raise NotImplementedError
|
||||
return self._origin
|
||||
|
||||
@property
|
||||
def normal(self):
|
||||
"""
|
||||
Plane normal [x, y, z]
|
||||
return np.ndarray
|
||||
"""
|
||||
if self._normal is None:
|
||||
raise NotImplementedError
|
||||
return self._normal
|
||||
|
||||
@property
|
||||
def opposite_normal(self):
|
||||
"""
|
||||
Plane normal in the opposite direction [x, y, z]
|
||||
return np.ndarray
|
||||
"""
|
||||
if self._opposite_normal is None:
|
||||
coordinates = []
|
||||
for coordinate in self.normal:
|
||||
coordinates.append(-coordinate)
|
||||
self._opposite_normal = np.array(coordinates)
|
||||
return self._opposite_normal
|
|
@ -10,6 +10,8 @@ import sys
|
|||
import numpy as np
|
||||
import math
|
||||
from city_model_structure.attributes.point import Point
|
||||
from trimesh import Trimesh
|
||||
import trimesh.intersections
|
||||
|
||||
|
||||
class Polygon:
|
||||
|
@ -29,6 +31,9 @@ class Polygon:
|
|||
self._edges = None
|
||||
# self._coordinates = None
|
||||
self._coordinates = coordinates
|
||||
self._triangles = None
|
||||
self._vertices = None
|
||||
self._faces = None
|
||||
|
||||
# @property
|
||||
# def points(self) -> List[Point]:
|
||||
|
@ -66,7 +71,7 @@ class Polygon:
|
|||
def coordinates(self) -> List[np.ndarray]:
|
||||
"""
|
||||
List of points in the shape of its coordinates belonging to the polygon [[x, y, z],...]
|
||||
:return: np.array
|
||||
:return: [np.ndarray]
|
||||
"""
|
||||
return self._coordinates
|
||||
|
||||
|
@ -234,58 +239,60 @@ class Polygon:
|
|||
# it had a problem with a class called 'triangle', but, if solved,
|
||||
# it could be a very good substitute of this method
|
||||
# this method is very dirty and has an infinite loop solved with a counter!!
|
||||
points_list = self.points_list
|
||||
normal = self.normal
|
||||
if np.linalg.norm(normal) == 0:
|
||||
sys.stderr.write(f'Not able to triangulate polygon\n')
|
||||
return [self]
|
||||
# are points concave or convex?
|
||||
total_points_list, concave_points, convex_points = self._starting_lists(points_list, normal)
|
||||
|
||||
# list of ears
|
||||
ears = []
|
||||
j = 0
|
||||
while (len(concave_points) > 3 or len(convex_points) != 0) and j < 100:
|
||||
j += 1
|
||||
for i in range(0, len(concave_points)):
|
||||
ear = self._triangle(points_list, total_points_list, concave_points[i])
|
||||
rest_points = []
|
||||
for p in total_points_list:
|
||||
rest_points.append(list(self.coordinates[p]))
|
||||
if self._is_ear(ear, rest_points):
|
||||
ears.append(ear)
|
||||
point_to_remove = concave_points[i]
|
||||
previous_point_in_list, next_point_in_list = self._enveloping_points(point_to_remove, total_points_list)
|
||||
total_points_list.remove(point_to_remove)
|
||||
concave_points.remove(point_to_remove)
|
||||
# Was any of the adjacent points convex? -> check if changed status to concave
|
||||
for convex_point in convex_points:
|
||||
if convex_point == previous_point_in_list:
|
||||
concave_points, convex_points, end_loop = self._if_concave_change_status(normal, points_list,
|
||||
convex_point, total_points_list,
|
||||
concave_points, convex_points,
|
||||
previous_point_in_list)
|
||||
if end_loop:
|
||||
break
|
||||
continue
|
||||
if convex_point == next_point_in_list:
|
||||
concave_points, convex_points, end_loop = self._if_concave_change_status(normal, points_list,
|
||||
convex_point, total_points_list,
|
||||
concave_points, convex_points,
|
||||
next_point_in_list)
|
||||
if end_loop:
|
||||
break
|
||||
continue
|
||||
break
|
||||
if len(total_points_list) <= 3 and len(convex_points) > 0:
|
||||
if self._triangles is None:
|
||||
points_list = self.points_list
|
||||
normal = self.normal
|
||||
if np.linalg.norm(normal) == 0:
|
||||
sys.stderr.write(f'Not able to triangulate polygon\n')
|
||||
return [self]
|
||||
if j >= 100:
|
||||
sys.stderr.write(f'Not able to triangulate polygon\n')
|
||||
return [self]
|
||||
last_ear = self._triangle(points_list, total_points_list, concave_points[1])
|
||||
ears.append(last_ear)
|
||||
return ears
|
||||
# are points concave or convex?
|
||||
total_points_list, concave_points, convex_points = self._starting_lists(points_list, normal)
|
||||
|
||||
# list of ears
|
||||
ears = []
|
||||
j = 0
|
||||
while (len(concave_points) > 3 or len(convex_points) != 0) and j < 100:
|
||||
j += 1
|
||||
for i in range(0, len(concave_points)):
|
||||
ear = self._triangle(points_list, total_points_list, concave_points[i])
|
||||
rest_points = []
|
||||
for p in total_points_list:
|
||||
rest_points.append(list(self.coordinates[p]))
|
||||
if self._is_ear(ear, rest_points):
|
||||
ears.append(ear)
|
||||
point_to_remove = concave_points[i]
|
||||
previous_point_in_list, next_point_in_list = self._enveloping_points(point_to_remove, total_points_list)
|
||||
total_points_list.remove(point_to_remove)
|
||||
concave_points.remove(point_to_remove)
|
||||
# Was any of the adjacent points convex? -> check if changed status to concave
|
||||
for convex_point in convex_points:
|
||||
if convex_point == previous_point_in_list:
|
||||
concave_points, convex_points, end_loop = self._if_concave_change_status(normal, points_list,
|
||||
convex_point, total_points_list,
|
||||
concave_points, convex_points,
|
||||
previous_point_in_list)
|
||||
if end_loop:
|
||||
break
|
||||
continue
|
||||
if convex_point == next_point_in_list:
|
||||
concave_points, convex_points, end_loop = self._if_concave_change_status(normal, points_list,
|
||||
convex_point, total_points_list,
|
||||
concave_points, convex_points,
|
||||
next_point_in_list)
|
||||
if end_loop:
|
||||
break
|
||||
continue
|
||||
break
|
||||
if len(total_points_list) <= 3 and len(convex_points) > 0:
|
||||
sys.stderr.write(f'Not able to triangulate polygon\n')
|
||||
return [self]
|
||||
if j >= 100:
|
||||
sys.stderr.write(f'Not able to triangulate polygon\n')
|
||||
return [self]
|
||||
last_ear = self._triangle(points_list, total_points_list, concave_points[1])
|
||||
ears.append(last_ear)
|
||||
self._triangles = ears
|
||||
return self._triangles
|
||||
|
||||
@staticmethod
|
||||
def _starting_lists(points_list, normal) -> [List[float], List[float], List[float]]:
|
||||
|
@ -514,30 +521,42 @@ class Polygon:
|
|||
|
||||
@property
|
||||
def inverse(self):
|
||||
"""
|
||||
Flips the order of the coordinates
|
||||
:return: [np.ndarray]
|
||||
"""
|
||||
if self._inverse is None:
|
||||
self._inverse = self.points[::-1]
|
||||
self._inverse = self.coordinates[::-1]
|
||||
return self._inverse
|
||||
|
||||
# def divide(self, polygon):
|
||||
|
||||
# return polygon_1, polygon_2, intersection
|
||||
def divide(self, plane):
|
||||
"""
|
||||
Divides the polygon in two by a plane
|
||||
:param plane: plane that intersects with self to divide it in two parts (Plane)
|
||||
:return: Polygon, Polygon, [Point]
|
||||
"""
|
||||
tri_polygons = Trimesh(vertices=self.vertices, faces=self.faces)
|
||||
intersection = trimesh.intersections.mesh_plane(tri_polygons, plane.normal, plane.origin.coordinates)
|
||||
polys_1 = trimesh.intersections.slice_mesh_plane(tri_polygons, plane.opposite_normal, plane.origin.coordinates)
|
||||
polys_2 = trimesh.intersections.slice_mesh_plane(tri_polygons, plane.normal, plane.origin.coordinates)
|
||||
triangles_1 = []
|
||||
for triangle in polys_1.triangles:
|
||||
triangles_1.append(Polygon(triangle))
|
||||
polygon_1 = self._reshape(triangles_1)
|
||||
triangles_2 = []
|
||||
for triangle in polys_2.triangles:
|
||||
triangles_2.append(Polygon(triangle))
|
||||
polygon_2 = self._reshape(triangles_2)
|
||||
return polygon_1, polygon_2, intersection
|
||||
|
||||
def reshape(self, triangles) -> Polygon:
|
||||
def _reshape(self, triangles) -> Polygon:
|
||||
edges_list = []
|
||||
for i in range(0, len(triangles)):
|
||||
for edge in triangles[i].edges:
|
||||
print('edge')
|
||||
print(edge[0].coordinates, edge[1].coordinates)
|
||||
if not self._edge_in_edges_list(edge, edges_list):
|
||||
edges_list.append(edge)
|
||||
print('list')
|
||||
for e in edges_list:
|
||||
print(e[0].coordinates, e[1].coordinates)
|
||||
else:
|
||||
print('remove')
|
||||
edges_list = self._remove_from_list(edge, edges_list)
|
||||
for e in edges_list:
|
||||
print(e[0].coordinates, e[1].coordinates)
|
||||
points = self._order_points(edges_list)
|
||||
return Polygon(points)
|
||||
|
||||
|
@ -551,16 +570,17 @@ class Polygon:
|
|||
|
||||
@staticmethod
|
||||
def _order_points(edges_list):
|
||||
# todo: not sure that this method works for any case -> RECHECK
|
||||
points = edges_list[0]
|
||||
for i in range(1, len(edges_list)):
|
||||
point_1 = edges_list[i][0]
|
||||
point_2 = points[len(points)-1]
|
||||
if point_1.distance_to_point(point_2) == 0:
|
||||
points.append(edges_list[i][1])
|
||||
for j in range(0, len(points)):
|
||||
for i in range(1, len(edges_list)):
|
||||
point_1 = edges_list[i][0]
|
||||
point_2 = points[len(points)-1]
|
||||
if point_1.distance_to_point(point_2) == 0:
|
||||
points.append(edges_list[i][1])
|
||||
points.remove(points[len(points)-1])
|
||||
array_points = []
|
||||
for point in points:
|
||||
print(point.coordinates)
|
||||
array_points.append(point.coordinates)
|
||||
return np.array(array_points)
|
||||
|
||||
|
@ -572,3 +592,73 @@ class Polygon:
|
|||
(ed[1].distance_to_point(edge[0]) == 0 and ed[0].distance_to_point(edge[1]) == 0)):
|
||||
new_list.append(ed)
|
||||
return new_list
|
||||
|
||||
@property
|
||||
def vertices(self) -> np.ndarray:
|
||||
"""
|
||||
Polyhedron vertices
|
||||
:return: np.ndarray(int)
|
||||
"""
|
||||
if self._vertices is None:
|
||||
vertices, self._vertices = [], []
|
||||
_ = [vertices.extend(s.coordinates) for s in self.triangulate()]
|
||||
for vertex_1 in vertices:
|
||||
found = False
|
||||
for vertex_2 in self._vertices:
|
||||
found = False
|
||||
power = 0
|
||||
for dimension in range(0, 3):
|
||||
power += math.pow(vertex_2[dimension] - vertex_1[dimension], 2)
|
||||
distance = math.sqrt(power)
|
||||
if distance == 0:
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
self._vertices.append(vertex_1)
|
||||
self._vertices = np.asarray(self._vertices)
|
||||
return self._vertices
|
||||
|
||||
@property
|
||||
def faces(self) -> List[List[int]]:
|
||||
"""
|
||||
Polyhedron triangular faces
|
||||
:return: [face]
|
||||
"""
|
||||
if self._faces is None:
|
||||
self._faces = []
|
||||
|
||||
for polygon in self.triangulate():
|
||||
face = []
|
||||
points = polygon.coordinates
|
||||
if len(points) != 3:
|
||||
sub_polygons = polygon.triangulate()
|
||||
# todo: I modified this! To be checked @Guille
|
||||
if len(sub_polygons) >= 1:
|
||||
for sub_polygon in sub_polygons:
|
||||
face = []
|
||||
points = sub_polygon.coordinates
|
||||
for point in points:
|
||||
face.append(self._position_of(point, face))
|
||||
self._faces.append(face)
|
||||
else:
|
||||
for point in points:
|
||||
face.append(self._position_of(point, face))
|
||||
self._faces.append(face)
|
||||
return self._faces
|
||||
|
||||
def _position_of(self, point, face):
|
||||
"""
|
||||
position of a specific point in the list of points that define a face
|
||||
:return: int
|
||||
"""
|
||||
vertices = self.vertices
|
||||
for i in range(len(vertices)):
|
||||
# ensure not duplicated vertex
|
||||
power = 0
|
||||
vertex2 = vertices[i]
|
||||
for dimension in range(0, 3):
|
||||
power += math.pow(vertex2[dimension] - point[dimension], 2)
|
||||
distance = math.sqrt(power)
|
||||
if i not in face and distance == 0:
|
||||
return i
|
||||
return -1
|
||||
|
|
98
city_model_structure/attributes/storey.py
Normal file
98
city_model_structure/attributes/storey.py
Normal file
|
@ -0,0 +1,98 @@
|
|||
"""
|
||||
Storey module
|
||||
SPDX - License - Identifier: LGPL - 3.0 - or -later
|
||||
Copyright © 2020 Project Author Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import List
|
||||
import numpy as np
|
||||
|
||||
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
|
||||
import helpers.constants as cte
|
||||
|
||||
|
||||
class Storey:
|
||||
# todo: rethink this class for buildings with windows
|
||||
"""
|
||||
Storey class
|
||||
"""
|
||||
def __init__(self, name, surfaces, neighbours):
|
||||
# todo: the information of the parent surface is lost -> need to recover it
|
||||
self._name = name
|
||||
self._surfaces = surfaces
|
||||
self._thermal_boundaries = None
|
||||
self._virtual_surfaces = None
|
||||
self._thermal_zone = None
|
||||
self._neighbours = neighbours
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""
|
||||
Storey's name
|
||||
:return: str
|
||||
"""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def surfaces(self) -> List[Surface]:
|
||||
"""
|
||||
External surfaces enclosing the storey
|
||||
:return: [Surface]
|
||||
"""
|
||||
return self._surfaces
|
||||
|
||||
@property
|
||||
def neighbours(self):
|
||||
"""
|
||||
Neighbour storeys' names
|
||||
:return: [str]
|
||||
"""
|
||||
return self._neighbours
|
||||
|
||||
@property
|
||||
def thermal_boundaries(self) -> List[ThermalBoundary]:
|
||||
"""
|
||||
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))
|
||||
return self._thermal_boundaries
|
||||
|
||||
@property
|
||||
def virtual_surfaces(self) -> List[Surface]:
|
||||
"""
|
||||
Internal surfaces enclosing the thermal zone
|
||||
:return: [Surface]
|
||||
"""
|
||||
if self._virtual_surfaces is None:
|
||||
self._virtual_surfaces = []
|
||||
for thermal_boundary in self.thermal_boundaries:
|
||||
self._virtual_surfaces.append(thermal_boundary.virtual_internal_surface)
|
||||
return self._virtual_surfaces
|
||||
|
||||
@property
|
||||
def thermal_zone(self) -> ThermalZone:
|
||||
"""
|
||||
Thermal zone inside the storey
|
||||
:return: ThermalZone
|
||||
"""
|
||||
if self._thermal_zone is None:
|
||||
self._thermal_zone = ThermalZone(self.virtual_surfaces)
|
||||
return self._thermal_zone
|
|
@ -9,24 +9,27 @@ from __future__ import annotations
|
|||
import numpy as np
|
||||
import uuid
|
||||
from city_model_structure.attributes.polygon import Polygon
|
||||
from city_model_structure.attributes.plane import Plane
|
||||
from city_model_structure.attributes.point import Point
|
||||
from city_model_structure.attributes.pv_system import PvSystem
|
||||
from city_model_structure.attributes.thermal_boundary import ThermalBoundary
|
||||
import helpers.constants as cte
|
||||
|
||||
|
||||
class Surface:
|
||||
"""
|
||||
Surface class
|
||||
"""
|
||||
def __init__(self, solid_polygon, perimeter_polygon, holes_polygons=None, surface_type=None, swr=None):
|
||||
def __init__(self, solid_polygon, perimeter_polygon, holes_polygons=None, name=None, surface_type=None, swr=None,
|
||||
is_child=False):
|
||||
self._type = surface_type
|
||||
self._swr = swr
|
||||
self._name = None
|
||||
self._name = name
|
||||
self._id = None
|
||||
self._azimuth = None
|
||||
self._inclination = None
|
||||
self._area_above_ground = None
|
||||
self._area_below_ground = None
|
||||
# todo @Guille: what is parent?????????????
|
||||
self._parent = None
|
||||
self._lower_corner = None
|
||||
self._upper_corner = None
|
||||
self._shared_surfaces = []
|
||||
|
@ -35,6 +38,8 @@ class Surface:
|
|||
self._holes_polygons = holes_polygons
|
||||
self._solid_polygon = solid_polygon
|
||||
self._pv_system_installed = None
|
||||
self._inverse = None
|
||||
self._thermal_boundary = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -244,3 +249,43 @@ class Surface:
|
|||
:param value: PvSystem
|
||||
"""
|
||||
self._pv_system_installed = value
|
||||
|
||||
@property
|
||||
def inverse(self) -> Surface:
|
||||
"""
|
||||
Returns the same surface pointing backwards
|
||||
:return: Surface
|
||||
"""
|
||||
if self._inverse is None:
|
||||
new_solid_polygon = Polygon(self.solid_polygon.inverse)
|
||||
new_perimeter_polygon = Polygon(self.perimeter_polygon.inverse)
|
||||
new_holes_polygons = []
|
||||
for hole in self.holes_polygons:
|
||||
new_holes_polygons.append(Polygon(hole.inverse))
|
||||
self._inverse = Surface(new_solid_polygon, new_perimeter_polygon, new_holes_polygons, cte.VIRTUAL_INTERNAL)
|
||||
return self._inverse
|
||||
|
||||
@property
|
||||
def associated_thermal_boundary(self) -> ThermalBoundary:
|
||||
"""
|
||||
Thermal boundary associated to this surface considered as the external face
|
||||
:return: ThermalBoundary
|
||||
"""
|
||||
if self._thermal_boundary is None:
|
||||
self._thermal_boundary = ThermalBoundary(self, delimits)
|
||||
return self._thermal_boundary
|
||||
|
||||
def shared_surfaces(self):
|
||||
# todo: check https://trimsh.org/trimesh.collision.html as an option to implement this method
|
||||
raise NotImplementedError
|
||||
|
||||
def divide(self, z):
|
||||
# todo: recheck this method for LoD3 (windows)
|
||||
origin = Point([0, 0, z])
|
||||
normal = np.array([0, 0, 1])
|
||||
plane = Plane(normal=normal, origin=origin)
|
||||
polygon = self.perimeter_polygon
|
||||
part_1, part_2, intersection = polygon.divide(plane)
|
||||
surface_child = Surface(part_1, part_1, name=self.name, surface_type=self.type, is_child=True)
|
||||
rest_surface = Surface(part_2, part_2, name=self.name, surface_type=self.type, is_child=True)
|
||||
return surface_child, rest_surface, intersection
|
||||
|
|
|
@ -4,13 +4,13 @@ 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
|
||||
"""
|
||||
from typing import List, TypeVar
|
||||
from typing import List, TypeVar, Union
|
||||
from city_model_structure.attributes.layer import Layer
|
||||
from city_model_structure.attributes.thermal_opening import ThermalOpening
|
||||
from city_model_structure.attributes.surface import Surface
|
||||
|
||||
ThermalZone = TypeVar('ThermalZone')
|
||||
Polygon = TypeVar('Polygon')
|
||||
Surface = TypeVar('Surface')
|
||||
|
||||
|
||||
class ThermalBoundary:
|
||||
|
@ -34,6 +34,8 @@ class ThermalBoundary:
|
|||
self._window_ratio = None
|
||||
self._refurbishment_measure = None
|
||||
self._surface_geometry = None
|
||||
self._thickness = None
|
||||
self._virtual_internal_surface = None
|
||||
|
||||
@property
|
||||
def surface(self) -> Surface:
|
||||
|
@ -76,22 +78,18 @@ class ThermalBoundary:
|
|||
"""
|
||||
return self._surface.solid_polygon.area
|
||||
|
||||
# todo: shouldn't be these two attributes come from the associated surface???
|
||||
@property
|
||||
def area_above_ground(self):
|
||||
def thickness(self):
|
||||
"""
|
||||
Thermal boundary area above ground in square meters
|
||||
Thermal boundary thickness in meters
|
||||
:return: float
|
||||
"""
|
||||
return self._surface.area_above_ground
|
||||
|
||||
@property
|
||||
def area_below_ground(self):
|
||||
"""
|
||||
Thermal boundary area below ground in square meters
|
||||
:return: float
|
||||
"""
|
||||
return self._surface.area_below_ground
|
||||
if self._thickness is None:
|
||||
self._thickness = 0.0
|
||||
if self.layers is not None:
|
||||
for layer in self.layers:
|
||||
self._thickness += layer.thickness
|
||||
return self._thickness
|
||||
|
||||
@property
|
||||
def outside_solar_absorptance(self):
|
||||
|
@ -295,9 +293,19 @@ class ThermalBoundary:
|
|||
self._he = value
|
||||
|
||||
@property
|
||||
def surface_geometry(self) -> Polygon:
|
||||
def surface_geometry(self) -> Union[NotImplementedError, Polygon]:
|
||||
"""
|
||||
Get the polygon that defines the thermal boundary
|
||||
:return: Polygon
|
||||
"""
|
||||
return self._surface_geometry
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def virtual_internal_surface(self) -> Surface:
|
||||
"""
|
||||
Get the internal surface of the thermal boundary
|
||||
:return: Surface
|
||||
"""
|
||||
if self._virtual_internal_surface is None:
|
||||
self._virtual_internal_surface = self.surface.inverse
|
||||
return self._virtual_internal_surface
|
||||
|
|
|
@ -11,12 +11,13 @@ from typing import List
|
|||
import numpy as np
|
||||
import math
|
||||
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.attributes.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.geometry_helper import GeometryHelper as gh
|
||||
from trimesh import Trimesh
|
||||
from helpers import constants as cte
|
||||
|
||||
|
||||
class Building(CityObject):
|
||||
|
@ -24,7 +25,7 @@ class Building(CityObject):
|
|||
Building(CityObject) class
|
||||
"""
|
||||
def __init__(self, name, lod, surfaces, year_of_construction, function,
|
||||
city_lower_corner, terrains=None, zones_surfaces_ids=None):
|
||||
city_lower_corner, terrains=None):
|
||||
super().__init__(name, lod, surfaces, city_lower_corner)
|
||||
self._basement_heated = None
|
||||
self._attic_heated = None
|
||||
|
@ -35,25 +36,12 @@ class Building(CityObject):
|
|||
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._thermal_zones = []
|
||||
if zones_surfaces_ids is not None:
|
||||
for zone_surfaces_ids in zones_surfaces_ids:
|
||||
zone_surfaces = []
|
||||
for surface_id in zone_surfaces_ids:
|
||||
zone_surfaces.append(self.surface(surface_id))
|
||||
self._thermal_zones.append(ThermalZone(zone_surfaces))
|
||||
else:
|
||||
zone_surfaces = surfaces
|
||||
self._thermal_zones.append(ThermalZone(zone_surfaces))
|
||||
for t_zones in self._thermal_zones:
|
||||
t_zones.bounded = [ThermalBoundary(s, [t_zones]) for s in t_zones.surfaces]
|
||||
|
||||
self._grounds = []
|
||||
self._roofs = []
|
||||
self._walls = []
|
||||
|
@ -311,40 +299,79 @@ class Building(CityObject):
|
|||
return self._eave_height
|
||||
|
||||
@property
|
||||
def storeys(self) -> [Trimesh]:
|
||||
def storeys(self) -> [Storey]:
|
||||
"""
|
||||
subsections of building trimesh by storage in case of no interiors defined
|
||||
:return: [Trimesh]
|
||||
subsections of building trimesh by storey in case of no interiors defined
|
||||
:return: [Storey]
|
||||
"""
|
||||
trimesh = self.simplified_polyhedron.trimesh
|
||||
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
|
||||
number_of_storeys, height = self._calculate_number_storeys_and_height(self.average_storey_height, self.eave_height,
|
||||
self.storeys_above_ground)
|
||||
if number_of_storeys == 0:
|
||||
return Storey('storey_0', self.surfaces, [None, None])
|
||||
|
||||
storeys = []
|
||||
for n in range(0, number_of_storeys - 1):
|
||||
point_plane = [self.city_object_lower_corner[0], self.city_object_lower_corner[1],
|
||||
self.city_object_lower_corner[2] + height * (n + 1)]
|
||||
normal = [0, 0, -1]
|
||||
storey, trimesh = gh.divide_mesh_by_plane(trimesh, normal, point_plane)
|
||||
storeys.append(storey)
|
||||
storeys.append(trimesh)
|
||||
surfaces_child_last_storey = []
|
||||
rest_surfaces = []
|
||||
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)
|
||||
storeys.append(Storey(name, surfaces_child, neighbours))
|
||||
name = 'storey_' + str(number_of_storeys-1)
|
||||
neighbours = ['storey_' + str(number_of_storeys-2), None]
|
||||
storeys.append(Storey(name, surfaces_child_last_storey, neighbours))
|
||||
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 = 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):
|
||||
"""
|
||||
|
@ -379,3 +406,32 @@ class Building(CityObject):
|
|||
@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)
|
||||
|
|
|
@ -202,7 +202,7 @@ class CityObject:
|
|||
self._beam = value
|
||||
|
||||
@property
|
||||
def city_object_lower_corner(self):
|
||||
def lower_corner(self):
|
||||
"""
|
||||
City object lower corner coordinates [x, y, z]
|
||||
"""
|
||||
|
|
|
@ -18,6 +18,7 @@ ATTIC_FLOOR = 'Attic floor'
|
|||
ROOF = 'Roof'
|
||||
INTERIOR_SLAB = 'Interior slab'
|
||||
INTERIOR_WALL = 'Interior wall'
|
||||
VIRTUAL_INTERNAL = 'Virtual internal'
|
||||
WINDOW = 'Window'
|
||||
DOOR = 'Door'
|
||||
SKYLIGHT = 'Skylight'
|
||||
|
|
|
@ -51,6 +51,7 @@ class TestConstructionFactory(TestCase):
|
|||
for building in city.buildings:
|
||||
self.assertIsNotNone(building.average_storey_height, 'average_storey_height is none')
|
||||
self.assertIsNotNone(building.storeys_above_ground, 'storeys_above_ground is none')
|
||||
self.assertIsNot(len(building.thermal_zones), 0, 'no building thermal_zones defined')
|
||||
for thermal_zone in building.thermal_zones:
|
||||
self.assertIsNotNone(thermal_zone.effective_thermal_capacity, 'effective_thermal_capacity is none')
|
||||
self.assertIsNotNone(thermal_zone.additional_thermal_bridge_u_value,
|
||||
|
@ -81,6 +82,7 @@ class TestConstructionFactory(TestCase):
|
|||
for building in city.buildings:
|
||||
self.assertIsNotNone(building.average_storey_height, 'average_storey_height is none')
|
||||
self.assertIsNotNone(building.storeys_above_ground, 'storeys_above_ground is none')
|
||||
self.assertIsNot(len(building.thermal_zones), 0, 'no building thermal_zones defined')
|
||||
for thermal_zone in building.thermal_zones:
|
||||
self.assertIsNotNone(thermal_zone.effective_thermal_capacity, 'effective_thermal_capacity is none')
|
||||
self.assertIsNotNone(thermal_zone.additional_thermal_bridge_u_value,
|
||||
|
|
|
@ -71,7 +71,7 @@ class TestGeometryFactory(TestCase):
|
|||
self.assertIsNotNone(building.usage_zones, 'building usage_zones is none')
|
||||
self.assertIsNone(building.average_storey_height, 'building average_storey_height is not none')
|
||||
self.assertIsNone(building.storeys_above_ground, 'building storeys_above_ground is not none')
|
||||
self.assertIsNotNone(building.thermal_zones, 'building thermal_zones is none')
|
||||
self.assertIsNot(len(building.thermal_zones), 0, 'no building thermal_zones defined')
|
||||
self.assertIsNotNone(building.type, 'building type is none')
|
||||
self.assertIsNotNone(building.max_height, 'building max_height is none')
|
||||
self.assertIsNotNone(building.floor_area, 'building floor_area is none')
|
||||
|
@ -108,6 +108,7 @@ class TestGeometryFactory(TestCase):
|
|||
file = 'one_building_in_kelowna.gml'
|
||||
city = self._get_citygml(file)
|
||||
for building in city.buildings:
|
||||
self.assertIsNot(len(building.thermal_zones), 0, 'no building thermal_zones defined')
|
||||
for thermal_zone in building.thermal_zones:
|
||||
self.assertIsNotNone(thermal_zone.surfaces, 'thermal_zone surfaces is none')
|
||||
self.assertIsNotNone(thermal_zone.bounded, 'thermal_zone bounded is none')
|
||||
|
@ -135,7 +136,9 @@ class TestGeometryFactory(TestCase):
|
|||
file = 'one_building_in_kelowna.gml'
|
||||
city = self._get_citygml(file)
|
||||
for building in city.buildings:
|
||||
self.assertIsNot(len(building.thermal_zones), 0, 'no building thermal_zones defined')
|
||||
for thermal_zone in building.thermal_zones:
|
||||
self.assertIsNot(len(thermal_zone.bounded), 0, 'no building thermal_boundaries defined')
|
||||
for thermal_boundary in thermal_zone.bounded:
|
||||
self.assertIsNotNone(thermal_boundary.surface, 'thermal_boundary surface is none')
|
||||
self.assertIsNotNone(thermal_boundary.type, 'thermal_boundary type is none')
|
||||
|
@ -154,7 +157,9 @@ class TestGeometryFactory(TestCase):
|
|||
file = 'one_building_in_kelowna.gml'
|
||||
city = self._get_citygml(file)
|
||||
for building in city.buildings:
|
||||
self.assertIsNot(len(building.thermal_zones), 0, 'no building thermal_zones defined')
|
||||
for thermal_zone in building.thermal_zones:
|
||||
self.assertIsNot(len(thermal_zone.bounded), 0, 'no building thermal_boundaries defined')
|
||||
for thermal_boundary in thermal_zone.bounded:
|
||||
for thermal_opening in thermal_boundary.thermal_openings:
|
||||
self.assertIsNone(thermal_opening.frame_ratio, 'thermal_opening frame_ratio was initialized')
|
||||
|
@ -200,6 +205,31 @@ class TestGeometryFactory(TestCase):
|
|||
for building in city.buildings:
|
||||
self.assertRaises(Exception, lambda: self._internal_function(function_format, building.function))
|
||||
|
||||
def test_citygml_storeys(self):
|
||||
"""
|
||||
Test division by storeys of buildings
|
||||
:return: None
|
||||
"""
|
||||
|
||||
file = 'one_building_in_kelowna.gml'
|
||||
city = self._get_citygml(file)
|
||||
for building in city.buildings:
|
||||
print('building')
|
||||
for surface in building.surfaces:
|
||||
print(surface.name)
|
||||
print(surface.type)
|
||||
print(surface.perimeter_polygon.area)
|
||||
building.average_storey_height = 1.5
|
||||
building.storeys_above_ground = 2
|
||||
storeys = building.storeys
|
||||
for storey in storeys:
|
||||
print(storey.name)
|
||||
print(storey.neighbours)
|
||||
for surface in storey.surfaces:
|
||||
print(surface.name)
|
||||
print(surface.type)
|
||||
print(surface.perimeter_polygon.area)
|
||||
|
||||
# obj
|
||||
def test_import_obj(self):
|
||||
file = 'kelowna.obj'
|
||||
|
|
|
@ -40,5 +40,6 @@ class TestSchedulesFactory(TestCase):
|
|||
occupancy_handler = 'comnet'
|
||||
SchedulesFactory(occupancy_handler, city).enrich()
|
||||
for building in city.buildings:
|
||||
self.assertIsNot(len(building.usage_zones), 0, 'no building usage_zones defined')
|
||||
for usage_zone in building.usage_zones:
|
||||
self.assertTrue(usage_zone.schedules)
|
||||
|
|
|
@ -42,7 +42,7 @@ class TestUsageFactory(TestCase):
|
|||
# case 1: HFT
|
||||
UsageFactory('hft', city).enrich()
|
||||
for building in city.buildings:
|
||||
self.assertIsNotNone(building.usage_zones, 'usage_zones not created')
|
||||
self.assertIsNot(len(building.usage_zones), 0, 'no building usage_zones defined')
|
||||
for usage_zone in building.usage_zones:
|
||||
self.assertIsNotNone(usage_zone.usage, 'usage is none')
|
||||
self.assertIsNotNone(usage_zone.internal_gains, 'usage is none')
|
||||
|
|
Loading…
Reference in New Issue
Block a user