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..06a45d3e 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,35 @@ +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 +from hub.exports.exports_factory import ExportsFactory +import csv +# 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() +ExportsFactory('obj', city, output_path).export() +ExportsFactory('stl', city, output_path).export() +WeatherFactory('epw', city).enrich() +CityBEM_workflow(city) +#energy_plus_workflow(city) +print('The CityBEM test workflow is 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..b43d11de --- /dev/null +++ b/scripts/CityBEM_run.py @@ -0,0 +1,170 @@ +import pandas as pd +import sys +import csv +from pathlib import Path +import subprocess +from hub.helpers.dictionaries import Dictionaries +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) + + #call functions to provide inputs for CityBEM and finally run CityBEM + export_geometry(city, CityBEM_path) + export_building_info(city, CityBEM_path) + 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 export_building_info(city, CityBEM_path): + """ + 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' + montreal_to_hub_function_dict = Dictionaries().montreal_function_to_hub_function + reverse_dict = {v: k for k, v in montreal_to_hub_function_dict.items()} #inverting the montreal_function_to_hub_function (this is not a good approach) + with open(buildingInfo_path, "w", newline="") as textfile: #here, "w" refers to write mode. This deletes 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: + row = ["b" + building.name, "99999", str(building.year_of_construction), str(reverse_dict.get(building.function)), "-73.5688", "45.5018"] + 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=["buildingName", + "constructionYear", + "function", + "roofType", + "maxHeight", + "storyHeight", + "storiesAboveGround", + "floorArea", + "volume", + "wallThickness", + "wallExternalH", + "wallInternalH", + "wallUValue" + ] + writer.writerow(header_row) #write the header row + #write comprehensive building data from the CityLayer's hub + for building in city.buildings: + wallCount=0 + for wall in building.walls: + if wallCount==0: + for thermalBoundary in wall.associated_thermal_boundaries: + wallThickness = thermalBoundary.thickness + wallExternalH=thermalBoundary.he + wallInternalH=thermalBoundary.hi + wallUValue=thermalBoundary.u_value + row = [ + "b" + building.name, + building.year_of_construction, + building.function, + building.roof_type, + building.max_height, + building._storeys_above_ground, + building.average_storey_height, + building.floor_area, + building.volume, + wallThickness, + wallExternalH, + wallInternalH, + wallUValue + ] + writer.writerow(row) + wallCount=wallCount+1 + +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