Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Two network comparison #12

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
210 changes: 210 additions & 0 deletions compare_graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import pypsa
import networkx as nx
import matplotlib.pyplot as plt


def graph_properties(network: pypsa.Network) -> dict:
"""
Extracts various properties of the provided PyPSA network object.

Args:
network (pypsa.Network): The PyPSA network object to analyze.

Returns:
dict: A dictionary containing the extracted network properties.
"""

# Get the underlying NetworkX graph object from the PyPSA network
graph = network.graph()

# Extract degree information for each node (node and its connected edges)
network_degree = dict(graph.degree())

# Calculate the average degree of connectivity in the network
network_average_degree_connectivity = nx.average_degree_connectivity(graph)

# Extract a list of edges and nodes from the NetworkX graph
network_edges = list(graph.edges())
network_nodes = list(graph.nodes())

# Get the total number of edges and nodes in the network
network_number_of_edges = graph.number_of_edges()
network_number_of_nodes = graph.number_of_nodes()

# Create a dictionary to store the extracted network properties
network_properties = {
"network_edges": network_edges,
"network_degree": network_degree,
"network_average_degree_connectivity": network_average_degree_connectivity,
"network_nodes": network_nodes,
"network_number_of_edges": network_number_of_edges,
"network_number_of_nodes": network_number_of_nodes,
}

return network_properties


def plot_network_graph(network: pypsa.Network, seed: int = 1969) -> None:
"""
Plots the network graph.

Args:
network: pypsa.Network
Returns:
None
"""
graph = network.graph()
options = {"node_color": "black", "node_size": 50,
"linewidths": 0, "width": 0.1}
pos = nx.spring_layout(graph, seed=seed) # Seed for reproducible layout
nx.draw(graph, pos, **options)
plt.savefig("network_graph.png", dpi=300)


def available_item_properties(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be better to split this method into two:

  • one method that compares the edges
  • one method that compares the nodes

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are referring to the compare_graph_properties on line 124 which is where I have implemented the method to compare both nodes and edges.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was a general comment about having separate methods for nodes and vertices so that you don't have to do:
if item not in ["network_nodes", "network_edges"]: # Validate item type

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, thanks for the clarity.

network1: pypsa.Network, network2: pypsa.Network, item: str
) -> list:
"""
Identifies items present in both network1 and network2 for the specified item type.

Args:
network1 (pypsa.Network): The first network to compare.
network2 (pypsa.Network): The second network to compare.
item (str): The type of item to check for availability, either "network_nodes" or "network_edges".

Returns:
list: A list of items present in both network1 and network2.
"""

if item not in ["network_nodes", "network_edges"]: # Validate item type
raise ValueError(
"Invalid item type. Must be 'network_nodes' or 'network_edges'."
)

available_item = [] # Initialize a list to store common items

# Iterate through items in network1 and check for presence in network2
for idx in network1[item]:
if idx in network2[item]:
available_item.append(idx)

return available_item


def missing_item_properties(
network1: pypsa.Network, network2: pypsa.Network, item: str
) -> list:
"""
Identifies items present in network1 but missing in network2 for the specified item type.

Args:
network1 (pypsa.Network): The first network to compare.
network2 (pypsa.Network): The second network to compare.
item (str): The type of item to check for missing entries, either "network_nodes" or "network_edges".

Returns:
list: A list of items present in network1 but missing in network2.
"""

if item not in ["network_nodes", "network_edges"]: # Validate item type
raise ValueError(
"Invalid item type. Must be 'network_nodes' or 'network_edges'."
)

missing_item = [] # Initialize a list to store missing items
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unfortunately this won't do what we want it to - here you want to compare the graphs without caring about the details of the buses making up the graph. Looking at the function graph.add_nodes_from(buses_i) at Line 64 of pypsa/graph.py, the function you've got here for comparing nodes would actually compare the lists of buses in each network. What we want to do instead is to compare e.g. the number of nodes in each graph

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, the pypsa-network is translated into a graph by using the buses as nodes and the links/links as edges. Treating the network as a graph means that no attention is paid to the size of the bus or the length of the links/lines.

Feel free to disagree :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, the graph method in pypsa was built ontop of networkx which is used to develop graphs in Python https://github.com/PyPSA/PyPSA/blob/master/pypsa/graph.py#L22

Copy link
Collaborator

@jessLryan jessLryan Mar 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At line 64 of the pypsa classs for graphs the graph is created from the set of buses:
graph.add_nodes_from(buses_i)
At line 53 of the same file, we set
buses_i = network.buses.index
As such, the nodes in the graph each correspond to the string index of the associated bus. This means that two nodes in the graph are defined to be equal if they have the same bus name. This means that when in your method you compare the nodes, you are only comparing the respective bus names in each network. This is not the information we want. Instead, we should look at general properties of the graph e.g. number of nodes, average degree.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could remove the nodes and edges properties from the code. But there can't be multiple buses with the same name in network.buses.index. I only added in case we need to do some more complex comparison which can also be useful for our matching algorithm which also relies on the index of buses.

let me know what you think?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the problem is that it isn't the names of the buses that we are interested in - it is their other properties


# Iterate through items in network1 and identify those missing in network2
for idx in network1[item]:
if idx not in network2[item]:
missing_item.append(idx)

return missing_item


def compare_graph_properties(network1: pypsa.Network, network2: pypsa.Network) -> dict:
"""
This function compares the properties of two PyPSA network objects.

Args:
network1 (pypsa.Network): The first network to compare.
network2 (pypsa.Network): The second network to compare.

Returns:
dict: A dictionary containing the similarities and differences between the networks.
"""

network1_properties = graph_properties(
network1) # Get properties of network 1
network2_properties = graph_properties(
network2) # Get properties of network 2

difference_dict = {} # Dictionary to store property differences
similar_dict = {} # Dictionary to store property similarities

results = {"similarities": similar_dict, "differences": difference_dict}

# Find common properties (edges & nodes) present in both networks
similar_dict["edges"] = available_item_properties(
network1_properties, network2_properties, "network_edges"
)
similar_dict["nodes"] = available_item_properties(
network1_properties, network2_properties, "network_nodes"
)

# Calculate the absolute difference in the number of edges and nodes
difference_dict["edge_difference"] = abs(
network1_properties["network_number_of_edges"]
- network2_properties["network_number_of_edges"]
)
difference_dict["node_difference"] = abs(
network1_properties["network_number_of_nodes"]
- network2_properties["network_number_of_nodes"]
)

# Find missing edges (those in network 1 but not in network 2 and vice versa)
edges_in_n1_not_in_n2 = missing_item_properties(
network1_properties, network2_properties, "network_edges"
)
edges_in_n2_not_in_n1 = missing_item_properties(
network2_properties, network1_properties, "network_edges"
)

# Find missing nodes (those in network 1 but not in network 2 and vice versa)
nodes_in_n1_not_in_n2 = missing_item_properties(
network1_properties, network2_properties, "network_nodes"
)
nodes_in_n2_not_in_n1 = missing_item_properties(
network2_properties, network1_properties, "network_nodes"
)

difference_dict["edges_in_n1_not_in_n2"] = edges_in_n1_not_in_n2
difference_dict["edges_in_n2_not_in_n1"] = edges_in_n2_not_in_n1
difference_dict["nodes_in_n1_not_in_n2"] = nodes_in_n1_not_in_n2
difference_dict["nodes_in_n2_not_in_n1"] = nodes_in_n2_not_in_n1

degree = {} # Dictionary to store degree information

# Calculate average degree for each network
average_network_degree = pd.DataFrame(
[
network1_properties["network_average_degree_connectivity"],
network2_properties["network_average_degree_connectivity"],
],
index=["network_1", "network_2"],
).T
degree["average_degree"] = average_network_degree.sort_index(inplace=True)

# Combine degree information for both networks into a DataFrame
network_degree = pd.DataFrame(
[network1_properties["network_degree"],
network2_properties["network_degree"]]
).T
network_degree.rename(
columns={"0": "network_1", "1": "network_2"}, inplace=True)

difference_dict["degree"] = {
"network_degree": network_degree,
"average_network_degree": average_network_degree,
}

return results