391 lines
20 KiB
Python
391 lines
20 KiB
Python
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.
|
|
The first final version is created at 10/07/2024
|
|
"""
|
|
#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)
|
|
export_indoor_temperature_setpoint_data(city, CityBEM_path)
|
|
export_internal_heat_gain_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):
|
|
"""
|
|
Extract and export detailed individual building data from the hub to replace CityBEM input archetypes, including 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 / 'Input_comprehensive_building_data_CityLayer.txt', 'w') as textfile:
|
|
writer = csv.writer(textfile, delimiter=',')
|
|
header_row="\t".join([
|
|
#building general information
|
|
"buildingName",
|
|
"constructionYear",
|
|
"function",
|
|
"roofType",
|
|
"maxHeight",
|
|
"storyHeight",
|
|
"storiesAboveGround",
|
|
"floorArea",
|
|
"volume",
|
|
"totalFloorArea",
|
|
#roof details
|
|
"roofThickness",
|
|
"roofExternalH",
|
|
"roofInternalH",
|
|
"roofUvalue",
|
|
"roofLongWaveEmittance",
|
|
"roofShortWaveReflectance",
|
|
"roofDensity",
|
|
"roofSpecificHeat",
|
|
"roofWWR",
|
|
#floor details
|
|
"floorThickness",
|
|
"floorExternalH",
|
|
"floorInternalH",
|
|
"floorUvalue",
|
|
"floorLongWaveEmittance",
|
|
"floorShortWaveReflectance",
|
|
"floorDensity",
|
|
"floorSpecificHeat",
|
|
"floorWWR",
|
|
#wall details
|
|
"wallThickness",
|
|
"wallExternalH",
|
|
"wallInternalH",
|
|
"wallUValue",
|
|
"wallLongWaveEmittance",
|
|
"wallShortWaveReflectance",
|
|
"wallDensity",
|
|
"wallSpecificHeat",
|
|
"wallWWRNorth",
|
|
"wallWWREast",
|
|
"wallWWRSouth",
|
|
"wallWWRWest",
|
|
#window details
|
|
"windowOverallUValue",
|
|
"windowGValue",
|
|
"windowFrameRatio",
|
|
#building thermal details
|
|
"thermalBridgesExtraLoses",
|
|
"infiltrationRateOff",
|
|
"infiltrationRateOn"
|
|
])
|
|
textfile.write(header_row + "\n") #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":
|
|
layers = thermal_boundary.layers #access the roof construction layers
|
|
non_zero_layers = [layer for layer in layers if layer.thickness > 0] #filter out layers with zero thickness
|
|
total_thickness = thermal_boundary.thickness
|
|
if total_thickness > 0:
|
|
weighted_density = sum(layer.thickness * layer.density for layer in non_zero_layers) / total_thickness #weighted average represneting the entire layer.
|
|
weighted_specific_heat = sum(
|
|
layer.thickness * layer.specific_heat for layer in non_zero_layers) / total_thickness
|
|
else:
|
|
weighted_specific_heat = 0 #handle the case where total_thickness is zero to avoid division by zero
|
|
weighted_density = 0
|
|
row_roof = [
|
|
thermal_boundary.thickness,
|
|
thermal_boundary.he,
|
|
thermal_boundary.hi,
|
|
thermal_boundary.u_value,
|
|
thermal_boundary.external_surface.long_wave_emittance,
|
|
thermal_boundary.external_surface.short_wave_reflectance,
|
|
weighted_density,
|
|
weighted_specific_heat,
|
|
thermal_boundary.window_ratio
|
|
]
|
|
elif thermal_boundary.type == "Ground": #ground means floor in CityBEM based on the legacy in CityBEM.
|
|
layers = thermal_boundary.layers # access the roof construction layers
|
|
non_zero_layers = [layer for layer in layers if layer.thickness > 0] # filter out layers with zero thickness
|
|
total_thickness = thermal_boundary.thickness
|
|
if total_thickness > 0:
|
|
weighted_density = sum(
|
|
layer.thickness * layer.density for layer in non_zero_layers) / total_thickness
|
|
weighted_specific_heat = sum(
|
|
layer.thickness * layer.specific_heat for layer in non_zero_layers) / total_thickness
|
|
else:
|
|
weighted_specific_heat = 0 # Handle the case where total_thickness is zero to avoid division by zero
|
|
weighted_density = 0
|
|
row_ground = [
|
|
thermal_boundary.thickness,
|
|
thermal_boundary.he,
|
|
thermal_boundary.hi,
|
|
thermal_boundary.u_value,
|
|
thermal_boundary.external_surface.long_wave_emittance,
|
|
thermal_boundary.external_surface.short_wave_reflectance,
|
|
weighted_density,
|
|
weighted_specific_heat,
|
|
thermal_boundary.window_ratio
|
|
]
|
|
elif thermal_boundary.type == "Wall" and wallCount == 0:
|
|
wallCount += 1 #wall counter. So far, it is assumed that all the walls have a similar properties to be exported to CityBEM, except the WWR
|
|
layers = thermal_boundary.layers # access the roof construction layers
|
|
non_zero_layers = [layer for layer in layers if
|
|
layer.thickness > 0] # filter out layers with zero thickness
|
|
total_thickness = thermal_boundary.thickness
|
|
if total_thickness > 0:
|
|
weighted_density = sum(layer.thickness * layer.density for layer in non_zero_layers) / total_thickness
|
|
weighted_specific_heat = sum(
|
|
layer.thickness * layer.specific_heat for layer in non_zero_layers) / total_thickness
|
|
else:
|
|
weighted_specific_heat = 0 # Handle the case where total_thickness is zero to avoid division by zero
|
|
weighted_density = 0
|
|
row_wall = [
|
|
thermal_boundary.thickness,
|
|
thermal_boundary.he,
|
|
thermal_boundary.hi,
|
|
thermal_boundary.u_value,
|
|
thermal_boundary.external_surface.long_wave_emittance,
|
|
thermal_boundary.external_surface.short_wave_reflectance,
|
|
weighted_density,
|
|
weighted_specific_heat,
|
|
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)
|
|
|
|
#convert each item in row to string (if needed) and join with tabs (tab separated data)
|
|
row_str = "\t".join(map(str, row))
|
|
#write the final row to the text file
|
|
textfile.write(row_str + "\n")
|
|
print("Individual building data is exported into a file named comprehensive_building_data.txt")
|
|
def export_indoor_temperature_setpoint_data(city, CityBEM_path):
|
|
"""
|
|
Extract and export individual building data on indoor temperature setpoints
|
|
:param city: City object containing necessary attributes for the workflow.
|
|
:param CityBEM_path: Path where CityBEM input and output files are stored.
|
|
"""
|
|
#open a text file in write mode (write mode removes the content if there is any)
|
|
with open(CityBEM_path /'Input_indoor_setpoint_temperature_CityLayer.txt', 'w') as textfile:
|
|
#iterate through each building
|
|
for building in city.buildings:
|
|
#write the building name
|
|
textfile.write("building"+building.name + '\t')
|
|
#iterate through each internal zone in the building
|
|
for internal_zone in building.internal_zones:
|
|
#iterate through each boundary in the internal zone
|
|
for boundary in internal_zone.thermal_zones_from_internal_zones:
|
|
#gather all indoor setpoint values for both cooling and heating
|
|
indoorSetpointValues = []
|
|
indoorSetpointValues.extend(boundary.thermal_control.cooling_set_point_schedules[0].values)#cooling on working days
|
|
indoorSetpointValues.extend(boundary.thermal_control.cooling_set_point_schedules[1].values)#cooling on Saturday
|
|
indoorSetpointValues.extend(boundary.thermal_control.cooling_set_point_schedules[2].values)#cooling on Sunday/holidays
|
|
indoorSetpointValues.extend(boundary.thermal_control.heating_set_point_schedules[0].values)#heating on working days
|
|
indoorSetpointValues.extend(boundary.thermal_control.heating_set_point_schedules[1].values)#heating on Saturday
|
|
indoorSetpointValues.extend(boundary.thermal_control.heating_set_point_schedules[2].values)#heating on Sunday/holidays
|
|
|
|
#convert values to a tab-separated strings
|
|
values_str = '\t'.join(map(str, indoorSetpointValues))
|
|
|
|
#write the values to the text file for this building
|
|
textfile.write(values_str + '\n')
|
|
|
|
print("Indoor temperature setpoints for every building is successfully exported into a text file named Input_indoor_setpoint_temperature_CityLayer.txt")
|
|
def export_internal_heat_gain_data(city, CityBEM_path):
|
|
"""
|
|
Extract and export individual building data on internal heat gains (occupant, lighting, and equipment)
|
|
:param city: City object containing necessary attributes for the workflow.
|
|
:param CityBEM_path: Path where CityBEM input and output files are stored.
|
|
"""
|
|
# open a text file in write mode (write mode removes the content if there is any)
|
|
with open(CityBEM_path / 'Input_internal_heat_gain_CityLayer.txt', 'w') as textfile:
|
|
# iterate through each building
|
|
for building in city.buildings:
|
|
# write the building name
|
|
textfile.write("building" + building.name + '\t') # (1) building name
|
|
# gather all internal heat gains for every building
|
|
internalHeatGains = []
|
|
# iterate through each internal zone in the building
|
|
for internal_zone in building.internal_zones:
|
|
# iterate through each internal usage in the internal zone
|
|
for usage in internal_zone.usages:
|
|
# iterate through internal heat gains
|
|
for internalGain in usage.internal_gains: # order: Occupancy, Lighting, and Appliances
|
|
internalHeatGains.append(internalGain.average_internal_gain) # (2) average_internal_gain
|
|
internalHeatGains.append(internalGain.convective_fraction) # (3) convective_fraction
|
|
internalHeatGains.append(internalGain.latent_fraction) # (4) latent_fraction
|
|
internalHeatGains.append(internalGain.radiative_fraction) # (5) radiative_fraction
|
|
internalHeatGains.extend(internalGain.schedules[0].values) # (6-29) Working day
|
|
internalHeatGains.extend(internalGain.schedules[1].values) # (30-54) Saturday
|
|
internalHeatGains.extend(internalGain.schedules[2].values) # (55-79)Sunday
|
|
# convert values to a tab-separated strings
|
|
values_str = '\t'.join(map(str, internalHeatGains))
|
|
# write the values to the text file for this building
|
|
textfile.write(values_str + '\n')
|
|
print("Internal heat gains for every building is successfully exported into a text file named Input_internal_heat_gain_CityLayer.txt")
|
|
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 |