diff --git a/.idea/energy_system_modelling_workflow.iml b/.idea/energy_system_modelling_workflow.iml index 3a079507..37cf6364 100644 --- a/.idea/energy_system_modelling_workflow.iml +++ b/.idea/energy_system_modelling_workflow.iml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index dc7953c5..e8e47fc7 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,7 @@ - + + + \ No newline at end of file diff --git a/hub/exports/formats/stl.py b/hub/exports/formats/stl.py index 3904c5f1..51a96d80 100644 --- a/hub/exports/formats/stl.py +++ b/hub/exports/formats/stl.py @@ -1,16 +1,106 @@ """ -export a city into Stl format +export a city into STL format. (Each building is a solid, suitable for RC models such as CityBEM) SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Concordia CERC group -Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca +Copyright © 2024 Concordia CERC group +Project Coder Saeed Rayegan sr283100@gmail.com """ +from pathlib import Path +import numpy as np +from scipy.spatial import Delaunay -from hub.exports.formats.triangular import Triangular - - -class Stl(Triangular): +class Stl: """ - Export to STL + Export to stl format """ def __init__(self, city, path): - super().__init__(city, path, 'stl', write_mode='wb') + self._city = city + self._path = path + self._export() + + def _triangulate_stl(self, points_2d, height): + #This function requires a set of 2D points for triangulation + # Assuming vertices is a NumPy array + tri = Delaunay(points_2d) + triangles2D = points_2d[tri.simplices] + triangles3D = [] + + # Iterate through each triangle in triangles2D + for triangle in triangles2D: + # Extract the existing x and y coordinates + x1, y1 = triangle[0] + x2, y2 = triangle[1] + x3, y3 = triangle[2] + + # Create a 3D point with the specified height + point3D=[[x1, height, y1],[x2, height, y2],[x3, height, y3]] + + # Append the 3D points to the triangle list + triangles3D.append(point3D) + + return triangles3D + + 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 x, y, z + + def _to_vertex_stl(self, coordinate): + x, y, z = self._ground(coordinate) + return [x, z, -y] # Return as a list # to match opengl expectations (check it later) + + def _to_normal_vertex_stl(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) + # Convert normal to list for easier handling in the write operation + return normal.tolist() + + + def _export(self): + if self._city.name is None: + self._city.name = 'unknown_city' + stl_name = f'{self._city.name}.stl' + stl_file_path = (Path(self._path).resolve() / stl_name).resolve() + with open(stl_file_path, 'w', encoding='utf-8') as stl: + for building in self._city.buildings: + stl.write(f"solid building{building.name}\n") + for surface in building.surfaces: + vertices = [] + normal = self._to_normal_vertex_stl(surface.perimeter_polygon.coordinates) #the normal vector should be calculated for every surface + for coordinate in surface.perimeter_polygon.coordinates: + vertex = self._to_vertex_stl(coordinate) + if vertex not in vertices: + vertices.append(vertex) + vertices = np.array(vertices) + #After collecting the unique vertices of a surface, there is a need to identify if it is located on the roof, floor, or side walls + roofStatus=1 #multiplication of the height of all vertices in a surface + heightSum=0 #summation of the height of all vertices in a surface + for vertex in vertices: + roofStatus *= vertex[1] + heightSum += vertex[1] + if roofStatus>0: + #this surface is the roof (first and third elements of vertices should be passed to the triangulation function) + triangles=self._triangulate_stl(vertices[:, [0, 2]], vertices[0][1]) + elif roofStatus==0 and heightSum==0: + # this surface is the floor + triangles=self._triangulate_stl(vertices[:, [0, 2]], vertices[0][1]) + elif roofStatus==0 and heightSum>0: + # this surface is a vertical wall (no need for triangulation as it can be done manually) + triangles = [[vertices[0],vertices[1],vertices[2]], [vertices[2], vertices[3], vertices[0]]] + + # write the facets (triangles) in the stl file + for triangle in triangles: + stl.write(f"facet normal {normal[0]} {normal[2]} {normal[1]}\n") #following the idea that y axis is the height + stl.write(" outer loop\n") + for vertex in triangle: + stl.write(f" vertex {vertex[0]} {vertex[1]} {vertex[2]}\n") + stl.write(" endloop\n") + stl.write("endfacet\n") + stl.write(f"endsolid building{building.name}\n") \ No newline at end of file diff --git a/main.py b/main.py index 6fb66a5e..0007e32c 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,31 @@ +from scripts.geojson_creator import process_geojson +from pathlib import Path +from scripts.ep_run_enrich import energy_plus_workflow +from scripts.CityBEM_run import CityBEM_workflow +from hub.imports.geometry_factory import GeometryFactory +from hub.helpers.dictionaries import Dictionaries +from hub.imports.construction_factory import ConstructionFactory +from hub.imports.usage_factory import UsageFactory +from hub.imports.weather_factory import WeatherFactory +from hub.imports.results_factory import ResultFactory +import hub.helpers.constants as cte +from hub.exports.exports_factory import ExportsFactory - - - - - +# Specify the GeoJSON file path +geojson_file = process_geojson(x=-73.5681295982132, y=45.49218262677643, diff=0.001) +file_path = (Path(__file__).parent / 'input_files' / 'output_buildings.geojson') +# Specify the output path for the PDF file +output_path = (Path(__file__).parent / 'out_files').resolve() +# Create city object from GeoJSON file +city = GeometryFactory('geojson', + path=file_path, + height_field='height', + year_of_construction_field='year_of_construction', + function_field='function', + function_to_hub=Dictionaries().montreal_function_to_hub_function).city +# Enrich city data +ConstructionFactory('nrcan', city).enrich() +UsageFactory('nrcan', city).enrich() +WeatherFactory('epw', city).enrich() +CityBEM_workflow(city) #run the city using a fast City-Building Energy Model, CityBEM +print ("test done") \ No newline at end of file diff --git a/scripts/CityBEM_run.py b/scripts/CityBEM_run.py new file mode 100644 index 00000000..2468e37c --- /dev/null +++ b/scripts/CityBEM_run.py @@ -0,0 +1,267 @@ +import pandas as pd +import sys +import csv +import json +from shapely.geometry import Polygon +from pathlib import Path +import subprocess +from hub.exports.exports_factory import ExportsFactory +from hub.imports.weather.epw_weather_parameters import EpwWeatherParameters + +sys.path.append('./') + + +def CityBEM_workflow(city): + """ + Main function to run the CityBEM under the CityLayer's hub. + + :Note: City object contains necessary attributes for the CityBEM workflow. + """ + #general output path for the CityLayer's hub + out_path = Path(__file__).parent.parent / 'out_files' + #create a directory for running CityBEM under the main out_path + CityBEM_path = out_path / 'CityBEM_input_output' + if not CityBEM_path.exists(): + CityBEM_path.mkdir(parents=True, exist_ok=True) + + # Define the path to the GeoJSON file + file_path = Path(__file__).parent.parent / 'input_files' / 'output_buildings.geojson' + + #load the geojson file (for now this is necessary, later, it should be removed to extract building usage type code, center lat and lon). Later, these should be added to the building class + with open(file_path, 'r') as f: + geojson_data = json.load(f) + + #call functions to provide inputs for CityBEM and finally run CityBEM + export_geometry(city, CityBEM_path) + export_building_info(city, CityBEM_path,geojson_data) + export_weather_data(city, CityBEM_path) + export_comprehensive_building_data(city, CityBEM_path) + run_CityBEM(CityBEM_path) +def export_geometry(city, CityBEM_path): + """ + Export the STL geometry from the hub and rename the exported geometry to a proper name for CityBEM. + :param city: City object containing necessary attributes for the workflow. + :param CityBEM_path: Path where CityBEM input and output files are stored. + """ + ExportsFactory('stl', city, CityBEM_path).export() + hubGeometryName = city.name + '.stl' + #delete old files related to geometry if they exist + CityBEMGeometryPath1 = CityBEM_path / 'Input_City_scale_geometry_CityBEM.stl' + CityBEMGeometryPath2 = CityBEM_path / 'Input_City_scale_geometry_CityBEM.txt' #delete this file to ensure CityBEM generates a new one based on the new input geometry + if CityBEMGeometryPath1.exists(): + CityBEMGeometryPath1.unlink() + if CityBEMGeometryPath2.exists(): + CityBEMGeometryPath2.unlink() + (CityBEM_path / hubGeometryName).rename(CityBEM_path / CityBEMGeometryPath1) + print("CityBEM input geometry file named Input_City_scale_geometry_CityBEM.stl file has been created successfully") +def get_building_info(geojson_data, building_id): + for feature in geojson_data['features']: + if feature['id'] == building_id: + function_code = feature['properties']['function'] + coordinates = feature['geometry']['coordinates'][0] + + #calculate the center of the polygon + polygon = Polygon(coordinates) + center = polygon.centroid + + return function_code, (center.x, center.y) + + return None, None +def export_building_info(city, CityBEM_path, geojson_file): + """ + Generate the input building information file for CityBEM. + + :param city: City object containing necessary attributes for the workflow. + :param CityBEM_path: Path where CityBEM input and output files are stored. + """ + buildingInfo_path = CityBEM_path / 'Input_City_scale_building_info.txt' + with open(buildingInfo_path, "w", newline="") as textfile: #here, "w" refers to write mode. This deletes everything inside the file if the file exists. + writer = csv.writer(textfile, delimiter="\t") #use tab delimiter for all CityBEM inputs + writer.writerow(["building_stl", "building_osm", "constructionYear", "codeUsageType", "centerLongitude", "centerLatitude"]) # Header + for building in city.buildings: + function_code, center_coordinates = get_building_info(geojson_file, int (building.name)) + row = ["b" + building.name, "999999", str(building.year_of_construction), str(function_code), str(center_coordinates[0]), str(center_coordinates[1])] + #note: based on CityBEM legacy, using a number like "999999" means that the data is not known/available. + writer.writerow(row) + + print("CityBEM input file named Input_City_scale_building_info.txt file has been created successfully") +def export_weather_data(city, CityBEM_path): + """ + Generate the input weather data file compatible to CityBEM. + + :param city: City object containing necessary attributes for the workflow. + :param CityBEM_path: Path where CityBEM input and output files are stored. + """ + weatherParameters = EpwWeatherParameters(city)._weather_values + weatherParameters = pd.DataFrame(weatherParameters) #transfer the weather data to a DataFrame + + with open(CityBEM_path / 'Input_weatherdata.txt', 'w') as textfile: + # write the header information + textfile.write('Weather_timestep(s)\t3600\n') + textfile.write('Weather_columns\t11\n') #so far, 11 columns can be extracted from the epw weather data. + textfile.write('Date\tTime\tGHI\tDNI\tDHI\tTa\tTD\tTG\tRH\tWS\tWD\n') + for _, row in weatherParameters.iterrows(): + #form the Date and Time + Date = f"{int(row['year'])}-{int(row['month']):02d}-{int(row['day']):02d}" + Time = f"{int(row['hour']):02d}:{int(row['minute']):02d}" + #retrieve the weather data + GHI = row['global_horizontal_radiation_wh_m2'] + DNI = row['direct_normal_radiation_wh_m2'] + DHI = row['diffuse_horizontal_radiation_wh_m2'] + Ta = row['dry_bulb_temperature_c'] + TD = row['dew_point_temperature_c'] + TG = row['dry_bulb_temperature_c'] + RH = row['relative_humidity_perc'] + WS = row['wind_speed_m_s'] + WD = row['wind_direction_deg'] + #write the data in tab-separated format into the text file + textfile.write(f"{Date}\t{Time}\t{GHI}\t{DNI}\t{DHI}\t{Ta}\t{TD}\t{TG}\t{RH}\t{WS}\t{WD}\n") + + print("CityBEM input file named Input_weatherdata.txt file has been created successfully") + +def export_comprehensive_building_data(city, CityBEM_path): + """ + Export all other information from buildings (both physical and thermal properties) + :param city: City object containing necessary attributes for the workflow. + :param CityBEM_path: Path where CityBEM input and output files are stored. + """ + with open(CityBEM_path / 'comprehensive_building_data.csv', 'w', newline='') as textfile: + writer = csv.writer(textfile, delimiter=',') + header_row=[ + #building general information + "buildingName", + "constructionYear", + "function", + "roofType", + "maxHeight", + "storyHeight", + "storiesAboveGround", + "floorArea", + "volume", + "totalFloorArea", + #roof details + "roofThickness", + "roofExternalH", + "roofInternalH", + "roofUvalue", + "roofWWR", + #floor details + "floorThickness", + "floorExternalH", + "floorInternalH", + "floorUvalue", + "floorWWR", + #wall details + "wallThickness", + "wallExternalH", + "wallInternalH", + "wallUValue", + "wallWWRNorth", + "wallWWREast", + "wallWWRSouth", + "wallWWRWest", + #window details + "windowOverallUValue", + "windowGValue", + "windowFrameRatio", + #building thermal details + "thermalBridgesExtraLoses", + "infiltrationRateOff", + "infiltrationRateOn" + ] + writer.writerow(header_row) #write the header row + + #extract and write comprehensive building data from the CityLayer's hub + for building in city.buildings: + #data should be appended based on the order of the headers. + row=[] + row.append("b" + building.name) + row.append(building.year_of_construction) + row.append(building.function) + row.append(building.roof_type) + row.append(building.max_height) + row.append(building._storeys_above_ground) + row.append(building.average_storey_height) + row.append(building.floor_area) + row.append(building.volume) + # Initialize boundary rows + row_roof = [None, None, None, None, None] + row_ground = [None, None, None, None, None] + row_wall = [None, None, None, None, None] + wallCount = 0 # so far, the data for one wall represents all the walls + for internal_zone in building.internal_zones: + totalFloorArea = internal_zone.thermal_zones_from_internal_zones[0].total_floor_area + row.append(totalFloorArea) #append the last item in "building general information" + WWR = internal_zone.thermal_archetype.constructions[0].window_ratio #window to wall ratio for the walls + northWWR = float(WWR['north'])/100. #the values from the hub is in percent. The conversion is needed. + eastWWR = float(WWR['east'])/100. + southWWR = float(WWR['south'])/100. + westWWR = float(WWR['west'])/100. + windowOverallUValue = internal_zone.thermal_archetype.constructions[0].window_overall_u_value + windowGValue = internal_zone.thermal_archetype.constructions[0].window_g_value + windowFrameRatio = internal_zone.thermal_archetype.constructions[0].window_frame_ratio + thermalBridgesExtraLoses = internal_zone.thermal_archetype.extra_loses_due_to_thermal_bridges + infiltrationRateOff = internal_zone.thermal_archetype.infiltration_rate_for_ventilation_system_off + infiltrationRateOn = internal_zone.thermal_archetype.infiltration_rate_for_ventilation_system_on + for boundary in internal_zone.thermal_zones_from_internal_zones: + for thermal_boundary in boundary.thermal_boundaries: + if thermal_boundary.type == "Roof": + row_roof = [ + thermal_boundary.thickness, + thermal_boundary.he, + thermal_boundary.hi, + thermal_boundary.u_value, + thermal_boundary.window_ratio + ] + elif thermal_boundary.type == "Ground": + row_ground = [ + thermal_boundary.thickness, + thermal_boundary.he, + thermal_boundary.hi, + thermal_boundary.u_value, + thermal_boundary.window_ratio + ] + elif thermal_boundary.type == "Wall" and wallCount == 0: + wallCount += 1 + row_wall = [ + thermal_boundary.thickness, + thermal_boundary.he, + thermal_boundary.hi, + thermal_boundary.u_value, + northWWR, + eastWWR, + southWWR, + westWWR + ] + row.extend(row_roof) + row.extend(row_ground) + row.extend(row_wall) + #append window details + row.append(windowOverallUValue) + row.append(windowGValue) + row.append(windowFrameRatio) + #append building thermal details + row.append(thermalBridgesExtraLoses) + row.append(infiltrationRateOff) + row.append(infiltrationRateOn) + writer.writerow(row) + +def run_CityBEM(CityBEM_path): + """ + Run the CityBEM executable after all inputs are processed. + + :param CityBEM_path: Path where CityBEM input and output files are stored. + """ + try: + print('CityBEM execution began:') + CityBEM_exe = CityBEM_path / 'CityBEM.exe' #path to the CityBEM executable + #check if the executable file exists + if not CityBEM_exe.exists(): + print(f"Error: {CityBEM_exe} does not exist.") + subprocess.run(str(CityBEM_exe), check=True, cwd=str(CityBEM_path)) #execute the CityBEM executable + print("CityBEM executable has finished successfully.") + except Exception as ex: + print(ex) + print('error: ', ex) + print('[CityBEM simulation abort]') + sys.stdout.flush() #print all the running information on the screen \ No newline at end of file