Commit 35910d34 authored by Felix's avatar Felix
Browse files

Merge branch 'dev' into main

parents 791b3796 4e68699d
# cycle-coalescence-algorithm
## Introduction
Hello everyone,
I wrote this package during my PhD, when working on the characterization of transport networks.
Have you ever wondered how cycles in graphs form a vector space and encapsulate nesting information? If so, were you never really sure how to deal with this? Here is a tool ready to use, enabling you to calculate the cycle bases, mapping them onto a merging tree, and analyze this tree's asymmetry.
......@@ -12,7 +9,7 @@ This project is based on the algorithm published in 'Extracting Hidden Hierarchi
./notebook contains examples to play with in the form of jupyter notebooks
## Installation
python3 -m pip install --index-url https://test.pypi.org/simple/ --no-deps cycle_analysis
pip install cycle_analysis
## Usage
```python
......
......@@ -7,34 +7,35 @@
import networkx as nx
import numpy as np
import sys
import cycle_tools_simple
import cycle_analysis.cycle_tools_simple as cycle_tools_simple
class coalescence(cycle_tools_simple.simple,object):
def __init__(self):
super(coalescence,self).__init__()
self.cycle_tree=nx.Graph()
self.counter_c=0
def calc_cycle_asymmetry(input_graph):
minimum_basis=self.construct_minimum_basis(input_graph)
cycle_tree=self.calc_cycle_coalescence(input_graph,minimum_basis)
tree_asymmetry=self.calc_tree_asymmetry(cycle_tree)
minimum_basis=self.construct_networkx_basis(input_graph)
self.calc_cycle_coalescence(input_graph,minimum_basis)
tree_asymmetry=self.calc_tree_asymmetry()
return tree_asymmetry
def calc_cycle_coalescence(self,input_graph,cycle_basis):
self.G=nx.Graph(input_graph)
#create cycle_map_tree with cycles' edges as tree nodes
# print([len(cb.edges()) for cb in cycle_basis])
cycle_tree=nx.Graph()
for cycle in cycle_basis:
cycle_tree.add_node(tuple(cycle.edges(keys=True)),label='base',weight=1.,branch_type='none',pos=(-1,-1))
self.cycle_tree.add_node(tuple(cycle.edges()),label='base',weight=1.,branch_type='none',pos=(-1,-1))
# get the weights of the input graph and sort
edges=nx.get_edge_attributes(input_graph,'weight')
edges=nx.get_edge_attributes(self.G,'weight')
sorted_edges=sorted(edges,key=edges.__getitem__)
counter_c=0
# merge the cycles which share an edge
for e in sorted_edges:
......@@ -42,6 +43,7 @@ class coalescence(cycle_tools_simple.simple,object):
#check whether all cycles are merged
if len(cycle_basis)== 1:
break
cycles_with_edge={}
for i,cycle in enumerate(cycle_basis):
......@@ -54,67 +56,65 @@ class coalescence(cycle_tools_simple.simple,object):
cycle_1=cycle_basis[idx_list[0]]
cycle_2=cycle_basis[idx_list[1]]
cycles_edge_sets=[cycle_1.edges(keys=True),cycle_2.edges(keys=True)]
merged_cycle=self.merge_cycles(input_graph,cycle_1,cycle_2)
# build merging tree
if cycle_tree.nodes[tuple(cycles_edge_sets[0])]['label']=='base':
cycle_tree.nodes[tuple(cycles_edge_sets[0])]['pos']=(counter_c,0)
counter_c+=1
if cycle_tree.nodes[tuple(cycles_edge_sets[1])]['label']=='base':
cycle_tree.nodes[tuple(cycles_edge_sets[1])]['pos']=(counter_c,0)
counter_c+=1
merged_cycle=self.merge_cycles(cycle_1,cycle_2)
cycle_basis.remove(cycle_1)
cycle_basis.remove(cycle_2)
cycle_basis.append(merged_cycle)
# build up the merging tree, set leave weights to nodes, set asymetry value to binary branchings
cycle_keys=[tuple(cycles_edge_sets[0]),tuple(cycles_edge_sets[1]),tuple(merged_cycle.edges(keys=True))]
self.build_cycle_tree(cycle_tree,cycle_keys)
for n in cycle_tree.nodes():
if cycle_tree.nodes[n]['pos'][0]==-1:
cycle_tree.nodes[n]['pos']=(counter_c,0)
counter_c+=1
self.build_cycle_tree(cycle_1,cycle_2,merged_cycle)
for n in self.cycle_tree.nodes():
if self.cycle_tree.nodes[n]['pos'][0]==-1:
self.cycle_tree.nodes[n]['pos']=(self.counter_c,0)
self.counter_c+=1
else:
continue
return self.cycle_tree
return cycle_tree
def calc_tree_asymmetry(self,cycle_tree):
def calc_tree_asymmetry(self):
dict_asymmetry={}
for n in cycle_tree.nodes():
for n in self.cycle_tree.nodes():
if cycle_tree.nodes[n]['branch_type']=='vanpelt_2':
dict_asymmetry[n]=(cycle_tree.nodes[n]['asymmetry'])
if self.cycle_tree.nodes[n]['branch_type']=='vanpelt_2':
dict_asymmetry[n]=(self.cycle_tree.nodes[n]['asymmetry'])
return dict_asymmetry
def build_cycle_tree(self,cycle_tree,cycle_keys):
def build_cycle_tree(self,*args):
cycle_1,cycle_2,merged_cycle=args
cycles_edge_sets=[cycle_1.edges(),cycle_2.edges()]
cycle_keys=[tuple(cycles_edge_sets[0]),tuple(cycles_edge_sets[1]),tuple(merged_cycle.edges())]
c_weight=np.zeros(2)
# build merging tree
for i in range(2):
c_weight[i]=self.cycle_tree.nodes[cycle_keys[i]]['weight']
if self.cycle_tree.nodes[tuple(cycles_edge_sets[i])]['label']=='base':
self.cycle_tree.nodes[tuple(cycles_edge_sets[i])]['pos']=(self.counter_c,0)
self.counter_c+=1
c_x=(cycle_tree.nodes[cycle_keys[0]]['pos'][0]+cycle_tree.nodes[cycle_keys[1]]['pos'][0])/2.
c_y=np.amax([cycle_tree.nodes[cycle_keys[0]]['pos'][1],cycle_tree.nodes[cycle_keys[1]]['pos'][1]]) + 2.
c1_weight=cycle_tree.nodes[cycle_keys[0]]['weight']
c2_weight=cycle_tree.nodes[cycle_keys[1]]['weight']
c_x=(self.cycle_tree.nodes[cycle_keys[0]]['pos'][0]+self.cycle_tree.nodes[cycle_keys[1]]['pos'][0])/2.
c_y=np.amax([self.cycle_tree.nodes[cycle_keys[0]]['pos'][1],self.cycle_tree.nodes[cycle_keys[1]]['pos'][1]]) + 2.
cycle_tree.add_node(cycle_keys[2],pos=(c_x,c_y),label='merged',weight=c1_weight+c2_weight)
cycle_tree.add_edge(cycle_keys[0],cycle_keys[2])
cycle_tree.add_edge(cycle_keys[1],cycle_keys[2])
self.cycle_tree.add_node(cycle_keys[2],pos=(c_x,c_y),label='merged',weight=c_weight[0]+c_weight[1])
for i in range(2):
self.cycle_tree.add_edge(cycle_keys[i],cycle_keys[2])
# criterium for avoiding redundant branchings
if c_y>=6:
cycle_tree.nodes[cycle_keys[2]]['branch_type']='vanpelt_2'
cycle_tree.nodes[cycle_keys[2]]['asymmetry']=np.absolute((c1_weight-c2_weight))/(c1_weight+c2_weight-2.)
self.cycle_tree.nodes[cycle_keys[2]]['branch_type']='vanpelt_2'
self.cycle_tree.nodes[cycle_keys[2]]['asymmetry']=np.absolute((c_weight[0]-c_weight[1]))/(c_weight[0]+c_weight[1]-2.)
else:
cycle_tree.nodes[cycle_keys[2]]['branch_type']='none'
self.cycle_tree.nodes[cycle_keys[2]]['branch_type']='none'
def merge_cycles(self,input_graph,cycle_1,cycle_2):
def merge_cycles(self,cycle_1,cycle_2):
cycles_edge_sets=[cycle_1.edges(),cycle_2.edges()]
merged_cycle=nx.MultiGraph()
merged_cycle=nx.Graph()
merged_cycle.graph['cycle_weight']=0
for i in range(2):
for e in cycles_edge_sets[i]:
......@@ -124,7 +124,7 @@ class coalescence(cycle_tools_simple.simple,object):
merged_cycle.add_edge(*e)
for e in merged_cycle.edges():
merged_cycle.graph['cycle_weight']+=input_graph.edges[e]['weight']
merged_cycle.graph['cycle_weight']+=self.G.edges[e]['weight']
list_merged=list(merged_cycle.nodes())
for n in list_merged:
......@@ -133,232 +133,38 @@ class coalescence(cycle_tools_simple.simple,object):
return merged_cycle
def generate_cycle_lists(self,input_graph):
total_cycle_dict={}
total_cycle_list=[]
# check for graph_type, then check for paralles in the Graph, if existent insert dummy nodes to resolve conflict, cast the network onto simple graph afterwards
counter=0
self.G=nx.Graph(input_graph)
for n in input_graph.nodes():
# building new tree using breadth first
spanning_tree,dict_path=self.breadth_first_tree(n)
diff_graph=nx.difference(input_graph,spanning_tree)
labels_e={}
for e in diff_graph.edges():
p_in=nx.shortest_path(spanning_tree,source=n,target=e[0])
p_out=nx.shortest_path(spanning_tree,source=n,target=e[1])
# label pathways
simple_cycle=nx.MultiGraph(cycle_weight=0.)
nx.add_path(simple_cycle,p_in)
nx.add_path(simple_cycle,p_out)
simple_cycle.add_edge(*e)
list_n=list(simple_cycle.nodes())
seen={}
for m in list(simple_cycle.edges()):
num_conncetions=simple_cycle.number_of_edges(*m)
if num_conncetions > 1 and m not in seen.keys():
seen[m]=1
elif num_conncetions > 1:
seen[m]+=1
for m in seen:
for i in range(seen[m]):
simple_cycle.remove_edge(m[0],m[1],i)
for q in list_n:
if simple_cycle.degree(q)==0:
simple_cycle.remove_node(q)
if nx.is_eulerian(simple_cycle):
# relabeling and weighting graph
for m in simple_cycle.edges():
simple_cycle.graph['cycle_weight']+=1.
total_cycle_list.append(simple_cycle)
total_cycle_dict.update({counter:nx.number_of_edges(simple_cycle)})
counter+=1
return total_cycle_dict,total_cycle_list
def construct_minimum_basis(self,input_graph):
# calc minimum weight basis and construct dictionary for weights of edges, takes a leave-less, connected, N > 1 SimpleGraph as input, no self-loops optimally, deviations are not raising any warnings
#sort basis vectors according to weight, creating a new minimum weight basis from the total_cycle_list
nullity=nx.number_of_edges(input_graph)-nx.number_of_nodes(input_graph)+nx.number_connected_components(input_graph)
total_cycle_dict,total_cycle_list=self.generate_cycle_lists(input_graph)
sorted_cycle_list=sorted(total_cycle_dict,key=total_cycle_dict.__getitem__)
minimum_basis=[]
minimum_label=[]
EC=nx.MultiGraph()
counter=0
total_cycle_list_sort=[total_cycle_list[i] for i in sorted_cycle_list]
for c in sorted_cycle_list:
cycle_edges_in_basis=True
new_cycle=total_cycle_list[c]
for e in new_cycle.edges(keys=True):
if not EC.has_edge(*e):
EC.add_edge(*e,label=counter)
counter+=1
cycle_edges_in_basis=False
#if cycle edges where not part of the supergraph yet then it becomes automatically part of the basis
# if not cycle_edges_in_basis:
# minimum_basis.append(total_cycle_list[c])
#
# #if cycle edges are already included we check for linear dependece
# else:
# linear_independent=False
# rows=len(list(EC.edges()))
# columns=len(minimum_basis)+1
# E=np.zeros((rows,columns))
# # translate the existent basis vectors into z2 representation
# for idx_c,cycle in enumerate(minimum_basis+[total_cycle_list[c]]):
# for m in cycle.edges(keys=True):
# if EC.has_edge(*m):
# E[EC.edges[m]['label'],idx_c]=1
#
# # calc echelon form
# a_columns=np.arange(columns-1)
# zwo=np.ones(columns)*2
# for column in a_columns:
# idx_nz=np.nonzero(E[column:,column])[0]
# if idx_nz.size:
# if len(idx_nz)==1:
# E[column,:],E[idx_nz[0]+column,:]=E[idx_nz[0]+column,:].copy(),E[column,:].copy()
# else:
# for r in idx_nz[1:]:
# aux_E=np.add(E[r+column],E[idx_nz[0]+column])
# E[r+column]=np.mod(aux_E,zwo)
# E[column,:],E[idx_nz[0]+column,:]=E[idx_nz[0]+column,:].copy(),E[column,:].copy()
# else:
# sys.exit('Error: minimum_weight_basis containing inconsistencies ...')
# # test echelon form for inconsistencies
# for r in range(rows):
# line_check=np.nonzero(E[r])[0]
# if len(line_check)==1 and line_check[0]==(columns-1):
# linear_independent=True
# break
# if linear_independent:
# minimum_basis.append(total_cycle_list[c])
if not cycle_edges_in_basis:
minimum_basis.append(new_cycle)
aux_label=[EC.edges[e]['label'] for e in new_cycle.edges(keys=True) ]
minimum_label.append(aux_label)
#if cycle edges are already included we check for linear dependece
else:
E=self.edge_matrix(EC, minimum_basis, minimum_label ,new_cycle)
linear_independent=self.compute_linear_independence(E)
# print(linear_independent)
if linear_independent:
minimum_basis.append(new_cycle)
aux_label=[EC.edges[e]['label'] for e in new_cycle ]
minimum_label.append(aux_label)
if len(minimum_basis)==nullity:
break
if len(minimum_basis)<nullity:
sys.exit('Error: Cycle basis badly constructed')
def compute_cycles_superlist(self,root):
return minimum_basis
spanning_tree,dict_path=self.breadth_first_tree(root)
diff_graph=nx.difference(self.G,spanning_tree)
list_cycles=[]
for e in diff_graph.edges():
def edge_matrix(self,*args):
simple_cycle,cycle_edges=self.find_cycle(dict_path,e,root)
list_cycles.append(cycle_edges)
EC, minimum_basis, minimum_label,new_cycle=args
rows=len(EC.edges())
columns=len(minimum_basis)+1
E=np.zeros((rows,columns))
return list_cycles
for idx_c,cycle in enumerate(minimum_basis):
E[minimum_label[idx_c],idx_c]=1
def construct_networkx_basis(self,input_graph):
for m in new_cycle.edges(keys=True):
E[EC.edges[m]['label'],-1]=1
C=self.construct_minimum_basis(input_graph)
return E
networkx_basis=[]
for cs in C:
new_cycle=nx.Graph()
new_cycle.graph['cycle_weight']=0.
for e in cs:
def compute_linear_independence(self,E):
new_cycle.add_edge(*e)
for k,v in self.G.edges[e].items():
new_cycle.edges[e][k]=v
new_cycle.graph['cycle_weight']+=1.
linear_independent=False
rows=len(E[:,0])
columns=len(E[0,:])
for n in new_cycle.nodes():
# calc echelon form
a_columns=np.arange(columns-1)
for column in a_columns:
idx_nz=np.nonzero(E[column:,column])[0]
idx=idx_nz[0]+column
for k,v in self.G.nodes[n].items():
new_cycle.nodes[n][k]=v
if len(idx_nz)==1:
E[[column,idx_nz[0]+column],:]=E[[idx_nz[0]+column,column],:]
else:
new_idx=idx_nz[1:]+column
aux_E=np.add(E[new_idx],E[idx])
E[new_idx]=np.mod(aux_E,2)
E[[column,idx_nz[0]+column],:]=E[[idx_nz[0]+column,column],:]
r=np.nonzero(E[columns-1:,-1])[0]
if r.size:
linear_independent=True
return linear_independent
def breadth_first_tree(self,root):
T=nx.Graph()
push_down=nx.get_node_attributes(self.G,'push')
len_n=len(self.G.nodes())
if len(push_down.keys())!=len_n:
push_down={}
for n in self.G.nodes():
push_down[n]=False
push_down[root]=True
root_queue=[]
labels=self.G.edges(root)
dict_path={}
dict_path[root]=[root]
T,dict_path,root_queue=self.compute_sprouts(root,T,labels ,push_down,dict_path,root_queue)
while T.number_of_nodes() < len_n:
new_queue=[]
for q in root_queue:
labels=self.G.edges(q)
T,dict_path,new_queue=self.compute_sprouts(q,T,labels,push_down,dict_path,new_queue)
root_queue=new_queue[:]
return T,dict_path
def compute_sprouts(self,*args):
root,T,labels,push_down,dict_path,root_queue=args
for e in labels:
if e[0]==root:
if not push_down[e[1]]:
T.add_edge(*e)
root_queue.append(e[1])
push_down[e[1]]=True
dict_path[e[1]]=dict_path[root]+[e[1]]
else:
if not push_down[e[0]]:
T.add_edge(*e)
root_queue.append(e[0])
push_down[e[0]]=True
dict_path[e[0]]=dict_path[root]+[e[0]]
networkx_basis.append(new_cycle)
return T,dict_path,root_queue
return networkx_basis
......@@ -5,12 +5,10 @@
# @Last modified by: kramer
# @Last modified time: 04-05-2021
import networkx as nx
import numpy as np
import sys
import cycle_tools
import cycle_analysis.cycle_tools as cycle_tools
class simple(cycle_tools.toolbox,object):
......@@ -21,8 +19,6 @@ class simple(cycle_tools.toolbox,object):
total_cycle_dict={}
total_cycle_list={}
self.G=nx.convert_node_labels_to_integers(self.G, first_label=0, ordering='default')
nx.set_node_attributes(self.G,False,'push')
# check for graph_type, then check for paralles in the Graph, if existent insert dummy nodes to resolve conflict, cast the network onto simple graph afterwards
......@@ -81,6 +77,28 @@ class simple(cycle_tools.toolbox,object):
return list_cycles
def construct_networkx_basis(self,input_graph):
C=self.construct_minimum_basis(input_graph)
networkx_basis=[]
for cs in C:
new_cycle=nx.Graph()
for e in cs:
new_cycle.add_edge(*e)
for k,v in self.G.edges[e].items():
new_cycle.edges[e][k]=v
for n in new_cycle.nodes():
for k,v in self.G.nodes[n].items():
new_cycle.nodes[n][k]=v
networkx_basis.append(new_cycle)
return networkx_basis
def construct_minimum_basis(self,input_graph):
# calc minimum weight basis and construct dictionary for weights of edges, takes a leave-less, connected, N > 1 SimpleGraph as input, no self-loops optimally, deviations are not raising any warnings
......@@ -94,7 +112,7 @@ class simple(cycle_tools.toolbox,object):
for c,e in total_cycle_dict.items():
total_cycle_len[c]=len(e)
sorted_cycle_list=sorted(total_cycle_len,key=total_cycle_len.__getitem__)
minimum_basis=[]
minimum_label=[]
EC=nx.Graph()
......@@ -120,7 +138,7 @@ class simple(cycle_tools.toolbox,object):
#if cycle edges are already included we check for linear dependece
else:
E=self.edge_matrix(EC, minimum_basis, minimum_label ,new_cycle)
E=self.edge_matrix(EC, len(minimum_basis), minimum_label ,new_cycle)
linear_independent=self.compute_linear_independence(E)
# print(linear_independent)
......@@ -137,37 +155,15 @@ class simple(cycle_tools.toolbox,object):
return minimum_basis
def construct_networkx_basis(self,input_graph):
C=self.construct_minimum_basis(input_graph)
networkx_basis=[]
for cs in C:
new_cycle=nx.Graph()
for e in cs:
new_cycle.add_edge(*e)
for k,v in self.G.edges[e].items():
new_cycle.edges[e][k]=v
for n in new_cycle.nodes():
for k,v in self.G.nodes[n].items():
new_cycle.nodes[n][k]=v
networkx_basis.append(new_cycle)
return networkx_basis
def edge_matrix(self,*args):
EC, minimum_basis, minimum_label,new_cycle=args
EC, length_basis, minimum_label,new_cycle=args
rows=len(EC.edges())
columns=len(minimum_basis)+1
columns=length_basis+1
E=np.zeros((rows,columns))
for idx_c,cycle in enumerate(minimum_basis):
E[minimum_label[idx_c],idx_c]=1
for i in range(length_basis):
E[minimum_label[i],i]=1
for m in new_cycle:
E[EC.edges[m]['label'],-1]=1
......
......@@ -8,7 +8,7 @@
import networkx as nx
import numpy as np
import random as rd
import cycle_analysis.cycle_coalescence as cc
import cycle_analysis.cycle_tools_simple as cc
# generate example pattern random/nested/gradient for testing baseline values of the order parameter
def generate_pattern(input_graph,mode):
......@@ -115,7 +115,8 @@ def generate_pattern(input_graph,mode):
new_input_graph=nx.compose(new_input_graph,tile)
input_graph=nx.Graph(new_input_graph)
basis=cc.construct_minimum_basis(new_input_graph)
T=cc.simple()
basis=T.construct_networkx_basis(new_input_graph)
simple_basis=[nx.Graph(b) for b in basis]
for b in simple_basis:
for n in b.nodes():
......
......@@ -7,7 +7,7 @@
"outputs": [
{
"data": {
"image/png": "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\n",
"image/png": "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\n",
"text/plain": [
"<Figure size 432x288 with 1 Axes>"
]
......@@ -19,18 +19,16 @@
}
],
"source": [
"import sys\n",
"sys.path.insert(0, \"../cycle_analysis\")\n",
"import cycle_tools_coalescence as ctc\n",
"import test as cat\n",