-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathgraph.py
194 lines (175 loc) · 8.62 KB
/
graph.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
'''
This script produces two radial convergence graphs (aka chord graphs) for Sobol
Sensitivity Analysis results in a dictionary format. It can directly work with
Sobol results produced by Rhodium. For Sobol results generated by SALib,
convert first, as in salib_example.py.
The script will produce one graph with straight lines and one with curved lines
(parabolas).
Written by Antonia Hadjimichael (https://github.com/antonia-had) based off code
by Enrico Ubaldi (https://github.com/ubi15/).
The script was written and tested in Python 3.6 and 3.7.
'''
import networkx as nx
import numpy as np
import itertools
import matplotlib.pyplot as plt
def drawgraphs(SAresults):
# Get list of parameters
parameters = list(SAresults['S1'].keys())
# Set min index value, for the effects to be considered significant
index_significance_value = 0.01
'''
Define some general layout settings.
'''
node_size_min = 15 # Max and min node size
node_size_max = 30
border_size_min = 1 # Max and min node border thickness
border_size_max = 8
edge_width_min = 1 # Max and min edge thickness
edge_width_max = 10
edge_distance_min = 0.1 # Max and min distance of the edge from the center of the circle
edge_distance_max = 0.6 # Only applicable to the curved edges
'''
Set up some variables and functions that will facilitate drawing circles and
moving items around.
'''
# Define circle center and radius
center = [0.0,0.0]
radius = 1.0
# Function to get distance between two points
def distance(p1,p2):
return np.sqrt(((p1-p2)**2).sum())
# Function to get middle point between two points
def middle(p1,p2):
return (p1+p2)/2
# Function to get the vertex of a curve between two points
def vertex(p1,p2,c):
m = middle(p1,p2)
curve_direction = c-m
return m+curve_direction*(edge_distance_min+edge_distance_max*(1-distance(m,c)/distance(c,p1)))
# Function to get the angle of the node from the center of the circle
def angle(p,c):
# Get x and y distance of point from center
[dx,dy] = p-c
if dx == 0: # If point on vertical axis (same x as center)
if dy>0: # If point is on positive vertical axis
return np.pi/2.
else: # If point is on negative vertical axis
return np.pi*3./2.
elif dx>0: # If point in the right quadrants
if dy>=0: # If point in the top right quadrant
return np.arctan(dy/dx)
else: # If point in the bottom right quadrant
return 2*np.pi+np.arctan(dy/dx)
elif dx<0: # If point in the left quadrants
return np.pi+np.arctan(dy/dx)
'''
First, set up graph with all parameters as nodes and draw all second order (S2)
indices as edges in the network. For every S2 index, we need a Source parameter,
a Target parameter, and the Weight of the line, given by the S2 index itself.
'''
combs = [list(c) for c in list(itertools.combinations(parameters, 2))]
Sources = list(list(zip(*combs))[0])
Targets = list(list(zip(*combs))[1])
# Sometimes computing errors produce negative Sobol indices. The following reads
# in all the indices and also ensures they are between 0 and 1.
Weights = [max(min(x, 1), 0) for x in [SAresults['S2'][Sources[i]][Targets[i]] for i in range(len(Sources))]]
Weights = [0 if x<index_significance_value else x for x in Weights]
# Set up graph
G = nx.Graph()
# Draw edges with appropriate weight
for s,t,weight in zip(Sources, Targets, Weights):
G.add_edges_from([(s,t)], w=weight)
# Generate dictionary of node postions in a circular layout
Pos = nx.circular_layout(G)
'''
Normalize node size according to first order (S1) index. First, read in S1 indices,
ensure they're between 0 and 1 and normalize them within the max and min range
of node sizes.
Then, normalize edge thickness according to S2.
'''
# Node size
first_order = [max(min(x, 1), 0) for x in [SAresults['S1'][key] for key in SAresults['S1']]]
first_order = [0 if x<index_significance_value else x for x in first_order]
node_size = [node_size_min*(1 + (node_size_max-node_size_min)*k/max(first_order)) for k in first_order]
# Node border thickness
total_order = [max(min(x, 1), 0) for x in [SAresults['ST'][key] for key in SAresults['ST']]]
total_order = [0 if x<index_significance_value else x for x in total_order]
border_size = [border_size_min*(1 + (border_size_max-border_size_min)*k/max(total_order)) for k in total_order]
# Edge thickness
edge_width = [edge_width_min*((edge_width_max-edge_width_min)*k/max(Weights)) for k in Weights]
'''
Draw network. This will draw the graph with straight lines along the edges and
across the circle.
'''
nx.draw_networkx_nodes(G, Pos, node_size=node_size, node_color='#98B5E2',
edgecolors='#1A3F7A', linewidths = border_size)
nx.draw_networkx_edges(G, Pos, width=edge_width, edge_color='#2E5591', alpha=0.7)
names = nx.draw_networkx_labels(G, Pos, font_size=12, font_color='#0B2D61', font_family='sans-serif')
for node, text in names.items():
position = (radius*1.1*np.cos(angle(Pos[node],center)), radius*1.1*np.sin(angle(Pos[node],center)))
text.set_position(position)
text.set_clip_on(False)
plt.gcf().set_size_inches(9,9) # Make figure a square
plt.axis('off')
'''
We can now draw the network with curved lines along the edges and across the circle.
Calculate all distances between 1 node and all the others (all distances are
the same since they're in a circle). We'll need this to identify the curves
we'll be drawing along the perimeter (i.e. those that are next to each other).
'''
min_distance = round(min([distance(Pos[list(G.nodes())[0]],Pos[n]) for n in list(G.nodes())[1:]]), 1)
# Figure to generate the curved edges between two points
def xy_edge(p1,p2): # Point 1, Point 2
m = middle(p1,p2) # Get middle point between the two nodes
# If the middle of the two points falls very close to the center, then the
# line between the two points is simply straight
if distance(m,center)<1e-6:
xpr = np.linspace(p1[0],p2[0],10)
ypr = np.linspace(p1[1],p2[1],10)
# If the distance between the two points is the minimum (i.e. they are next
# to each other), draw the edge along the perimeter
elif distance(p1,p2)<=min_distance:
# Get angles of two points
p1_angle = angle(p1,center)
p2_angle = angle(p2,center)
# Check if the points are more than a hemisphere apart
if max(p1_angle,p2_angle)-min(p1_angle,p2_angle) > np.pi:
radi = np.linspace(max(p1_angle,p2_angle)-2*np.pi,min(p1_angle,p2_angle))
else:
radi = np.linspace(min(p1_angle,p2_angle),max(p1_angle,p2_angle))
xpr = radius*np.cos(radi)+center[0]
ypr = radius*np.sin(radi)+center[1]
# Otherwise, draw curve (parabola)
else:
edge_vertex = vertex(p1,p2,center)
a = distance(edge_vertex, m)/((distance(p1,p2)/2)**2)
yp = np.linspace(-distance(p1,p2)/2, distance(p1,p2)/2,100)
xp = a*(yp**2)
xp += distance(center,edge_vertex)
theta_m = angle(middle(p1,p2),center)
xpr = np.cos(theta_m)*xp - np.sin(theta_m)*yp
ypr = np.sin(theta_m)*xp + np.cos(theta_m)*yp
xpr += center[0]
ypr += center[1]
return xpr,ypr
'''
Draw network. This will draw the graph with curved lines along the edges and
across the circle.
'''
fig = plt.figure(figsize=(9,9))
ax = fig.add_subplot(1,1,1)
for i, e in enumerate(G.edges()):
x,y=xy_edge(Pos[e[0]],Pos[e[1]])
ax.plot(x,y,'-',c='#2E5591',lw=edge_width[i],alpha=0.7)
for i, n in enumerate(G.nodes()):
ax.plot(Pos[n][0],Pos[n][1], 'o', c='#98B5E2', markersize=node_size[i]/5, markeredgecolor = '#1A3F7A', markeredgewidth = border_size[i]*1.15)
for i, text in enumerate(G.nodes()):
if node_size[i]<100:
position = (radius*1.05*np.cos(angle(Pos[text],center)), radius*1.05*np.sin(angle(Pos[text],center)))
else:
position = (radius*1.01*np.cos(angle(Pos[text],center)), radius*1.01*np.sin(angle(Pos[text],center)))
plt.annotate(text, position, fontsize = 12, color='#0B2D61', family='sans-serif')
ax.axis('off')
fig.tight_layout()
plt.show()