import numpy as np import matplotlib import matplotlib.pyplot as plt from matplotlib.pyplot import cm from matplotlib import animation from mpl_toolkits.mplot3d import Axes3D import scipy as sp import networkx as nx from scipy import cluster from scipy import interpolate import sklearn.cluster as sklc import community as comm get_ipython().magic('matplotlib') np.set_printoptions(precision=3, linewidth=120) plt.rcParams['animation.ffmpeg_path'] = 'C:\\FFMPEG\\bin\\ffmpeg.exe' def computeClusters(A): # perform community detection with Louvain method # input: adjacency matrix A # outputs: modularity, number of communities, community sizes, mapping of nodes to communities G = nx.from_numpy_matrix(A) # calculate graph partition part = comm.best_partition(G) # calculate partition modularity mod_num = comm.modularity(part,G) print('Modularity: ' + str(comm.modularity(part,G))) # map nodes to communities comm2Nodes = {} Nodes2comm = np.zeros(len(A[:,0])) for idx in set(part.values()): comm2Nodes[idx] = [] for node,community in part.items(): (comm2Nodes[community]).append(node) Nodes2comm[node] = community # calculate community sizes, record number of communities commSizes=[] for community in comm2Nodes.keys(): commSizes.append(len(comm2Nodes[community])) print('Community sizes: ' + str(commSizes)) num_comm = len(commSizes) print('Number of Communities: ' + str(num_comm)) print() return mod_num,num_comm,commSizes,Nodes2comm def f1_calc(comms1,comms2): # calculate similarity between community assignments in two lists of graph nodes # use Rand similarity index # inputs: two lists of community assignments # output: similarity measurement (Rand index) num_nodes = len(comms1) sum_out = 0 for i in range(0,num_nodes): for j in range(i+1,num_nodes): same_comm1 = comms1[i] == comms1[j] same_comm2 = comms2[i] == comms2[j] sum_out = sum_out + (same_comm1 == same_comm2) #normalize the output sum_out = sum_out/(num_nodes*(num_nodes-1)/2) return sum_out class grph_seq(): # create a sequence of weighted graphs using interpolation def __init__(self,g_size,wt,slices,f_per_s,data_source,real_data_name,geodesic,use_bias): # initialize the dynamic graph # g_size (int): number of nodes in graph # wt (Boolean): flag for using weighted or unweighted snapshots # slices (int): number of snapshots # f_per_s (array of ints): number of interpolated points desired between each sequential pair of snapshots # data_source (string): how to generate the snapshot data # real_data_name (string): file name for given data source # geodesic (int): indicator for using entry-wise linear (0), AI geodesic (1), or LE geodesic (2) interpolation # use_bias (double): bias value to use (0 for no bias) self.fps = f_per_s self.n_vert = g_size self.frames = np.sum(f_per_s) + 1 self.slices = slices self.num_slices = len(slices) self.use_bias = use_bias # record total number of frames up to snapshot i frame_sum = [0] for i in range(1,self.num_slices): frame_sum.append(np.sum(f_per_s[0:i],dtype=int)) self.frame_sum = frame_sum lap_evals = np.zeros((self.n_vert,self.frames)) lap_evecs = np.zeros((self.n_vert,self.n_vert,self.frames)) lap_evals_unbias = np.zeros((self.n_vert,self.frames)) lap_evecs_unbias = np.zeros((self.n_vert,self.n_vert,self.frames)) laplace = np.zeros((self.n_vert,self.n_vert,self.frames)) laplace_unbias = np.zeros((self.n_vert,self.n_vert,self.frames)) alpha_A = np.zeros((self.n_vert,self.n_vert-1,self.frames)) R_vals = np.zeros((self.n_vert-1,self.n_vert-1,self.frames)) # input reference slices for i in range(0,self.num_slices): if data_source == 'real data': eps_const = use_bias/self.n_vert adj_temp2 = np.zeros((g_size,g_size)) file_name = real_data_name + '-' + str(slices[i]) + '.edges' for grph_file in open(file_name): node_temp = grph_file.split(' ') node_temp[1] = node_temp[1].rstrip('\n') if (int(node_temp[0]) < self.n_vert) and (int(node_temp[1]) < self.n_vert): adj_temp2[int(node_temp[0])-1,int(node_temp[1])-1] = 1 adj_temp2[int(node_temp[1])-1,int(node_temp[0])-1] = 1 else: # generate random power-law graphs g = nx.random_graphs.powerlaw_cluster_graph(g_size, 1, .4) adj_temp = nx.adjacency_matrix(g) adj_temp2 = np.double(np.asarray(adj_temp.todense())) # for weighted graph if wt: rander = np.asarray(5*np.random.rand(self.n_vert,self.n_vert)) rander = (rander + rander.T)/2 adj_temp2 = np.double(adj_temp2*rander) if use_bias > 0: # add bias term to keep rank constant adj_temp2 = adj_temp2 + eps_const*(np.ones((g_size,g_size))-np.eye(g_size)) laplace[:,:,frame_sum[i]] = -adj_temp2 for j in range(0,self.n_vert): laplace[j,j,frame_sum[i]] = np.sum(adj_temp2[j,:]) - adj_temp2[j,j] # calculate eigenvalues, eigenvectors evals,evecs = np.linalg.eigh(laplace[:,:,frame_sum[i]]) alpha_A[:,:,frame_sum[i]] = evecs[:,1:] lap_evecs[:,:,frame_sum[i]] = np.real(np.round(evecs,decimals=6)) lap_evals[:,frame_sum[i]] = np.real(np.round(evals,decimals=6)) # calculate intermediate slices for i in range(0,self.num_slices-1): if geodesic == 1: # use affine-invariant geodesic if i == 0: alpha_1 = alpha_A[:,:,0] alpha_2 = alpha_A[:,:,frame_sum[1]] # theta will always be one because the nullspace of the Laplacian will always be the same O_B, s_AB, O_A = np.linalg.svd(np.dot(alpha_2.T,alpha_1), full_matrices=True) O_A = O_A.T #print(s_AB) # U_A and U_B will also be the same because you're staying in the same subspace U_A = np.dot(alpha_1,O_A) U_B = np.dot(alpha_2,O_B) if np.linalg.norm(U_A-U_B) > 1e-3: print('error') print(np.linalg.norm(U_A-U_B)) else: U_mat = U_A self.U_mat = U_mat R_A = np.dot(np.dot(U_mat.T,laplace[:,:,frame_sum[i]]),U_mat) R_B = np.dot(np.dot(U_mat.T,laplace[:,:,frame_sum[i+1]]),U_mat) R_vals[:,:,frame_sum[i]] = R_A R_vals[:,:,frame_sum[i+1]] = R_B temp1 = sp.linalg.fractional_matrix_power(R_A,-0.5) temp2 = sp.linalg.fractional_matrix_power(R_A,0.5) temp3 = sp.linalg.logm(np.dot(temp1,np.dot(R_B,temp1))) for j in range(1,f_per_s[i]): # calculate R_temp R_A_temp = np.dot(temp2,np.dot(sp.linalg.expm(temp3*j/f_per_s[i]),temp2)) R_vals[:,:,j+frame_sum[i]] = R_A_temp laplace[:,:,j+frame_sum[i]] = np.round(np.dot(U_mat,np.dot(R_A_temp,U_mat.T)),6) evals,evecs = np.linalg.eigh(laplace[:,:,j+frame_sum[i]]) lap_evecs[:,:,j+frame_sum[i]] = np.real(np.round(evecs,decimals=6)) lap_evals[:,j+frame_sum[i]] = np.real(np.round(evals,decimals=6)) elif geodesic == 2: # use log-Euclidean geodesic if i == 0: alpha_1 = alpha_A[:,:,0] alpha_2 = alpha_A[:,:,frame_sum[1]] # theta will always be one because the nullspace of the Laplacian will always be the same O_B, s_AB, O_A = np.linalg.svd(np.dot(alpha_2.T,alpha_1), full_matrices=True) O_A = O_A.T #print(s_AB) # U_A and U_B will also be the same because you're staying in the same subspace U_A = np.dot(alpha_1,O_A) U_B = np.dot(alpha_2,O_B) if np.linalg.norm(U_A-U_B) > 1e-3: print('error') print(np.linalg.norm(U_A-U_B)) else: U_mat = U_A self.U_mat = U_mat R_A = np.dot(np.dot(U_mat.T,laplace[:,:,frame_sum[i]]),U_mat) R_B = np.dot(np.dot(U_mat.T,laplace[:,:,frame_sum[i+1]]),U_mat) R_vals[:,:,frame_sum[i]] = R_A R_vals[:,:,frame_sum[i+1]] = R_B temp1 = sp.linalg.logm(R_A) temp2 = sp.linalg.logm(R_B) for j in range(1,f_per_s[i]): # calculate R_temp R_A_temp = sp.linalg.expm((1-j/f_per_s[i])*temp1 + temp2*j/f_per_s[i]) R_vals[:,:,j+frame_sum[i]] = R_A_temp laplace[:,:,j+frame_sum[i]] = np.round(np.dot(U_mat,np.dot(R_A_temp,U_mat.T)),6) evals,evecs = np.linalg.eigh(laplace[:,:,j+frame_sum[i]]) lap_evecs[:,:,j+frame_sum[i]] = np.real(np.round(evecs,decimals=6)) lap_evals[:,j+frame_sum[i]] = np.real(np.round(evals,decimals=6)) else: for j in range(1,f_per_s[i]): laplace[:,:,j+frame_sum[i]] = (laplace[:,:,frame_sum[i]]* (1-j/f_per_s[i]) + laplace[:,:,frame_sum[i+1]]*j/f_per_s[i]) evals,evecs = np.linalg.eigh(laplace[:,:,j+frame_sum[i]]) lap_evecs[:,:,j+frame_sum[i]] = np.real(np.round(evecs,decimals=6)) lap_evals[:,j+frame_sum[i]] = np.real(np.round(evals,decimals=6)) # correct eigenvector orientation lap_evecs[:,0,0] = lap_evecs[:,0,0]*np.sign(lap_evecs[0,0,0]) lap_evecs[:,1,0] = lap_evecs[:,1,0]*np.sign(lap_evecs[0,1,0]) lap_evecs[:,2,0] = lap_evecs[:,2,0]*np.sign(lap_evecs[0,2,0]) for i in range(1,self.frames): for j in range(0,4): lap_evecs[:,j,i] = np.sign(np.dot(lap_evecs[:,j,i-1],lap_evecs[:,j,i]))*lap_evecs[:,j,i] # remove bias term here if use_bias > 0: for i in range(0,self.frames): laplace_unbias[:,:,i] = laplace[:,:,i] - eps_const*( self.n_vert*np.eye(self.n_vert)-np.ones((self.n_vert,self.n_vert))) evals,evecs = np.linalg.eigh(laplace_unbias[:,:,i]) lap_evecs_unbias[:,:,i] = np.real(np.round(evecs,decimals=6)) lap_evals_unbias[:,i] = np.real(np.round(evals,decimals=6)) else: laplace_unbias = laplace lap_evecs_unbias = lap_evecs lap_evals_unbias = lap_evals # calculate adjacency matrices adj_mat = np.zeros((self.n_vert,self.n_vert,self.frames)) for i in range(0,self.frames): adj_mat[:,:,i] = np.round(np.diag(np.diag(laplace_unbias[:,:,i])) - laplace_unbias[:,:,i],6) self.laplace = laplace self.lap_evals = lap_evals self.lap_evecs = lap_evecs self.R_vals = R_vals self.adj_mat = adj_mat self.laplace_unbias = laplace_unbias self.lap_evals_unbias = lap_evals_unbias self.lap_evecs_unbias = lap_evecs_unbias return def ave_community(self,sigma): # calculate simple average of laplacians # create spectral plot with second and third eigenvectors # use symmetric Gaussian kernel with standard deviation of sigma # calculate average laplacian lap_ave = np.zeros((self.n_vert,self.n_vert)) for ind in self.frame_sum: lap_ave += self.laplace[:,:,ind] lap_ave = lap_ave/self.num_slices adj_ave = np.diag(np.diag(lap_ave)) - lap_ave # remove bias term here if self.use_bias > 0: adj_ave = adj_ave - self.use_bias/self.n_vert*(np.ones((self.n_vert,self.n_vert)) - np.eye(self.n_vert)) self.adj_mat_ave_lin = np.round(adj_ave,6) if sigma > 0: # calculate eigendecomposition evals,evecs = np.linalg.eigh(lap_ave) evecs = np.real(np.round(evecs,decimals=6)) evals = np.real(np.round(evals,decimals=6)) # fix the orientation of the relevant eigenvectors evecs[:,0] = evecs[:,0]*np.sign(evecs[0,0]) evecs[:,1] = evecs[:,1]*np.sign(evecs[0,1]) evecs[:,2] = evecs[:,2]*np.sign(evecs[0,2]) # plot results bound = np.round(1/np.log(self.n_vert/4),2) spacing = bound/50 x,y = np.mgrid[-bound:(bound+spacing):spacing,-bound:(bound+spacing):spacing] f_size = np.shape(x) f = np.zeros((f_size[0],f_size[1])) for j in range(0,self.n_vert): f += np.exp(-((x-evecs[j,1])**2 + (y-evecs[j,2])**2)/(2*sigma**2)) fig = plt.figure plt.axes(xlim=(-bound, bound), ylim=(-bound, bound)) pt_plot = plt.plot(evecs[:,1],evecs[:,2],'go',markeredgecolor='k') cont_plot = plt.contour(x,y,f,50) plt.show() return def ave_community2(self,sigma): # calculate the log-Euclidean average of laplacians # create spectral plot with second and third eigenvectors # use symmetric Gaussian kernel with standard deviation of sigma # calculate average laplacian R_ave = np.zeros((self.n_vert-1,self.n_vert-1)) for ind in self.frame_sum: R_ave += sp.linalg.logm(self.R_vals[:,:,ind]) R_ave = R_ave/self.num_slices R_ave = sp.linalg.expm(R_ave) # calculate eigendecomposition lap_ave = np.dot(self.U_mat,np.dot(R_ave,self.U_mat.T)) adj_ave = np.diag(np.diag(lap_ave)) - lap_ave # remove bias term here if self.use_bias > 0: adj_ave = adj_ave - self.use_bias/self.n_vert*(np.ones((self.n_vert,self.n_vert)) - np.eye(self.n_vert)) self.adj_mat_ave_geo2 = np.round(adj_ave,6) if sigma > 0: evals,evecs = np.linalg.eigh(lap_ave) evecs = np.real(np.round(evecs,decimals=6)) evals = np.real(np.round(evals,decimals=6)) # fix the orientation of the relevant eigenvectors evecs[:,0] = evecs[:,0]*np.sign(evecs[0,0]) evecs[:,1] = evecs[:,1]*np.sign(evecs[0,1]) evecs[:,2] = evecs[:,2]*np.sign(evecs[0,2]) # plot results bound = np.round(1/np.log(self.n_vert/4),2) spacing = bound/50 x,y = np.mgrid[-bound:(bound+spacing):spacing,-bound:(bound+spacing):spacing] f_size = np.shape(x) f = np.zeros((f_size[0],f_size[1])) for j in range(0,self.n_vert): f += np.exp(-((x-evecs[j,1])**2 + (y-evecs[j,2])**2)/(2*sigma**2)) fig = plt.figure plt.axes(xlim=(-bound, bound), ylim=(-bound, bound)) pt_plot = plt.plot(evecs[:,1],evecs[:,2],'go',markeredgecolor='k') cont_plot = plt.contour(x,y,f,50) plt.show() return def ave_community3(self,sigma): # find the affine-invariant average of Laplacians # create spectral plot with second and third eigenvectors # use symmetric Gaussian kernel with standard deviation of sigma # calculate R's, choose initial point as arithmetic mean lap_R = np.zeros((self.n_vert-1,self.n_vert-1,self.num_slices)) lap_mean = np.zeros((self.n_vert,self.n_vert)) # calculate R matrices for i in range(0,self.num_slices): lap_R[:,:,i] = self.R_vals[:,:,self.frame_sum[i]] self.ave_community(sigma=0) adj_temp = self.adj_mat_ave_lin + self.use_bias/self.n_vert*( np.ones((self.n_vert,self.n_vert))-np.eye(self.n_vert)) lap_mean = np.diag(np.sum(adj_temp,axis=0)) - adj_temp R_mean = np.dot(self.U_mat.T,np.dot(lap_mean,self.U_mat)) # calculate average using gradient descent conv = 10 iters = 0 while (conv > 1e-3) and (iters < 200): mean_grad = np.zeros((self.n_vert-1,self.n_vert-1)) R_temp1 = sp.linalg.fractional_matrix_power(R_mean,0.5) R_temp2 = sp.linalg.fractional_matrix_power(R_mean,-0.5) for i in range(0,self.num_slices): R_temp3 = sp.linalg.logm(np.dot(R_temp2,np.dot(lap_R[:,:,i],R_temp2))) R_temp3 = np.real(R_temp3) mean_grad += R_temp3/self.num_slices conv = np.linalg.norm(mean_grad) R_mean = np.dot(R_temp1,np.dot(sp.linalg.expm(0.5*mean_grad/(1+0.1*iters)),R_temp1)) iters += 1 print(conv) print(iters) lap_mean = np.dot(self.U_mat,np.dot(R_mean,self.U_mat.T)) adj_ave = np.diag(np.diag(lap_mean)) - lap_mean # remove bias term here if self.use_bias > 0: adj_ave = adj_ave - self.use_bias/self.n_vert*(np.ones((self.n_vert,self.n_vert)) - np.eye(self.n_vert)) self.adj_mat_ave_geo = np.round(adj_ave,6) # calculate eigendecomposition evals,evecs = np.linalg.eigh(lap_mean) evecs = np.real(np.round(evecs,decimals=6)) evals = np.real(np.round(evals,decimals=6)) # fix the orientation of the relevant eigenvectors evecs[:,0] = evecs[:,0]*np.sign(evecs[0,0]) evecs[:,1] = evecs[:,1]*np.sign(evecs[0,1]) evecs[:,2] = evecs[:,2]*np.sign(evecs[0,2]) # plot results bound = np.round(1/np.log(self.n_vert/4),2) bound = np.round(40/self.n_vert,2) spacing = bound/50 x,y = np.mgrid[-bound:(bound+spacing):spacing,-bound:(bound+spacing):spacing] f_size = np.shape(x) f = np.zeros((f_size[0],f_size[1])) for j in range(0,self.n_vert): f += np.exp(-((x-evecs[j,1])**2 + (y-evecs[j,2])**2)/(2*sigma**2)) fig = plt.figure plt.axes(xlim=(-bound, bound), ylim=(-bound, bound)) pt_plot = plt.plot(evecs[:,1],evecs[:,2],'go',markeredgecolor='k') cont_plot = plt.contour(x,y,f,50) plt.show() return def plot_slice(self,sigma,slice_num): # select a slice and do a spectral plot for that slice bound = np.round(1/np.log(self.n_vert/4),2) bound = np.round(40/self.n_vert,2) spacing = bound/50 x,y = np.mgrid[-bound:(bound+spacing):spacing,-bound:(bound+spacing):spacing] g_size = np.shape(x) f = np.zeros((g_size[0],g_size[1])) evec_temp = self.lap_evecs n_pts = self.n_vert for j in range(0,n_pts): f += np.exp(-((x-evec_temp[j,1,slice_num])**2 + (y-evec_temp[j,2,slice_num])**2)/(2*sigma**2)) fig = plt.figure() plt.axes(xlim=(-bound, bound), ylim=(-bound, bound)) pt_plot = plt.plot(self.lap_evecs[:,1,slice_num],self.lap_evecs[:,2,slice_num],'go',markeredgecolor='k') cont_plot = plt.contour(x,y,f,50) plt.show() def cont_plot(self,sigma): # create animation of spectral plots # create grid bound = np.round(1/np.log(self.n_vert/4),2) bound = np.round(40/self.n_vert,2) spacing = bound/50 x,y = np.mgrid[-bound:(bound+spacing):spacing,-bound:(bound+spacing):spacing] f_size = np.shape(x) f = np.zeros((f_size[0],f_size[1],self.frames)) evec_temp = self.lap_evecs n_pts = self.n_vert for i in range(0,self.frames): for j in range(0,n_pts): f[:,:,i] += np.exp(-((x-evec_temp[j,1,i])**2 + (y-evec_temp[j,2,i])**2)/(2*sigma**2)) # create plots fig = plt.figure() ax = fig.add_subplot(111) ax.set_xlim(-bound,bound) ax.set_ylim(-bound,bound) pt_plot = ax.plot(self.lap_evecs[:,1,0],self.lap_evecs[:,2,0],'go',markeredgecolor='k') cont_plot = ax.contour(x,y,f[:,:,0],50) def anim_cont_plot(i,f,pt_plot,cont_plot): ax.clear() pt_plot = ax.plot(self.lap_evecs[:,1,i],self.lap_evecs[:,2,i],'go',markeredgecolor='k') cont_plot = ax.contour(x,y,f[:,:,i],50) ax.set_xlim(-bound,bound) ax.set_ylim(-bound,bound) return pt_plot,cont_plot, anim = animation.FuncAnimation(fig,anim_max_find,fargs=(f,pt_plot,cont_plot), frames=int(self.frames),blit=False,repeat=False) plt.show() FFwriter = animation.FFMpegWriter() anim.save('demo.mp4',writer=FFwriter,dpi=200) return def graph_constr(self,cutoff): # use networkx.from_numpy_matrix to turn the interpolated adjacency matrices into networkx graphs # store list of graphs produced graph_list = [] for i in range(0,self.frames): adj_mat_temp = np.matrix(np.round(self.adj_mat[:,:,i],cutoff)) graph_list.append(nx.from_numpy_matrix(adj_mat_temp)) self.graph_list = graph_list return def graph_plot(self,line_size,node_size): # animate the graph plot as the graph moves along the geodesic fig = plt.figure() ax_curr = fig.add_subplot(111) ax_curr.set_xlim(-1.1,1.1) ax_curr.set_ylim(-1.1,1.1) labels={} for i in range(0,self.n_vert): labels[i] = i edges = self.graph_list[0].edges() weights = [self.graph_list[0][u][v]['weight']*line_size for u,v in edges] pos = nx.circular_layout(self.graph_list[-1]) node_col_temp = np.chararray((self.n_vert,1)) node_col_temp[:] = 'r' node_col_temp[np.asarray(nx.isolates(self.graph_list[0]))] = 'k' node_col = node_col_temp.tostring() anim_out = nx.draw(self.graph_list[0],pos=pos,ax=ax_curr,edges=edges,width=weights, node_size=node_size,labels=labels,font_size=8,cmap=cm.coolwarm,node_color=list(node_col)) def anim_graph_plot(i): ax_curr.clear() edges = self.graph_list[i].edges() weights = [self.graph_list[i][u][v]['weight']*line_size for u,v in edges] node_col_temp = np.chararray((self.n_vert,1)) node_col_temp[:] = 'r' node_col_temp[nx.isolates(self.graph_list[i])] = 'k' node_col = node_col_temp.tostring() anim_out = nx.draw(self.graph_list[i],pos=pos,ax=ax_curr,edges=edges,width=weights, node_size=node_size,labels=labels,font_size=8,node_color=list(node_col)) ax_curr.set_xlim(-1.1,1.1) ax_curr.set_ylim(-1.1,1.1) return anim_out, anim = animation.FuncAnimation(fig,anim_graph_plot,frames=int(self.frames),blit=False,repeat=False) plt.show() FFwriter = animation.FFMpegWriter() anim.save('demo.mp4',writer=FFwriter,dpi=200) return