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