Get networkx subgraph containing all nodes in between - python

I have a networkx DiGraph and I want to extract a subgraph from it by passing in a list of nodes. The subgraph however can contain all nodes that might be in between the nodes that I have passed. I checked nx.subgraph() but it does not work like I intend to. As for a small example:
import networkx as nx
G = nx.DiGraph()
edges = [(7, 4), (3, 8), (3, 2), (3, 0), (3, 1), (7, 5), (7, 6), (7, 8)]
G.add_edges_from(edges)
H = get_subgraph(G, [0,6,7,8])
How can I write the function get_subgraph() so that H has the edges [(3, 8), (3, 0), (7, 6), (7, 8)]?
The subgraph I need is such that it contains all the nodes that are in the ougoing and incoming paths between the nodes that I pass in the get_subgraph()function.

A way to do this could be to find the longest path length between the specified set of nodes, and then find the corresponding induced subgraph containing all nodes in the path. However, being a directed graph, there will be no direct path between say nodes 3 and 7. So we need to find the paths in an undirected copy of the graph. Let's set up the problem:
G = nx.DiGraph()
edges = [(7, 4), (3, 8), (3, 2), (3, 0), (3, 1), (7, 5), (7, 6), (7, 8)]
G.add_edges_from(edges)
plt.figure(figsize=(10,6))
pos = nx.spring_layout(G, scale=20, k=3/np.sqrt(G.order()))
nx.draw(G, pos, node_color='lightblue',
with_labels=True,
node_size=1500,
arrowsize=20)
Now we ca obtain and undirected copy of the graph with nx.to_undirected and find all nx.shortest_path_length for the specified nodes:
from itertools import combinations
H = nx.to_undirected(G)
nodelist = [0,6,7,8]
paths = {}
for nodes in combinations(nodelist, r=2):
paths[nodes] = nx.shortest_path_length(H, *nodes)
print(paths)
# {(0, 6): 4, (0, 7): 3, (0, 8): 2, (6, 7): 1, (6, 8): 2, (7, 8): 1}
We can find the longest path in the undirected graph with:
max_path = max(paths.items(), key=lambda x: x[1])[0]
longest_induced_path = nx.shortest_path(H, *max_path)
And the corresponding induced subgraph can be obtained with Graph.subgraph:
sG = nx.subgraph(G, longest_induced_path)
pos = nx.spring_layout(sG, scale=20, k=3/np.sqrt(G.order()))
nx.draw(sG, pos, node_color='lightblue',
with_labels=True,
node_size=1500,
arrowsize=20)

i understand this from question:
you need all nodes in a path but provide some nodes of that path and algorithm should give all nodes of that path and then you can pass that nodes to a graph and make a new graph.
it should be what you want:
1. you must iterate over all pairs of nodes with this method:
from itertools import combinations
b= combinations('ABCD', 2)
print(list(b)) --> [('A', 'B'), ('A', 'C'), ('A', 'D'), ('B', 'C'), ('B', 'D'), ('C', 'D')]
you must get all pathes with this:
https://networkx.github.io/documentation/stable/reference/algorithms/simple_paths.html
you must select path with maximum nodes and that is your solution.

Related

Networkx dfs_edges not functioning as intended by professor

The expected output from dfs_edges(G, 4) is correct, but it's off by the position of one edges and I can't figure out why
Input:
G = nx.Graph()
edges = [(1,2),(1,3),(1,5),(2,5),(2,6),(3,4),(3,5),(4,5),(4,6),(4,7),(5,6)]
root = 4
G.add_edges_from(edges)
printGraph(G)
Expected Output:
DFS traversal from from 4 = [(4, 3), (4, 7), (3, 1), (1, 2), (2, 5), (5, 6)]
My Code:
import networkx as nx
def printGraph(G):
print("DFS traversal from from 4 =", list(nx.dfs_edges(G,4)))
Result from code:
DFS traversal from from 4 = [(4, 3), (3, 1), (1, 2), (2, 5), (5, 6), (4, 7)]
The problem is the position of the (4, 7) edge, while I can manually place it where it should be, but that would mean it wouldn't work on the hidden text cases (Moodle)
This is your graph:
In a depth-first search starting at node 4, I would expect node 7 be be visited either first or last, not right after node 3. The output you're getting is valid for dfs_edges. The expected output is what's incorrect.
The answer got and expected are both correct, turns out he didn't want dfs_edges, he wanted the edges of the dfs tree instead, so this worked:
print("DFS traversal from from 4 =", nx.dfs_tree(G, root)).edges())

How to iterate through edges in a graph if they are not in a pre-selected list

I am filtering a subset of edges so I can iterate through them. In this case, I am excluding the "end edges", which are the final edges along a chain:
import networkx as nx
graph = nx.Graph()
graph.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 4)])
end_nodes = [n for n in graph.nodes if nx.degree(graph, n) == 1]
end_edges = graph.edges(end_nodes)
print(f"end edges: {end_edges}")
for edge in graph.edges:
if edge not in end_edges:
print(f"edge {edge} is not an end edge.")
else:
print(f"edge {edge} is an end edge.")
However, when you run this code, you get the following output:
end edges: [(0, 1), (4, 3)]
edge (0, 1) is an end edge.
edge (1, 2) is an end edge.
edge (2, 3) is an end edge.
edge (3, 4) is an end edge.
Edges (1, 2) and (2, 3) are not in end_edges, yet it returns False when the conditional edge not in end_edges is checked (seeming to imply that it is in fact included, when it seems to not be).
What is going on, and how can I filter this properly?
Python version is 3.7, NetworkX is 2.4.
You can convert end_nodes to a set of edges and keep the edges unordered.
>>> graph = nx.Graph()
>>> graph.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 4)])
>>> end_nodes = [n for n in graph.nodes if nx.degree(graph, n) == 1]
>>> end_edges = set(map(frozenset, graph.edges(end_nodes)))
>>> end_edges
{frozenset({3, 4}), frozenset({0, 1})}
>>> for edge in graph.edges:
... print(edge, frozenset(edge) in end_edges)
...
(0, 1) True
(1, 2) False
(2, 3) False
(3, 4) True
import networkx as nx
graph = nx.Graph()
graph.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 4)])
end_nodes = [n for n in graph.nodes if nx.degree(graph, n) == 1]
end_edges = graph.edges(end_nodes)
print(f"end edges: {end_edges}")
for edge in graph.edges:
if edge not in list(end_edges):
print(f"edge {edge} is not an end edge.")
else:
print(f"edge {edge} is an end edge.")
This should return what you ask for.

How to merge values from dictionary on different keys while iterating through it ? Finite element mesh merge algorithm

I am working on my phd and I am stuck on this step. The problem consists of implementing a finite element mesh merging algorithm and maybe my solution is not the best, so if you think of a better one I am open to suggestions.
Regarding the problem: I have a finite element mesh, which is composed of QUAD elements (squares with 4 nodes) and TRIA elements (triangles with 3 nodes). These elements are connected on edges, an edge is defined by 2 nodes (edge=[node1,node2]). I have a list of edges that I do not want to merge, but for the rest of the edges I want the program to merge the elements with the common edge.
As a simple example: assume I have 4 elements A,B,C and D (QUAD elms, defined by 4 nodes). The mesh looks something like this
1--------------2----------------3
| | |
| A | B |
| | |
4--------------5----------------6
| | |
| C | D |
| | |
7--------------8----------------9
These elements are defined in a dictionary:
mesh_dict={'A': [1,2,5,4], 'B':[2,3,6,5], 'C':[4,5,8,7],'D':[5,6,9,8]}
I also have a dictionary for the node position with values for X,Y,Z coordinates. Let's say I want to merge on edge [4,5] and [5,6].
My solution is the following: I start iterating through the elements in mesh_dict, I find the neighbors of the element with a function get_elm_neighbors(element), I check the angle between elements with function check_angle(elm1,elm2,angle) (I need the angle between elements to be below a certain threshold), than I check for which edge should be merged by get_edge_not_bar(), than I have a function which updates the nodes for the first element to complete the merging.
for e in mesh_dict:
if e not in delete_keys:
neighbors=get_elm_neighbors(e)
for key,value in neighbors.items():
check = check_angle(e,key,0.5)
if check:
nodes = get_edge_not_bar(value)
if nodes:
new_values=merge_elms(e,key,nodes)
d = {e: new_values}
mesh_dict_merged.update(d)
mesh_dict.update(d)
delete_keys.append(key)
My problem is that I need to delete the elements that remain after the merging. For example in the above case I start on element A and I merge on the edge [4,5], after that the elm A definition will be 'A':[1,2,8,7], then I need to delete elm C and proceed with the iteration.
My solution was to create a duplicate dictionary mesh_dict_merge in which I update the values for the elements and then delete the ones that I don't want to while iterating through the original dict but taking into consideration the deleted elements (deleted_keys list) to not go through them
I guess my question is if there is a way to iterate through the dictionary, update values and delete keys while doing so ? Or if there is a better solution to approach this problem, maybe iterate through nodes instead of elements ?
EDIT: changed 'A': [1,2,4,5] to 'A': [1,2,5,4]
It can be done updating the elements on-the-fly. But I should not recommend it because your algorithm will depend on the order you iterate the elements, and may be not deterministic. This mean that two meshes with identical geometry and topology could give different results depending on the labels you use.
The recommendation is :
Compute all dihedral angles in your mesh. Store those that are under your merge threshold.
Find the minimum angle and merge the two elements that share that edge.
Update the dihedral angles around the new element. This include removing angles from elements that have merged, and optionally include new angles for the new element.
Repeat from step 2 until every angle is over the threshold, or until the number of elements is the desired.
The optional part in step 3 allows to determine the aggressiveness of your method. Sometimes it is better not to include new angles and repeat several times the complete process to avoid focus the reduction too much in a zone.
I thought about how to find adjacent elements by finding elements that shared the same edge - but I had to have edges as a pair of end indices in sorted order.
I could then work out touches (should work for triangle elements too).
I introduce dont_merge as a set of ordered edge indices that cannot be merged away then merge into merged_ordered_edges and finally convert back to the mesh format of your original with edges going around each element.
I have commented out a call to check_angle(name1, name2) which you would have to add in. I assume that the check would succeed every time by the comment.
# -*- coding: utf-8 -*-
"""
Finite element mesh merge algorithm
https://stackoverflow.com/questions/59079755/how-to-merge-values-from-dictionary-on-different-keys-while-iterating-through-it
Created on Thu Nov 28 21:59:07 2019
#author: Paddy3118
"""
#%%
mesh_dict={'A': [1,2,5,4], 'B':[2,3,6,5], 'C':[4,5,8,7],'D':[5,6,9,8]}
#
ordered_edges = {k: {tuple(sorted(endpoints))
for endpoints in zip(v, v[1:] + v[:1])}
for k, v in mesh_dict.items()}
# = {'A': {(1, 2), (1, 4), (2, 5), (4, 5)},
# 'B': {(2, 3), (2, 5), (3, 6), (5, 6)},
# 'C': {(4, 5), (4, 7), (5, 8), (7, 8)},
# 'D': {(5, 6), (5, 8), (6, 9), (8, 9)}}
#%%
from collections import defaultdict
touching = defaultdict(list)
for name, edges in ordered_edges.items():
for edge in edges:
touching[edge].append(name)
touches = {edge: names
for edge, names in touching.items()
if len(names) > 1}
# = {(2, 5): ['A', 'B'],
# (4, 5): ['A', 'C'],
# (5, 6): ['B', 'D'],
# (5, 8): ['C', 'D']}
#%%
dont_merge = set([(4, 5), (23, 24)])
for edge, (name1, name2) in touches.items():
if (edge not in dont_merge
and ordered_edges[name1] and ordered_edges[name2]
#and check_angle(name1, name2)
):
# merge
ordered_edges[name1].update(ordered_edges[name2])
ordered_edges[name1].discard(edge) # that edge is merged away
ordered_edges[name2] = set() # gone
merged_ordered_edges = {}
for name, edges in ordered_edges.items():
if edges:
merged_ordered_edges[name] = sorted(edges)
edges.clear() # Only one name of shared object used
# = {'A': [(1, 2), (1, 4), (2, 3), (3, 6), (4, 5), (5, 6)],
# 'C': [(4, 5), (4, 7), (5, 6), (6, 9), (7, 8), (8, 9)]}
## You would then need a routine to change the ordered edges format
## back to your initial mesh_dict format that goes around the periphery
## (Or would you)?
#%%
def ordered_to_periphery(edges):
"""
In [124]: ordered_to_periphery([(1, 2), (1, 4), (2, 3), (3, 6), (4, 5), (5, 8), (6, 9), (8, 9)])
Out[124]: [(1, 2), (2, 3), (3, 6), (6, 9), (9, 8), (8, 5), (5, 4), (4, 1)]
"""
p = [edges.pop(0)] if edges else []
last = p[-1][-1] if p else None
while edges:
for n, (i, j) in enumerate(edges):
if i == last:
p.append((i, j))
last = j
edges.pop(n)
break
elif j == last:
p.append((j, i))
last = i
edges.pop(n)
break
return p
#%%
merged_mesh = {name: ordered_to_periphery(edges)
for name, edges in merged_ordered_edges.items()}
# = {'A': [(1, 2), (2, 3), (3, 6), (6, 5), (5, 4), (4, 1)],
# 'C': [(4, 5), (5, 6), (6, 9), (9, 8), (8, 7), (7, 4)]}
P.S. Any chance of a mention if you use this?

Minimum weight BFS graph span of undirected graph

This is probably a beginner question at best, but have been playing with graphs and have been implementing BFS searches on various exercises. I can't quite figure out how to actually keep track on the weight of the edges I have visited in order to create a minimum complete spanning of the graph. My graph is in the format:
{0: [(1, 1), (2, 1)], 1: [(0, 1), (2, 1)], 2: [(1, 1), (0, 1)]}
Where the first vertice is 0 with adjacent vertices of 1 and 2 with weights of 1 and 1 respectively. So in clearer terms the keys in the graph dictionary represent vertices, and each tuple in the key value represent a vertice, weight pair.
So what I have in my BFS function is:
def bfs(graph, start):
"""returns total weight needed to visit
each vertice in the graph with the minimum
overall weight possible"""
if [] in graph.values():
return "Not Possible"
weight = 0
visited, queue = set(), [start]
while queue:
vertex = queue.pop(0)
if vertex not in visited:
visited.add(vertex)
for node in graph[vertex]:
queue.append(node[0])
weight += node[1]
return weight
At the moment with my original graph this function would return 6 where it should be 2. I think this is because it is iterating over each vertice and adding the adjacent weights, even though they have already been visited.
This also wouldn't actually choose the minimum weighted path, it only keep track of the weight of the path it has taken, whatever that may be. How can I address this?
A longer example:
{0: [(1, 5), (2, 7), (3, 12)], 1: [(0, 5), (2, 9), (4, 7)], 2: [(0, 7), (1, 9), (3, 4), (4, 4), (5, 3)], 3: [(0, 12), (2, 4), (5, 7)], 4: [(1, 7), (2, 4), (5, 2), (6, 5)], 5: [(2, 3), (3, 7), (4, 2), (6, 2)], 6: [(4, 5), (5, 2)]}
This produces a weight of 134 where the correct answer should be 23
Is there some algorithm I am missing that can keep track of the weighted edges and choose the best path from this?
I am aware of Dijkstra’s Algorithm but as far as I am aware that is suitable for a path with a designated start and end, and not a complete graph span?
Dijkastra's algorithm and bfs are useful in finding minimum path between two vertices.However if you want to find the minimum spanning tree please check out Kruskal's algorithm instead.
Here is the link:
https://en.wikipedia.org/wiki/Kruskal%27s_algorithm
Pseudocode:
KRUSKAL(G):
1 A = ∅
2 foreach v ∈ G.V:
3 MAKE-SET(v)
4 foreach (u, v) in G.E ordered by weight(u, v), increasing:
5 if FIND-SET(u) ≠ FIND-SET(v):
6 A = A ∪ {(u, v)}
7 UNION(u, v)
8 return A
It is implemented using union-find(disjointed set) data structure.

Find edges in a cycle networkx python

I would like to make an algorithm to find if an edge belongs to a cycle, in an undirected graph, using networkx in Python.
I am thinking to use cycle_basis and get all the cycles in the graph.
My problem is that cycle_basis returns a list of nodes. How can I convert them to edges?
You can construct the edges from the cycle by connecting adjacent nodes.
In [1]: import networkx as nx
In [2]: G = nx.Graph()
In [3]: G.add_cycle([1,2,3,4])
In [4]: G.add_cycle([10,20,30])
In [5]: basis = nx.cycle_basis(G)
In [6]: basis
Out[6]: [[2, 3, 4, 1], [20, 30, 10]]
In [7]: edges = [zip(nodes,(nodes[1:]+nodes[:1])) for nodes in basis]
In [8]: edges
Out[8]: [[(2, 3), (3, 4), (4, 1), (1, 2)], [(20, 30), (30, 10), (10, 20)]]
Here is my take at it, using just lambda functions (I love lambda functions!):
import networkx as nx
G = nx.Graph()
G.add_cycle([1,2,3,4])
G.add_cycle([10,20,30])
G.add_edge(1,10)
in_path = lambda e, path: (e[0], e[1]) in path or (e[1], e[0]) in path
cycle_to_path = lambda path: list(zip(path+path[:1], path[1:] + path[:1]))
in_a_cycle = lambda e, cycle: in_path(e, cycle_to_path(cycle))
in_any_cycle = lambda e, g: any(in_a_cycle(e, c) for c in nx.cycle_basis(g))
for edge in G.edges():
print(edge, 'in a cycle:', in_any_cycle(edge, G))
in case you don't find a nice solution, here's an ugly one.
with edges() you can get a list of edges that are adjacent to nodes in a cycle. unfortunately, this includes edges adjacent to nodes outside the cycle
you can now filter the list of edges by removing those which connect nodes that are not part of the cycle.
please keep us posted if you find a less wasteful solution.
With the help of Aric, and a little trick to check both directions, I finally did this that looks ok.
import networkx as nx
G = nx.Graph()
G.add_cycle([1,2,3,4])
G.add_cycle([10,20,30])
G.add_edge(1,10)
def edge_in_cycle(edge, graph):
u, v = edge
basis = nx.cycle_basis(graph)
edges = [zip(nodes,(nodes[1:]+nodes[:1])) for nodes in basis]
found = False
for cycle in edges:
if (u, v) in cycle or (v, u) in cycle:
found = True
return found
for edge in G.edges():
print edge, 'in a cycle:', edge_in_cycle(edge, G)
output:
(1, 2) in a cycle: True
(1, 4) in a cycle: True
(1, 10) in a cycle: False
(2, 3) in a cycle: True
(3, 4) in a cycle: True
(10, 20) in a cycle: True
(10, 30) in a cycle: True
(20, 30) in a cycle: True
You can directly obtain the edges in a cycle with the find_cycle method. If you want to test if an edge belongs to a cycle, you should check if both of its vertices are part of the same cycle.
Using the example in the answers above:
import networkx as nx
G = nx.Graph()
G.add_cycle([1,2,3,4])
G.add_cycle([10,20,30])
G.add_edge(1,10)
nx.find_cycle(G, 1) # [(1, 2), (2, 3), (3, 4), (4, 1)]
nx.find_cycle(G, 10) # [(10, 20), (20, 30), (30, 10)]
On the other hand, the edge (2, 3) (or (3, 2) as your graph is undirected) is part of a cycle defined first:
nx.find_cycle(G, 2) # [(2, 1), (1, 4), (4, 3), (3, 2)]

Categories