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