In my research, I use OpenStreetMap data for traffic-related simulations. Part of the data preparation involves using the osmnx library to get a simplified graph of the road network.
Currently, we do not want to consider one ways. In other words, every road should be represented as a single edge, regardless of whether or not it's a one-way or two-way street. This essentially means that I am looking to have an undirected graph rather than a directed graph.
The main problem is that osmnx's simplify graph only works with directed graphs.
If I call osmnx's simplify_graph function using a MultiDiGraph, I end up with something like this. In this example, the contiguous edges are not being merged because the part in purple is one-way whereas the pink and light blue parts are two-way streets. Relevant OpenStreetMap way IDs are 46678071, 110711994 and 237298378. However, this is not what I am looking for; I would like these three edges to be merged, regardless of the fact that one of them is one-way.
ox.settings.log_console = True
G = ox.graph_from_xml("osm_network_agglomeration_montreal.xml",
simplify=False,
retain_all=True,
bidirectional=False)
# Only retain graph that is inside a certain zone
G = ox.truncate.truncate_graph_polygon(G, boundary_polygon)
# Filter edges based on highway type
allowed_highway_types = ["primary",
"secondary",
"tertiary"]
edges_subset = []
for u, v, data in G.edges(data=True):
if data['highway'] in allowed_highway_types:
edges_subset.append((u, v, 0))
G_subset = G.edge_subgraph(edges_subset)
#G_subset = ox.get_undirected(G_subset) # Can't do this as simplify_graph only works with directed graphs.
# Simplify the graph: get rid of interstitial nodes
G_truncated = ox.simplify_graph(G_subset, strict=True, remove_rings=False)
# Convert to an undirected graph. We don't want parallel edges unless their geometries differ.
G_truncated = ox.get_undirected(G_truncated)
gdf_nodes, gdf_links = ox.graph_to_gdfs(G_truncated)
# Get rid of the variables we won't need anymore
#del G, edges_subset, G_subset
So, my question is: is there a way to simplify an undirected graph? I am fine with modifying OSMNX's code and submitting a pull request if that's what's required here.
Thanks!
One possibility is to convert the graph to undirected and then back to directed, so that all edges become two-way:
from networkx import Graph, DiGraph, path_graph
G = path_graph(4, create_using=DiGraph)
print(G.is_directed())
# True
print(G.edges)
# [(0, 1), (1, 2), (2, 3)]
G = DiGraph(Graph(G))
print(G.is_directed())
# True
print(G.edges)
# [(0, 1), (1, 0), (1, 2), (2, 1), (2, 3), (3, 2)]
Related
I am solving a specific problem, I would appreciate any suggestions you had for this problem, its trivial but I am not sure if this is the best way to solve this problem.
The Problem: Given a graph G, lets say you get a set of connected component subgraphs {C_1, C_2, ....., C_N}. Now partition the aforementioned set into K subsets such that every element in the subset are isomorphic with each-other. You can have atmost N subsets.
The Solution:
Run a find connected sugraphs. This will return all the connected components as graphs.
Partition this connected components subgraphs based on number of nodes (Which should be fine in our case). Optional
For the subgraphs. Run the following:
Initialize a dictionary with the following. A random node as the key and a singleton set containing the graph itself as its value and add remaining nodes to a deque.
For the remaining nodes:
pop the element in the front of the deque. If there's an isomorphism match add that graph to the dictionary of subgraph, else push the element to its back.
Once there's absolute certainity that we have reached a cycle. I.e. we are scanning the same element which we pushed to the back of the queue. This node's cycle is done. Pop the front of the queue and add the <graph:set([graph])> entry to the dictionary mentioned before. Repeat until there are elements left in the queue.
Can this be further otpimized?
import networkx as nx
nx_graph = nx.Graph()
edges = [
(1, 2),
(2, 3),
(3, 4),
(5, 6),
(6, 7),
(7, 8),
(9, 10),
(10, 11),
(10, 12)
]
for edge in edges:
nx_graph.add_edge(edge[0], edge[1])
nx.draw(nx_graph, pos=nx.spring_layout(nx_graph), node_color='#1ab2c3', with_labels=True)
Graph:
from collections import deque
def paritition_isomorphic_subgraphs(graph):
subgraphs_gen = (graph.subgraph(c) for c in nx.connected_components(graph))
subgraphs_list = list(subgraphs_gen)
graph_queue = deque(subgraphs_list)
graph_of_interest = graph_queue.popleft()
isomprohic_elements = {
graph_of_interest: set([graph_of_interest])
}
last_element_popped = None
count = 0
first_mismatch = None
while graph_queue:
if graph_queue[0] == first_mismatch:
count = 0
graph_of_interest = graph_queue.popleft()
isomprohic_elements[graph_of_interest] = set([graph_of_interest])
if graph_queue:
graph = graph_queue.popleft()
if nx.is_isomorphic(graph_of_interest, graph):
isomprohic_elements[graph_of_interest].add(graph)
else:
if count == 0:
first_mismatch = graph
graph_queue.append(graph)
count += 1
return list(isomprohic_elements.values())
I have a networkx DiGraph and I want to extract the subgraph that contains a certain number of nodes.
For example, the Digraph is 0-1-2-3-4-5. I want to obtain all the subgraphs that contains 3 nodes. The result should be: 0-1-2, 1-2-3, 2-3-4, 3-4-5.
How can I do that?
I'm not completely sure if I understand correctly: Your example implies that you only want connected subgraphs? In a directed graph there's more than one kind of connectivity (weak and strong). So you have to decide which one you're looking for.
This might work:
import networkx as nx
from itertools import combinations
# The graph in your example (as I understand it)
G = nx.DiGraph((i, i+1) for i in range(5))
num_of_nodes = 3 # Number of nodes in the subgraphs (here 3, as in your example)
subgraphs = [] # List for collecting the required subgraphs
for nodes in combinations(G.nodes, num_of_nodes):
G_sub = G.subgraph(nodes) # Create subgraph induced by nodes
# Check for weak connectivity
if nx.is_weakly_connected(G_sub):
subgraphs.append(G_sub)
combinations(G.nodes, num_of_nodes) iterates over all unique combinations of num_of_nodes many nodes from G.
The selected subgraphs are exactly the ones you mentioned:
print([H.nodes for H in subgraphs])
print([H.edges for H in subgraphs])
shows
[NodeView((0, 1, 2)), NodeView((1, 2, 3)), NodeView((2, 3, 4)), NodeView((3, 4, 5))]
[OutEdgeView([(0, 1), (1, 2)]), OutEdgeView([(1, 2), (2, 3)]), OutEdgeView([(2, 3), (3, 4)]), OutEdgeView([(3, 4), (4, 5)])]
If your graph is
G = nx.DiGraph([(i, i+1) for i in range(5)] + [(i+1, i) for i in range(5)])
and you're looking for strong connectivity then you have to use
...
# Check for strong connectivity
if nx.is_strongly_connected(G_sub):
...
(The usual warning: G.subgraph() only gives you a view.)
So. I'm trying to find paths through a directed graph.
I start out with a Pandas data frame with 3 columns: 'Source', 'Target', 'weight.' The weight is used to keep track of the action of each target (it's just a number).
Then I convert the df into an edge list:
edge_list = nx.from_pandas_edgelist(df, 'Source','Target','weight')
Then for good measure, I convert this into a directed graph (and I'm fairly confident this is happening correctly)
directed_graph = nx.DiGraph(edge_list)
However, whenever I start to search for paths within the graphs, I'm getting undirected paths. I checked and double-checked but if I define a path
path = nx.shortest_path(directed_graph,source=A,target=B,weight='weight')
the path that's returned can't be found by following the directed paths found in the graph. (It can, however be found by following undirected paths, so I guess that's something)
You can direclty create your digraph by specifying create_using=nx.DiGraph()
g = nx.from_pandas_edgelist(df, 'Source','Target','weight', create_using=nx.DiGraph())
nx.from_pandas_edgelist returns an instance of a graph, not an edge list, so you are first creating a graph and then converting it to a digraph by adding two directed arcs (i,j), (j,i) for each undirected edge (i,j).
Example:
>>> g=nx.from_edgelist([(1,2),(3,4)])
>>> g.edges()
EdgeView([(1, 2), (3, 4)])
>>> nx.DiGraph(g).edges()
OutEdgeView([(1, 2), (2, 1), (3, 4), (4, 3)])
This is a follow up to my previous question.
The question is on the order in which the nodes are saved in the edge list while creating an undirected graph. I have a graph created using the below code. A simple graph is created and new nodes and edges are added between two nodes that already exist.
import networkx as nx
import matplotlib.pyplot as plt
from pprint import pprint
G = nx.OrderedGraph()
head_nodes = range(0, 9)
tail_nodes = range(1, 10)
edge_ls = list(zip(head_nodes, tail_nodes))
G.add_nodes_from(range(0, 10))
G.add_edges_from(edge_ls)
head = 0
tail = 1
G.remove_edge(head, tail)
Nnodes = G.number_of_nodes()
newnodes = [head, Nnodes+1, Nnodes+2, Nnodes+3, tail] # head and tail already exists
newedges = [(x, y) for x, y in zip(newnodes[0:len(newnodes)-1], newnodes[1:len(newnodes)])]
G.add_edges_from(newedges)
I = nx.incidence_matrix(G)
pprint(I)
pprint(G.edges())
nx.draw(G, with_labels=True)
plt.show()
The output of using an undirected graph is:
EdgeView([(0, 11), (1, 2), (1, 13), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8), (8, 9), (11, 12), (12, 13)])
From the output, we can observe that the edge that was created using G.add_edge(13,1) is displayed as (1,13). I understand this happens because the graph is undirected.
When a directed graph (G.OrderedDiGraph)is used, the output is:
EdgeView([(0, 11), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8), (8, 9), (11, 12), (12, 13), (13, 1)])
(13, 1) is as I expected the result to be.
However, I am interested to know if there is a particular way in which the nodes can be named so that one can avoid Networkx from reordering the nodes from (u,v) , which is the user input, to (v,u) in an undirected graph.
EDIT: I am avoiding the use of diGraph because there are inputs like this for which diGraph yields the following result
In this directed graph, nodes 24 and 28 have a sum of indegree and outdegree to be 1. However, I want the arrows to be directed from node 24 to 28, similar to unidirectional flow in a traffic network where there could be different routes for the traffic to flow from point 24 to point 28. The directions created by diGraph of Networkx doesn't represent my real system. Therefore, I don't want to use diGraph.
No, this is not possible with networkx. Even if you try to use an OrderedGraph, the best you will get is
a consistent order not a particular order.
When you choose to use a Graph instead of a DiGraph, you are explicitly stating that it is undirected, so direction does not matter, and it would be inefficient for networkx to save information about the direction. In the underlying data structure, it doesn't save an edge list. Rather it saves a dict which says what the neighbors of each node are (so no information is stored about the order the user entered it in). When it reconstructs an EdgeView, it uses this dict, so all information about how the user entered it is already lost.
If you need to do this, I would recommend using a DiGraph. If you need it to be some hybrid of directed (tracking the order of an edge) while still undirected (treating each edge as having both directions), I would recommend storing the edge directions separately from the networkx graph.
As I said previously and Joel has said in his answer, you need to store the edges in your own data structure that suits your need instead of relying on the graph. Pick a data structure and define some function add_edge which adds to both that data structure and your graph, and only use that function to add edges. If you simply want to remember all the edges you ever had, this will do:
edges = set()
def add_edge(u, v):
G.add_edge(u, v)
edges.add((u, v))
If you want to retrieve some subset of the original edges based on a change to the graph (e.g. you want to see which edges remain after removing nodes) then use a dictionary which maps both orderings of each edge to the original:
edges = {}
def add_edge(u, v):
G.add_edge(u, v)
edges[(u, v)] = (u, v)
edges[(v, u)] = (u, v)
Then after you change the graph you can get the original edges like so:
{edges[edge] for edge in G.edges()}
Suppose I have edges between [(1,2),(2,1),(1,3)], how can I remove (1,3) since it is not bidrectional like the edge between 1 and 2?
I'm assuming this is a DiGraph. In that case, first find the edges you want to remove. Then remove them.
to_remove = [(v,u) for v,u in G.edges() if not G.has_edge(u,v)]
G.remove_edges_from(to_remove)
The list to_remove has all those edges in G for which G does not have the opposite edge (it is a list comprehension).
This one is a little more lengthy, but don't modify the edges directly
import networkx as nx
# create the graph
G = nx.DiGraph()
G.add_edges_from([(1,2),(2,1),(1,3),(4,1),(1,5),(1,6),(5,1)])
H = nx.difference(G.to_undirected().to_directed(), G) # get the uni-directional edges
G = nx.difference(G, H.to_undirected()) # get the difference graph
G.edges()
# [(1, 2), (1, 5), (2, 1), (5, 1)]