(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 8bddefabde
4 changed files with 1407 additions and 436 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

@ -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,7 +29,6 @@ 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):
"""
@ -41,6 +39,8 @@ class DistrictHeatingNetworkCreator:
"""
self.buildings_file = buildings_file
self.roads_file = roads_file
self.node_counter = 0 # Counter to assign unique integer IDs to nodes
self.node_mapping = {} # Mapping from (x, y) tuples to integer IDs
def run(self) -> nx.Graph:
"""
@ -57,6 +57,7 @@ class DistrictHeatingNetworkCreator:
self._iteratively_remove_edges()
self._add_centroids_to_mst()
self._convert_edge_weights_to_meters()
self._reassign_node_ids() # Reassign node IDs to be sequential
return self.final_mst
except Exception as e:
logging.error(f"Error during network creation: {e}")
@ -73,6 +74,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 +82,7 @@ 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))
# Load road data
with open(self.roads_file, 'r') as file:
@ -184,11 +187,24 @@ 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 = self._get_or_create_node_id(coords[i])
v = self._get_or_create_node_id(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
def _get_or_create_node_id(self, pos):
"""
Get the node ID for a position or create a new one if it doesn't exist.
"""
if pos not in self.node_mapping:
self.node_counter += 1
self.node_mapping[pos] = self.node_counter
# Initially, assume the node is a junction
self.G.add_node(self.node_counter, pos=pos, type='junction', name=f'junction_{self.node_counter}')
return self.node_mapping[pos]
def _create_mst(self):
"""
Create a Minimum Spanning Tree (MST) from the graph.
@ -197,9 +213,9 @@ class DistrictHeatingNetworkCreator:
def find_paths_between_nearest_points(g: nx.Graph, nearest_points: List[Point]) -> List[Tuple]:
edges = []
for i, start_point in enumerate(nearest_points):
start = (start_point.x, start_point.y)
start = self._get_or_create_node_id((start_point.x, start_point.y))
for end_point in nearest_points[i + 1:]:
end = (end_point.x, end_point.y)
end = self._get_or_create_node_id((end_point.x, end_point.y))
if nx.has_path(g, start, end):
path = nx.shortest_path(g, source=start, target=end, weight='weight')
path_edges = list(zip(path[:-1], path[1:]))
@ -219,21 +235,41 @@ class DistrictHeatingNetworkCreator:
final_edges.extend((x, y, self.G[x][y]['weight']) for x, y in path_edges)
self.final_mst = nx.Graph()
self.final_mst.add_weighted_edges_from(final_edges)
# Ensure all nodes in final_mst have the required attributes
for node in self.final_mst.nodes:
pos = self._get_pos_from_node(node)
if pos:
if 'type' not in self.final_mst.nodes[node]:
self.final_mst.nodes[node]['type'] = 'junction'
self.final_mst.nodes[node].update({
'pos': pos,
'name': f'junction_{node}' if self.final_mst.nodes[node]['type'] == 'junction' else self.final_mst.nodes[node]['name']
})
except Exception as e:
logging.error(f"Error creating MST: {e}")
raise
def _get_pos_from_node(self, node):
"""
Get the position (x, y) for a node.
"""
for pos, node_id in self.node_mapping.items():
if node_id == node:
return pos
return None
def _iteratively_remove_edges(self):
"""
Iteratively remove edges that do not have any nearest points and have one end with only one connection.
Also remove nodes that don't have any connections and street nodes with only one connection.
"""
nearest_points_tuples = [(point.x, point.y) for point in self.nearest_points]
nearest_points_ids = [self._get_or_create_node_id((point.x, point.y)) for point in self.nearest_points]
def find_edges_to_remove(graph: nx.Graph) -> List[Tuple]:
edges_to_remove = []
for u, v, d in graph.edges(data=True):
if u not in nearest_points_tuples and v not in nearest_points_tuples:
if u not in nearest_points_ids and v not in nearest_points_ids:
if graph.degree(u) == 1 or graph.degree(v) == 1:
edges_to_remove.append((u, v, d))
return edges_to_remove
@ -259,7 +295,7 @@ class DistrictHeatingNetworkCreator:
def find_single_connection_street_nodes(graph: nx.Graph) -> List[Tuple]:
single_connection_street_nodes = []
for node in graph.nodes():
if node not in nearest_points_tuples and graph.degree(node) == 1:
if node not in nearest_points_ids and graph.degree(node) == 1:
single_connection_street_nodes.append(node)
return single_connection_street_nodes
@ -284,24 +320,25 @@ class DistrictHeatingNetworkCreator:
"""
try:
for i, centroid in enumerate(self.centroids):
centroid_tuple = (centroid.x, centroid.y)
building_name = self.building_names[i]
pos = (centroid.x, centroid.y)
node_id = self._get_or_create_node_id(pos)
# Add the centroid node with its attributes
self.final_mst.add_node(centroid_tuple, type='building', name=building_name)
# Update the node to be a building
self.final_mst.add_node(node_id, pos=pos, type='building', name=building_name)
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)
if self.final_mst.nodes[node].get('type') == 'junction':
node_point = Point(self.final_mst.nodes[node]['pos'])
distance = centroid.distance(node_point)
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(node_id, nearest_point, weight=min_distance)
except Exception as e:
logging.error(f"Error adding centroids to MST: {e}")
raise
@ -312,20 +349,37 @@ 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)
pos_u = self._get_pos_from_node(u)
pos_v = self._get_pos_from_node(v)
distance = haversine(pos_u[0], pos_u[1], pos_v[0], pos_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 _reassign_node_ids(self):
"""
Reassign node IDs to ensure they are sequential starting from 1.
"""
mapping = {old_id: new_id for new_id, old_id in enumerate(self.final_mst.nodes, start=1)}
self.final_mst = nx.relabel_nodes(self.final_mst, mapping)
for node_id in self.final_mst.nodes:
if 'type' not in self.final_mst.nodes[node_id]:
self.final_mst.nodes[node_id]['type'] = 'junction'
pos = self._get_pos_from_node(node_id)
node_type = self.final_mst.nodes[node_id]['type']
name = self.final_mst.nodes[node_id].get('name', f'junction_{node_id}')
self.final_mst.nodes[node_id].update({
'pos': pos,
'name': name if node_type == 'building' else f'junction_{node_id}'
})
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()}
pos = {node: self.final_mst.nodes[node]['pos'] 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')
plt.title('District Heating Network Graph')

File diff suppressed because it is too large Load Diff