From 9624be2cf9431a258d1f49a343be1660adc2d331 Mon Sep 17 00:00:00 2001 From: p_monsalvete Date: Fri, 1 Sep 2023 12:53:01 -0400 Subject: [PATCH 1/6] solved bugs introduced with changes in thermal_zone_from_internal_zones new structure --- .../building_demand/construction.py | 17 +++ .../building_demand/internal_zone.py | 1 + .../building_demand/thermal_boundary.py | 13 +++ hub/exports/building_energy/idf.py | 102 +++++++++--------- .../construction/eilat_physics_parameters.py | 1 + .../construction/nrcan_physics_parameters.py | 1 + .../construction/nrel_physics_parameters.py | 1 + 7 files changed, 87 insertions(+), 49 deletions(-) diff --git a/hub/city_model_structure/building_demand/construction.py b/hub/city_model_structure/building_demand/construction.py index ad82999d..e41d2981 100644 --- a/hub/city_model_structure/building_demand/construction.py +++ b/hub/city_model_structure/building_demand/construction.py @@ -14,6 +14,7 @@ class Construction: """ def __init__(self): self._type = None + self._name = None self._layers = None self._window_ratio = None self._window_frame_ratio = None @@ -37,6 +38,22 @@ class Construction: """ self._type = value + @property + def name(self): + """ + Get construction name + :return: str + """ + return self._name + + @name.setter + def name(self, value): + """ + Set construction name + :param value: str + """ + self._name = value + @property def layers(self) -> [Layer]: """ diff --git a/hub/city_model_structure/building_demand/internal_zone.py b/hub/city_model_structure/building_demand/internal_zone.py index c0494f0a..8bf2c98b 100644 --- a/hub/city_model_structure/building_demand/internal_zone.py +++ b/hub/city_model_structure/building_demand/internal_zone.py @@ -130,6 +130,7 @@ class InternalZone: for hole in surface.holes_polygons: windows_areas.append(hole.area) _thermal_boundary = ThermalBoundary(surface, surface.solid_polygon.area, windows_areas) + surface.associated_thermal_boundaries = [_thermal_boundary] _thermal_boundaries.append(_thermal_boundary) _number_of_storeys = int(self.volume / self.area / self.thermal_archetype.average_storey_height) _thermal_zone = ThermalZone(_thermal_boundaries, self, self.volume, self.area, _number_of_storeys) diff --git a/hub/city_model_structure/building_demand/thermal_boundary.py b/hub/city_model_structure/building_demand/thermal_boundary.py index 66746037..1ff00244 100644 --- a/hub/city_model_structure/building_demand/thermal_boundary.py +++ b/hub/city_model_structure/building_demand/thermal_boundary.py @@ -256,6 +256,19 @@ class ThermalBoundary: raise TypeError('Constructions layers are not initialized') from TypeError return self._u_value + @property + def construction_name(self): + """ + Get construction name + :return: str + """ + if self._construction_archetype is not None: + self._construction_name = self._construction_archetype.name + else: + logging.error('Construction name not defined\n') + raise ValueError('Construction name not defined') + return self._construction_name + @u_value.setter def u_value(self, value): """ diff --git a/hub/exports/building_energy/idf.py b/hub/exports/building_energy/idf.py index 02de155e..52945dd4 100644 --- a/hub/exports/building_energy/idf.py +++ b/hub/exports/building_energy/idf.py @@ -148,28 +148,28 @@ class Idf: def _add_material(self, layer): for material in self._idf.idfobjects[self._MATERIAL]: - if material.Name == layer.material.name: + if material.Name == layer.name: return for material in self._idf.idfobjects[self._MATERIAL_NOMASS]: - if material.Name == layer.material.name: + if material.Name == layer.name: return - if layer.material.no_mass: + if layer.no_mass: self._idf.newidfobject(self._MATERIAL_NOMASS, - Name=layer.material.name, + Name=layer.name, Roughness=self._ROUGHNESS, - Thermal_Resistance=layer.material.thermal_resistance + Thermal_Resistance=layer.thermal_resistance ) else: self._idf.newidfobject(self._MATERIAL, - Name=layer.material.name, + Name=layer.name, Roughness=self._ROUGHNESS, Thickness=layer.thickness, - Conductivity=layer.material.conductivity, - Density=layer.material.density, - Specific_Heat=layer.material.specific_heat, - Thermal_Absorptance=layer.material.thermal_absorptance, - Solar_Absorptance=layer.material.solar_absorptance, - Visible_Absorptance=layer.material.visible_absorptance + Conductivity=layer.conductivity, + Density=layer.density, + Specific_Heat=layer.specific_heat, + Thermal_Absorptance=layer.thermal_absorptance, + Solar_Absorptance=layer.solar_absorptance, + Visible_Absorptance=layer.visible_absorptance ) @staticmethod @@ -338,11 +338,11 @@ class Idf: _kwargs = {'Name': vegetation_name, 'Outside_Layer': thermal_boundary.parent_surface.vegetation.name} for i in range(0, len(layers) - 1): - _kwargs[f'Layer_{i + 2}'] = layers[i].material.name + _kwargs[f'Layer_{i + 2}'] = layers[i].name else: - _kwargs = {'Name': thermal_boundary.construction_name, 'Outside_Layer': layers[0].material.name} + _kwargs = {'Name': thermal_boundary.construction_name, 'Outside_Layer': layers[0].name} for i in range(1, len(layers) - 1): - _kwargs[f'Layer_{i + 1}'] = layers[i].material.name + _kwargs[f'Layer_{i + 1}'] = layers[i].name self._idf.newidfobject(self._CONSTRUCTION, **_kwargs) def _add_window_construction_and_material(self, thermal_opening): @@ -512,12 +512,12 @@ class Idf: self._rename_building(self._city.name) self._lod = self._city.level_of_detail.geometry for building in self._city.buildings: - print('building name', building.name) for internal_zone in building.internal_zones: if internal_zone.thermal_zones_from_internal_zones is None: continue for thermal_zone in internal_zone.thermal_zones_from_internal_zones: for thermal_boundary in thermal_zone.thermal_boundaries: + self._add_construction(thermal_boundary) if thermal_boundary.parent_surface.vegetation is not None: self._add_vegetation_material(thermal_boundary.parent_surface.vegetation) @@ -560,7 +560,7 @@ class Idf: self._add_dhw(thermal_zone, building.name) if self._export_type == "Surfaces": if building.name in self._target_buildings or building.name in self._adjacent_buildings: - if building.internal_zones[0].thermal_zones_from_internal_zones is not None: + if building.thermal_zones_from_internal_zones is not None: self._add_surfaces(building, building.name) else: self._add_pure_geometry(building, building.name) @@ -569,6 +569,7 @@ class Idf: else: self._add_block(building) + print('BBBBBBBBBB') self._idf.newidfobject( "OUTPUT:VARIABLE", Variable_Name="Zone Ideal Loads Supply Air Total Heating Energy", @@ -677,41 +678,40 @@ class Idf: self._idf.set_wwr(wwr) def _add_surfaces(self, building, zone_name): - for internal_zone in building.internal_zones: - for thermal_zone in internal_zone.thermal_zones_from_internal_zones: - for boundary in thermal_zone.thermal_boundaries: - idf_surface_type = self.idf_surfaces[boundary.parent_surface.type] - outside_boundary_condition = 'Outdoors' - sun_exposure = 'SunExposed' - wind_exposure = 'WindExposed' - _kwargs = {'Name': f'{boundary.parent_surface.name}', - 'Surface_Type': idf_surface_type, - 'Zone_Name': zone_name} - if boundary.parent_surface.type == cte.GROUND: - outside_boundary_condition = 'Ground' - sun_exposure = 'NoSun' - wind_exposure = 'NoWind' - if boundary.parent_surface.percentage_shared is not None and boundary.parent_surface.percentage_shared > 0.5: - outside_boundary_condition = 'Surface' - outside_boundary_condition_object = boundary.parent_surface.name - sun_exposure = 'NoSun' - wind_exposure = 'NoWind' - _kwargs['Outside_Boundary_Condition_Object'] = outside_boundary_condition_object - _kwargs['Outside_Boundary_Condition'] = outside_boundary_condition - _kwargs['Sun_Exposure'] = sun_exposure - _kwargs['Wind_Exposure'] = wind_exposure + for thermal_zone in building.thermal_zones_from_internal_zones: + for boundary in thermal_zone.thermal_boundaries: + idf_surface_type = self.idf_surfaces[boundary.parent_surface.type] + outside_boundary_condition = 'Outdoors' + sun_exposure = 'SunExposed' + wind_exposure = 'WindExposed' + _kwargs = {'Name': f'{boundary.parent_surface.name}', + 'Surface_Type': idf_surface_type, + 'Zone_Name': zone_name} + if boundary.parent_surface.type == cte.GROUND: + outside_boundary_condition = 'Ground' + sun_exposure = 'NoSun' + wind_exposure = 'NoWind' + if boundary.parent_surface.percentage_shared is not None and boundary.parent_surface.percentage_shared > 0.5: + outside_boundary_condition = 'Surface' + outside_boundary_condition_object = boundary.parent_surface.name + sun_exposure = 'NoSun' + wind_exposure = 'NoWind' + _kwargs['Outside_Boundary_Condition_Object'] = outside_boundary_condition_object + _kwargs['Outside_Boundary_Condition'] = outside_boundary_condition + _kwargs['Sun_Exposure'] = sun_exposure + _kwargs['Wind_Exposure'] = wind_exposure - if boundary.parent_surface.vegetation is not None: - construction_name = f'{boundary.construction_name}_{boundary.parent_surface.vegetation.name}' - else: - construction_name = boundary.construction_name - _kwargs['Construction_Name'] = construction_name + if boundary.parent_surface.vegetation is not None: + construction_name = f'{boundary.construction_name}_{boundary.parent_surface.vegetation.name}' + else: + construction_name = boundary.construction_name + _kwargs['Construction_Name'] = construction_name - surface = self._idf.newidfobject(self._SURFACE, **_kwargs) + surface = self._idf.newidfobject(self._SURFACE, **_kwargs) - coordinates = self._matrix_to_list(boundary.parent_surface.solid_polygon.coordinates, - self._city.lower_corner) - surface.setcoords(coordinates) + coordinates = self._matrix_to_list(boundary.parent_surface.solid_polygon.coordinates, + self._city.lower_corner) + surface.setcoords(coordinates) if self._lod >= 3: for internal_zone in building.internal_zones: @@ -723,7 +723,11 @@ class Idf: wwr = 0 for surface in building.surfaces: if surface.type == cte.WALL: + print(surface) + print(surface.associated_thermal_boundaries) + print(surface.associated_thermal_boundaries[0].window_ratio) wwr = surface.associated_thermal_boundaries[0].window_ratio + print(wwr) self._idf.set_wwr(wwr, construction='window_construction_1') def _add_windows_by_vertices(self, boundary): diff --git a/hub/imports/construction/eilat_physics_parameters.py b/hub/imports/construction/eilat_physics_parameters.py index ea088178..c09db490 100644 --- a/hub/imports/construction/eilat_physics_parameters.py +++ b/hub/imports/construction/eilat_physics_parameters.py @@ -71,6 +71,7 @@ class EilatPhysicsParameters: for catalog_construction in catalog_archetype.constructions: construction = Construction() construction.type = catalog_construction.type + construction.name = catalog_construction.name if catalog_construction.window_ratio is not None: for _orientation in catalog_construction.window_ratio: if catalog_construction.window_ratio[_orientation] is None: diff --git a/hub/imports/construction/nrcan_physics_parameters.py b/hub/imports/construction/nrcan_physics_parameters.py index 9ab9964c..8caede23 100644 --- a/hub/imports/construction/nrcan_physics_parameters.py +++ b/hub/imports/construction/nrcan_physics_parameters.py @@ -71,6 +71,7 @@ class NrcanPhysicsParameters: for catalog_construction in catalog_archetype.constructions: construction = Construction() construction.type = catalog_construction.type + construction.name = catalog_construction.name if catalog_construction.window_ratio is not None: for _orientation in catalog_construction.window_ratio: if catalog_construction.window_ratio[_orientation] is None: diff --git a/hub/imports/construction/nrel_physics_parameters.py b/hub/imports/construction/nrel_physics_parameters.py index 55b4d3b7..6f61102a 100644 --- a/hub/imports/construction/nrel_physics_parameters.py +++ b/hub/imports/construction/nrel_physics_parameters.py @@ -73,6 +73,7 @@ class NrelPhysicsParameters: for catalog_construction in catalog_archetype.constructions: construction = Construction() construction.type = catalog_construction.type + construction.name = catalog_construction.name if catalog_construction.window_ratio is not None: construction.window_ratio = {'north': catalog_construction.window_ratio, 'east': catalog_construction.window_ratio, From 4ae09915b1042fdcfbf963c4297611a4917de940 Mon Sep 17 00:00:00 2001 From: p_monsalvete Date: Fri, 1 Sep 2023 13:02:01 -0400 Subject: [PATCH 2/6] erased not needed prints --- hub/exports/building_energy/idf.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/hub/exports/building_energy/idf.py b/hub/exports/building_energy/idf.py index 52945dd4..50f97849 100644 --- a/hub/exports/building_energy/idf.py +++ b/hub/exports/building_energy/idf.py @@ -569,7 +569,6 @@ class Idf: else: self._add_block(building) - print('BBBBBBBBBB') self._idf.newidfobject( "OUTPUT:VARIABLE", Variable_Name="Zone Ideal Loads Supply Air Total Heating Energy", @@ -723,11 +722,7 @@ class Idf: wwr = 0 for surface in building.surfaces: if surface.type == cte.WALL: - print(surface) - print(surface.associated_thermal_boundaries) - print(surface.associated_thermal_boundaries[0].window_ratio) wwr = surface.associated_thermal_boundaries[0].window_ratio - print(wwr) self._idf.set_wwr(wwr, construction='window_construction_1') def _add_windows_by_vertices(self, boundary): From aa6b3c9e74796e6c843a6c46099ad9e56afa6cc2 Mon Sep 17 00:00:00 2001 From: guille Date: Thu, 14 Sep 2023 06:42:28 +0200 Subject: [PATCH 3/6] add two todos to change name into material_name in the layer class --- hub/city_model_structure/building_demand/layer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hub/city_model_structure/building_demand/layer.py b/hub/city_model_structure/building_demand/layer.py index 3075f0bc..0becf6af 100644 --- a/hub/city_model_structure/building_demand/layer.py +++ b/hub/city_model_structure/building_demand/layer.py @@ -59,6 +59,7 @@ class Layer: Get material name :return: str """ + # todo: this should be named material_name instead return self._name @name.setter @@ -67,6 +68,7 @@ class Layer: Set material name :param value: string """ + # todo: this should be named material_name instead self._name = str(value) @property From 207058a16f7c2b04e1ee9ad23d1568f5796f9f16 Mon Sep 17 00:00:00 2001 From: guille Date: Thu, 28 Sep 2023 15:20:28 +0200 Subject: [PATCH 4/6] obj export improvements --- hub/exports/formats/obj.py | 63 +++++++++++++++++++++++++++++++------- tests/test_exports.py | 13 +------- 2 files changed, 53 insertions(+), 23 deletions(-) diff --git a/hub/exports/formats/obj.py b/hub/exports/formats/obj.py index 2d0776d6..5faa02d3 100644 --- a/hub/exports/formats/obj.py +++ b/hub/exports/formats/obj.py @@ -4,9 +4,11 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Concordia CERC group Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca """ - +import math from pathlib import Path +import numpy as np + class Obj: """ @@ -17,41 +19,80 @@ class Obj: self._path = path self._export() - def _to_vertex(self, coordinate): + def _ground(self, coordinate): x = coordinate[0] - self._city.lower_corner[0] y = coordinate[1] - self._city.lower_corner[1] z = coordinate[2] - self._city.lower_corner[2] - return f'v {x} {y} {z}\n' + return x, y, z + + def _to_vertex(self, coordinate): + x, y, z = self._ground(coordinate) + return f'v {x} {z} {y}\n' + + def _to_texture_vertex(self, coordinate): + u, v, _ = self._ground(coordinate) + return f'vt {u} {v}\n' + + def _to_normal_vertex(self, coordinates): + ground_vertex = [] + for coordinate in coordinates: + x, y, z = self._ground(coordinate) + ground_vertex.append(np.array([x, y, z])) + # recalculate the normal to get grounded values + edge_1 = ground_vertex[1] - ground_vertex[0] + edge_2 = ground_vertex[2] - ground_vertex[0] + normal = np.cross(edge_1, edge_2) + normal = normal / np.linalg.norm(normal) + return f'vn {normal[0]} {normal[1]} {normal[2]}\n' def _export(self): if self._city.name is None: self._city.name = 'unknown_city' - file_name = self._city.name + '.obj' - file_path = (Path(self._path).resolve() / file_name).resolve() + obj_name = f'{self._city.name}.obj' + mtl_name = f'{self._city.name}.mtl' + obj_file_path = (Path(self._path).resolve() / obj_name).resolve() + mtl_file_path = (Path(self._path).resolve() / mtl_name).resolve() + with open(mtl_file_path, 'w', encoding='utf-8') as mtl: + mtl.write("newmtl cerc_base_material\n") + mtl.write("Ka 1.0 1.0 1.0 # Ambient color (white)\n") + mtl.write("Kd 0.3 0.8 0.3 # Diffuse color (greenish)\n") + mtl.write("Ks 1.0 1.0 1.0 # Specular color (white)\n") + mtl.write("Ns 400.0 # Specular exponent (defines shininess)\n") vertices = {} - with open(file_path, 'w', encoding='utf-8') as obj: + normals_index = {} + faces = [] + vertex_index = 0 + normal_index = 0 + with open(obj_file_path, 'w', encoding='utf-8') as obj: obj.write("# cerc-hub export\n") - vertex_index = 0 - faces = [] + obj.write(f'mtllib {mtl_name}') + for building in self._city.buildings: obj.write(f'# building {building.name}\n') obj.write(f'g {building.name}\n') obj.write('s off\n') for surface in building.surfaces: obj.write(f'# surface {surface.name}\n') - face = 'f ' + face = [] + normal = self._to_normal_vertex(surface.perimeter_polygon.coordinates) + normal_index += 1 + textures = [] for coordinate in surface.perimeter_polygon.coordinates: vertex = self._to_vertex(coordinate) + if vertex not in vertices: vertex_index += 1 vertices[vertex] = vertex_index current = vertex_index obj.write(vertex) + textures.append(self._to_texture_vertex(coordinate)) # only append if non-existing else: current = vertices[vertex] - face = f'{face} {current}' + face.insert(0, f'{current}/{current}/{normal_index}') # insert counterclockwise + obj.writelines(normal) # add the normal + obj.writelines(textures) # add the texture vertex - faces.append(f'{face} {face.split(" ")[1]}\n') + faces.append(f"f {' '.join(face)}\n") obj.writelines(faces) faces = [] diff --git a/tests/test_exports.py b/tests/test_exports.py index 3bb0f91f..ff808968 100644 --- a/tests/test_exports.py +++ b/tests/test_exports.py @@ -66,12 +66,7 @@ class TestExports(TestCase): def _export(self, export_type, from_pickle=False): self._complete_city = self._get_complete_city(from_pickle) - try: - ExportsFactory(export_type, self._complete_city, self._output_path).export() - except ValueError as err: - if export_type != 'stl': - logging.warning('No backend export for STL test, skipped') - raise err + ExportsFactory(export_type, self._complete_city, self._output_path).export() def _export_building_energy(self, export_type, from_pickle=False): self._complete_city = self._get_complete_city(from_pickle) @@ -83,12 +78,6 @@ class TestExports(TestCase): """ self._export('obj', False) - def test_stl_export(self): - """ - export to stl - """ - self._export('stl', False) - def test_energy_ade_export(self): """ export to energy ADE From 8bd75c790ffbe89d86cadc2ba1783f54f349420d Mon Sep 17 00:00:00 2001 From: guille Date: Fri, 29 Sep 2023 05:55:36 +0200 Subject: [PATCH 5/6] Bug correction for building upper_corner it's no longer set to -inf --- hub/city_model_structure/building.py | 3 +++ hub/city_model_structure/building_demand/surface.py | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/hub/city_model_structure/building.py b/hub/city_model_structure/building.py index 8d8b5ea3..0f4dbc8b 100644 --- a/hub/city_model_structure/building.py +++ b/hub/city_model_structure/building.py @@ -70,6 +70,9 @@ class Building(CityObject): 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]) + self._max_x = max(self._max_x, surface.upper_corner[0]) + self._max_y = max(self._max_y, surface.upper_corner[1]) + self._max_z = max(self._max_z, surface.upper_corner[2]) surface.id = surface_id if surface.type == cte.GROUND: self._grounds.append(surface) diff --git a/hub/city_model_structure/building_demand/surface.py b/hub/city_model_structure/building_demand/surface.py index 7fbef20d..9f85efb6 100644 --- a/hub/city_model_structure/building_demand/surface.py +++ b/hub/city_model_structure/building_demand/surface.py @@ -154,7 +154,6 @@ class Surface: if self._inclination is None: self._inclination = np.arccos(self.perimeter_polygon.normal[2]) return self._inclination - @property def type(self): """ From cf783d8998d9d730e9bafbcc1ad8bf167335f97f Mon Sep 17 00:00:00 2001 From: guille Date: Fri, 29 Sep 2023 07:54:46 +0200 Subject: [PATCH 6/6] add max height to the city in geojson when the high field is given --- hub/imports/geometry/geojson.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hub/imports/geometry/geojson.py b/hub/imports/geometry/geojson.py index 84269cd1..c92688ee 100644 --- a/hub/imports/geometry/geojson.py +++ b/hub/imports/geometry/geojson.py @@ -116,6 +116,7 @@ class Geojson: if self._extrusion_height_field is not None: extrusion_height = float(feature['properties'][self._extrusion_height_field]) lod = 1 + self._max_z = max(self._max_z, extrusion_height) year_of_construction = None if self._year_of_construction_field is not None: year_of_construction = int(feature['properties'][self._year_of_construction_field])