(WIP) feature: add pipe sizing to dhn analysis

This commit is contained in:
Majid Rezaei 2024-08-01 11:39:05 -04:00
parent ad2d0a86d5
commit 6f7d08f5ee
6 changed files with 350 additions and 529 deletions

23
main.py
View File

@ -25,6 +25,7 @@ from scripts.pv_feasibility import pv_feasibility
import matplotlib.pyplot as plt
from scripts.district_heating_network.district_heating_network_creator import DistrictHeatingNetworkCreator
from scripts.district_heating_network.road_processor import road_processor
from scripts.district_heating_network.district_heating_factory import DistrictHeatingFactory
base_path = Path(__file__).parent
dir_manager = DirectoryManager(base_path)
@ -61,7 +62,7 @@ UsageFactory('nrcan', city).enrich()
WeatherFactory('epw', city).enrich()
# EnergyPlus workflow
# energy_plus_workflow(city, energy_plus_output_path)
energy_plus_workflow(city, energy_plus_output_path)
roads_file = road_processor(location[1], location[0], 0.001)
@ -69,5 +70,21 @@ dhn_creator = DistrictHeatingNetworkCreator(geojson_file_path, roads_file)
network_graph = dhn_creator.run()
for node_id, attrs in network_graph.nodes(data=True):
print(f"Node {node_id} has attributes: {dict(attrs)}")
DistrictHeatingFactory(
city,
network_graph,
60,
40,
0.8
).enrich()
DistrictHeatingFactory(
city,
network_graph,
60,
40,
0.8
).sizing()
for u, v, attributes in network_graph.edges(data=True):
print(f"Edge between {u} and {v} with attributes: {attributes}")

View File

@ -0,0 +1,86 @@
import json
import csv
import logging
class NetworkGraphExporter:
"""
A class to export a network graph to various formats like CSV and GeoJSON.
"""
def __init__(self, graph):
"""
Initialize the exporter with a network graph.
:param graph: A NetworkX graph object.
"""
self.graph = graph
def to_csv(self, file_path):
"""
Save the graph nodes with their type, names, and positions to a CSV file.
:param file_path: The path to the output CSV file.
"""
try:
with open(file_path, mode='w', newline='') as file:
writer = csv.writer(file)
writer.writerow(['Node ID', 'Name', 'Type', 'Position'])
for node, data in self.graph.nodes(data=True):
writer.writerow([node, data['name'], data['type'], data['pos']])
logging.info(f"Graph successfully saved to CSV file: {file_path}")
except Exception as e:
logging.error(f"Error saving graph to CSV file: {e}")
def to_geojson(self, file_path):
"""
Save the graph to a GeoJSON file.
:param file_path: The path to the output GeoJSON file.
"""
try:
features = []
for node, data in self.graph.nodes(data=True):
feature = {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": data['pos']
},
"properties": {
"id": node,
"name": data['name'],
"type": data['type']
}
}
features.append(feature)
for u, v, data in self.graph.edges(data=True):
feature = {
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [self.graph.nodes[u]['pos'], self.graph.nodes[v]['pos']]
},
"properties": {
"length": data['length']
}
}
features.append(feature)
geojson = {
"type": "FeatureCollection",
"features": features
}
with open(file_path, 'w') as f:
json.dump(geojson, f, indent=2)
logging.info(f"Graph successfully saved to GeoJSON file: {file_path}")
except Exception as e:
logging.error(f"Error saving graph to GeoJSON file: {e}")
# Configure logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logging.getLogger("numexpr").setLevel(logging.ERROR)

View File

@ -1,20 +1,26 @@
import networkx as nx
import logging
import CoolProp as CP
import math
class DistrictHeatingFactory:
"""
DistrictHeatingFactory class
"""
def __init__(self, city, graph):
def __init__(self, city, graph, supply_temperature, return_temperature, simultaneity_factor):
self._city = city
self._network_graph = graph
self._supply_temperature = supply_temperature
self._return_temperature = return_temperature
self.simultaneity_factor = simultaneity_factor
self.fluid = "Water"
def enrich(self):
"""
Enrich the network graph nodes with attributes from the city buildings.
Enrich the network graph nodes with the whole building object from the city buildings.
"""
for node in self._network_graph.nodes(data=True):
node_id, node_attrs = node
if node_attrs.get('type') == 'building':
@ -22,11 +28,51 @@ class DistrictHeatingFactory:
building_found = False
for building in self._city.buildings:
if building.name == building_name:
building_attrs = vars(building)
for attr, value in building_attrs.items():
if attr not in self._network_graph.nodes[node_id]:
self._network_graph.nodes[node_id][attr] = value
self._network_graph.nodes[node_id]['building_obj'] = building
building_found = True
break
if not building_found:
logging.error(msg=f"Building with name '{building_name}' not found in city.")
logging.error(msg=f"Building with name '{building_name}' not found in city.")
def sizing(self):
"""
Calculate the diameter of the pipes in the district heating network.
"""
for node in self._network_graph.nodes(data=True):
node_id, node_attrs = node
if node_attrs.get('type') == 'building':
building = node_attrs.get('building_obj') # Adjusted key to match your data
if building.heating_peak_load["year"][0]:
# Calculate peak mass flow rate
peak_mass_flow_rate = building.heating_peak_load["year"][0] / CP.PropsSI('C',
'T',
(
self._supply_temperature +
self._return_temperature
) / 2,
'P',
101325,
self.fluid) / (
self._supply_temperature - self._return_temperature)
# Calculate density of the fluid
density = CP.PropsSI('D', # 'D' for density
'T',
(
self._supply_temperature +
self._return_temperature
) / 2,
'P',
101325,
self.fluid)
# Set the design velocity (V)
velocity = 0.9 # m/s
# Calculate the diameter (D)
diameter = math.sqrt((4 * peak_mass_flow_rate) / (density * velocity * math.pi)) * 1000 # mm
# Find the edge connected to the building node
for neighbor in self._network_graph.neighbors(node_id):
if not self._network_graph.nodes[neighbor].get('type') == 'building': # Ensure it's a pipe connection
self._network_graph.edges[node_id, neighbor]['diameter'] = diameter

View File

@ -8,9 +8,8 @@ from typing import List, Tuple
from rtree import index
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logging.getLogger('numexpr').setLevel(logging.ERROR)
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logging.getLogger("numexpr").setLevel(logging.ERROR)
def haversine(lon1, lat1, lon2, lat2):
"""
@ -30,17 +29,21 @@ def haversine(lon1, lat1, lon2, lat2):
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
return R * c # Output distance in meters
class DistrictHeatingNetworkCreator:
def __init__(self, buildings_file: str, roads_file: str):
def __init__(self, buildings_file: str, roads_file: str, central_plant_locations: List[Tuple[float, float]]):
"""
Initialize the class with paths to the buildings and roads data files.
Initialize the class with paths to the buildings and roads data files, and central plant locations.
:param buildings_file: Path to the GeoJSON file containing building data.
:param roads_file: Path to the GeoJSON file containing roads data.
:param central_plant_locations: List of tuples containing the coordinates of central plant locations.
"""
if len(central_plant_locations) < 1:
raise ValueError("The list of central plant locations must have at least one member.")
self.buildings_file = buildings_file
self.roads_file = roads_file
self.central_plant_locations = central_plant_locations
def run(self) -> nx.Graph:
"""
@ -57,7 +60,8 @@ class DistrictHeatingNetworkCreator:
self._iteratively_remove_edges()
self._add_centroids_to_mst()
self._convert_edge_weights_to_meters()
return self.final_mst
self._create_final_network_graph()
return self.network_graph
except Exception as e:
logging.error(f"Error during network creation: {e}")
raise
@ -73,6 +77,7 @@ class DistrictHeatingNetworkCreator:
self.centroids = []
self.building_names = []
self.building_positions = []
buildings = city['features']
for building in buildings:
coordinates = building['geometry']['coordinates'][0]
@ -80,6 +85,14 @@ class DistrictHeatingNetworkCreator:
centroid = building_polygon.centroid
self.centroids.append(centroid)
self.building_names.append(str(building['id']))
self.building_positions.append((centroid.x, centroid.y))
# Add central plant locations as centroids
for i, loc in enumerate(self.central_plant_locations, start=1):
centroid = Point(loc)
self.centroids.append(centroid)
self.building_names.append(f'central_plant_{i}')
self.building_positions.append((centroid.x, centroid.y))
# Load road data
with open(self.roads_file, 'r') as file:
@ -184,7 +197,9 @@ class DistrictHeatingNetworkCreator:
for line in self.cleaned_lines:
coords = list(line.coords)
for i in range(len(coords) - 1):
self.G.add_edge(coords[i], coords[i + 1], weight=Point(coords[i]).distance(Point(coords[i + 1])))
u = coords[i]
v = coords[i + 1]
self.G.add_edge(u, v, weight=Point(coords[i]).distance(Point(coords[i + 1])))
except Exception as e:
logging.error(f"Error creating graph: {e}")
raise
@ -284,24 +299,22 @@ class DistrictHeatingNetworkCreator:
"""
try:
for i, centroid in enumerate(self.centroids):
centroid_tuple = (centroid.x, centroid.y)
building_name = self.building_names[i]
# Add the centroid node with its attributes
self.final_mst.add_node(centroid_tuple, type='building', name=building_name)
pos = (centroid.x, centroid.y)
node_type = 'building' if 'central_plant' not in building_name else 'generation'
self.final_mst.add_node(pos, type=node_type, name=building_name, pos=pos)
nearest_point = None
min_distance = float('inf')
for node in self.final_mst.nodes():
if self.final_mst.nodes[node].get('type') != 'building':
node_point = Point(node)
distance = centroid.distance(node_point)
if self.final_mst.nodes[node].get('type') != 'building' and self.final_mst.nodes[node].get('type') != 'generation':
distance = centroid.distance(Point(node))
if distance < min_distance:
min_distance = distance
nearest_point = node
if nearest_point:
self.final_mst.add_edge(centroid_tuple, nearest_point, weight=min_distance)
self.final_mst.add_edge(pos, nearest_point, weight=min_distance)
except Exception as e:
logging.error(f"Error adding centroids to MST: {e}")
raise
@ -312,22 +325,48 @@ class DistrictHeatingNetworkCreator:
"""
try:
for u, v, data in self.final_mst.edges(data=True):
lon1, lat1 = u
lon2, lat2 = v
distance = haversine(lon1, lat1, lon2, lat2)
distance = haversine(u[0], u[1], v[0], v[1])
self.final_mst[u][v]['weight'] = distance
except Exception as e:
logging.error(f"Error converting edge weights to meters: {e}")
raise
def _create_final_network_graph(self):
"""
Create the final network graph with the required attributes from the final MST.
"""
self.network_graph = nx.Graph()
node_id = 1
node_mapping = {}
for node in self.final_mst.nodes:
pos = node
if 'type' in self.final_mst.nodes[node]:
if self.final_mst.nodes[node]['type'] == 'building':
name = self.final_mst.nodes[node]['name']
node_type = 'building'
elif self.final_mst.nodes[node]['type'] == 'generation':
name = self.final_mst.nodes[node]['name']
node_type = 'generation'
else:
name = f'junction_{node_id}'
node_type = 'junction'
self.network_graph.add_node(node_id, name=name, type=node_type, pos=pos)
node_mapping[node] = node_id
node_id += 1
for u, v, data in self.final_mst.edges(data=True):
u_new = node_mapping[u]
v_new = node_mapping[v]
length = data['weight']
self.network_graph.add_edge(u_new, v_new, length=length)
def plot_network_graph(self):
"""
Plot the network graph using matplotlib and networkx.
"""
plt.figure(figsize=(15, 10))
pos = {node: (node[0], node[1]) for node in self.final_mst.nodes()}
nx.draw_networkx_nodes(self.final_mst, pos, node_color='blue', node_size=50)
nx.draw_networkx_edges(self.final_mst, pos, edge_color='gray')
pos = {node: data['pos'] for node, data in self.network_graph.nodes(data=True)}
nx.draw_networkx_nodes(self.network_graph, pos, node_color='blue', node_size=50)
nx.draw_networkx_edges(self.network_graph, pos, edge_color='gray')
plt.title('District Heating Network Graph')
plt.axis('off')
plt.show()

View File

@ -1,54 +0,0 @@
import json
from shapely import LineString, Point
import networkx as nx
from pathlib import Path
def networkx_to_geojson(graph: nx.Graph) -> Path:
"""
Convert a NetworkX graph to GeoJSON format.
:param graph: A NetworkX graph.
:return: GeoJSON formatted dictionary.
"""
features = []
for u, v, data in graph.edges(data=True):
line = LineString([u, v])
feature = {
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": list(line.coords)
},
"properties": {
"weight": data.get("weight", 1.0)
}
}
features.append(feature)
for node, data in graph.nodes(data=True):
point = Point(node)
feature = {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": list(point.coords)[0]
},
"properties": {
"type": data.get("type", "unknown"),
"id": data.get("id", "N/A")
}
}
features.append(feature)
geojson = {
"type": "FeatureCollection",
"features": features
}
output_geojson_file = Path('./out_files/network_graph.geojson').resolve()
with open(output_geojson_file, 'w') as file:
json.dump(geojson, file, indent=4)
return output_geojson_file

File diff suppressed because one or more lines are too long