CityBEM workflow operational
This commit is contained in:
parent
1e34687496
commit
7ee9f03678
@ -2,7 +2,7 @@
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="hub" jdkType="Python SDK" />
|
||||
<orderEntry type="jdk" jdkName="C:\Users\sr283\miniconda3\envs\Hub" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
@ -1,4 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="hub" project-jdk-type="Python SDK" />
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="C:\Users\sr283\miniconda3\envs\Hub" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="C:\Users\sr283\miniconda3\envs\Hub" project-jdk-type="Python SDK" />
|
||||
</project>
|
@ -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")
|
37
main.py
37
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')
|
170
scripts/CityBEM_run.py
Normal file
170
scripts/CityBEM_run.py
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user