diff --git a/netrw/__init__.py b/netrw/__init__.py index 83f2d6c..b6cfce4 100644 --- a/netrw/__init__.py +++ b/netrw/__init__.py @@ -7,4 +7,5 @@ A 2022 NetSI Collabathon Product. """ +from .analysis import * from .rewire import * diff --git a/netrw/analysis/__init__.py b/netrw/analysis/__init__.py new file mode 100644 index 0000000..77bad4f --- /dev/null +++ b/netrw/analysis/__init__.py @@ -0,0 +1,5 @@ +from .confusion import * +from .distance_trajectory import * +from .plot_property_values_over_time import * +from .properties_heatmap import * +from .properties_overtime import * diff --git a/netrw/analysis/confusion.py b/netrw/analysis/confusion.py new file mode 100644 index 0000000..dbf6543 --- /dev/null +++ b/netrw/analysis/confusion.py @@ -0,0 +1,45 @@ +import numpy as np + + +def rewiring_distance_confusion_matrix( + G, rewiring_methods, distance_measures, timesteps=100, ensemble_size=10 +): + """Plotting distances from start graph for different rewiring schemes and distance metrics + + Parameters + ---------- + G : NetworkX Graph + The starting graph + rewiring_methods : list of Rewiring classes in the rewiring module. + methods for rewiring graphs. each class specified must have a + `full_rewire` method and that method must have `timesteps` as + a keyword argument. + distance_measures : netrd Distance + metric for measuring the distance between the before and after graphs + timesteps : int, default: 100 + the number of iterations + ensemble_size : int, default: 10 + the number of rewiring trajectories to run. + + Returns + ------- + numpy matrix + a matrix where rows are rewiring methods, columns are distance metrics + and entries are average distances. + + Notes + ----- + Currently this method does not support keyword args for the rewiring methods + and distance metrics. + """ + n = len(rewiring_methods) + m = len(distance_measures) + C = np.zeros([n, m]) + for i in range(n): + rw = rewiring_methods[i]() + for j in range(m): + dist = distance_measures[i]() + for k in range(ensemble_size): + rG = rw.full_rewire(G, timesteps=timesteps) + C[i, j] += dist(rG, G) / ensemble_size + return C diff --git a/netrw/analysis/distance_trajectory.py b/netrw/analysis/distance_trajectory.py new file mode 100644 index 0000000..a946045 --- /dev/null +++ b/netrw/analysis/distance_trajectory.py @@ -0,0 +1,164 @@ +import copy +import warnings + +import netrd +import networkx as nx +import numpy as np +from matplotlib import pyplot as plt + +from ..rewire import NetworkXEdgeSwap + + +def distanceTrajectory( + G, + distance=netrd.distance.Hamming, + rewire=NetworkXEdgeSwap, + num_steps=100, + num_runs=100, + distance_kwargs={}, + rewire_kwargs={}, +): + """ + Get some data on graph distances as a function of number of rewiring steps. + + Parameters + ---------- + G : networkx Graph or DiGraph + + distance : netrd graph distance class + + rewire : netrw rewire class + + num_steps : integer or list + number of rewiring steps to be tracked or ordered list of rewiring + steps to be tracked + + num_runs : integer + number of trajectories to be generated for evaluating the standard + deviation for a set of rewiring trajectories + + distance_kwargs : dictionary + a dictionary of keyword arguments for an instantiation of the netrd + distance class + + rewire_kwargs : dictionary + a dictionary of keyword arguments for an instantiation of + the netrw rewire class + """ + + G0 = copy.deepcopy(G) + + # check whether input for num rewire in a number of rewiring steps (int) + # or a list of steps + if isinstance(num_steps, list): + rewire_steps = num_steps + else: + rewire_steps = range(num_steps) + + # initialize data array + data = np.zeros((len(rewire_steps), num_runs)) + + # define a rewire function + step_rewire = rewire().step_rewire + rewire_function = lambda g: step_rewire(g, copy_graph=False, **rewire_kwargs) + + # define a distance function + distfun = distance() # get a class instantiation + distance_function = lambda g1, g2: distfun(g1, g2, **distance_kwargs) + + for j in range(num_runs): + for i in range(max(rewire_steps)): + rewire_function(G0) + data[i + 1, j] = distance_function(G0, G) + + return data + + +def plotDistanceTrajectory( + G, + distance=netrd.distance.Hamming, + num_steps=100, + show=["mean", "median", "std-env"], + labels=None, + add_legend=True, + fig=None, + ax=None, + linecolors=None, + envcolor="cyan", + xlabel="Number of rewiring steps", + ylabel=None, + **kwargs +): + + # check whether input for num steps in a number of rewiring steps (int) + # or a list of steps + if hasattr(num_steps, "__iter__"): + rewire_steps = num_steps + else: + rewire_steps = range(num_steps) + + # set ylabel + if ylabel is None: + ylabel = distance.__name__ + r" distance to $G_0$" + + # set line labels + if labels is None: + labels = show + elif hasattr(labels, "__iter__"): + if len(labels) != len(show): + raise ValueError( + "List for keyword argument `show` and list for" + + "`keyword argument` must have the same length." + ) + else: + raise ValueError("Keyword argument `labels` must be None or list.") + + # set line colors + if linecolors is None: + tabcolors = ["tab:blue", "tab:orange", "tab:green", "tab:red", "tab:purple"] + linecolors = [tabcolors[i % len(tabcolors)] for i in range(len(show))] + elif hasattr(linecolors, "__iter__"): + if len(linecolors) != len(show): + raise ValueError( + "List for keyword argument `show` and list for" + + "`keyword argument` must have the same length." + ) + else: + raise ValueError("Keyword argument `labels` must be None or list.") + + # get data + data = distanceTrajectory(G, distance=distance, num_steps=num_steps, **kwargs) + + # get data for lines for plot + line_data = [] + for s in show: + if s == "mean": + line_data += [np.mean(data, axis=1)] + elif s == "std": + line_data += [np.std(data, axis=1)] + elif s == "median": + line_data += [np.median(data, axis=1)] + elif s == "std-env": + mean = np.mean(data, axis=1) + std = np.std(data, axis=1) + env_data = [std - mean, std + mean] + else: + warnings.warn("Unknown summary statistic", s, "will be ignored.") + + std = np.std(data, axis=1) + + if fig is None: + fig = plt.gcf() + if ax is None: + ax = plt.subplot(111) + if "std-env" in show: + ax.fill_between(rewire_steps, env_data[0], env_data[1], color=envcolor) + + for i in range(len(line_data)): + ax.plot(rewire_steps, line_data[i], color=linecolors[i], label=labels[i]) + + if add_legend: + plt.legend() + + plt.xlabel(xlabel) + plt.ylabel(ylabel) diff --git a/netrw/analysis/plot_property_values_over_time.py b/netrw/analysis/plot_property_values_over_time.py new file mode 100644 index 0000000..ba0a6f5 --- /dev/null +++ b/netrw/analysis/plot_property_values_over_time.py @@ -0,0 +1,67 @@ +import matplotlib.pyplot as plt +import networkx as nx +import numpy as np + +import netrw + + +def plot_property_values_over_time(propvals, ylabel=""): + """ + Plot mean and standard deviation of how a network property changes over time during multiple iterations of a rewiring process. + + Parameters + ---------- + propvals :2d nump array + 2d numpy array of output values from dictionary of properties_overtime. + The network property is calculated at each step and iteration of the rewiring process. Rows are single iteration over a rewiring process. + Columns show different iterations of the rewiring process from the initial graph. + + ylabel: string, optional + Label for y axis of graph for mean and standard deviation of network property + Default is no label. + + Returns + ------- + fig: matplotlib figure + Figure of mean and standard deviation for a network property throughout the rewiring process. + + """ + + alllist = [] # list of all properties for all iterations at each of the time steps + + valarray = propvals + num_rows, num_cols = valarray.shape + + for k in range(num_cols): + alllist.append([]) + for l in range(num_rows): + alllist[k].append(valarray[l][k]) + + # find mean and standard deviation over different iterations of rewiring process + meanlist = [] + sdlist = [] + for k in range(num_cols): + meanlist.append(np.mean(alllist[k])) + sdlist.append(np.std(alllist[k])) + + # find upper and lower bound of standard deviation interval around the mean + upperbd = [] + lowerbd = [] + for a in range(len(meanlist)): + upperbd.append(meanlist[a] + sdlist[a]) + lowerbd.append(meanlist[a] - sdlist[a]) + + fig, (ax0) = plt.subplots(nrows=1) + ax0.plot(range(num_cols), meanlist, color="blue", linewidth=2) + ax0.plot(range(num_cols), upperbd, color="blue") + ax0.plot(range(num_cols), lowerbd, color="blue") + ax0.fill_between( + range(num_cols), upperbd, lowerbd, color="cornflowerblue", alpha=0.5 + ) + + ax0.set_xlabel("number of rewiring steps") + ax0.set_ylabel(ylabel) + + fig.show() + + return fig diff --git a/netrw/analysis/properties_heatmap.py b/netrw/analysis/properties_heatmap.py new file mode 100644 index 0000000..e69de29 diff --git a/netrw/analysis/properties_overtime.py b/netrw/analysis/properties_overtime.py new file mode 100644 index 0000000..013a59c --- /dev/null +++ b/netrw/analysis/properties_overtime.py @@ -0,0 +1,51 @@ +from copy import deepcopy + +import matplotlib.pyplot as plt +import networkx as nx +import numpy as np + +import netrw +from netrw.rewire import AlgebraicConnectivity, KarrerRewirer, NetworkXEdgeSwap + + +def properties_overtime(init_graph, rewire_method, property1, tmax, numit): + """ + Analyze the property values of a network as a function of rewire steps. + Looks at how a network property changes as a rewiring process occurs. + + Parameters + ---------- + init_graph : NetworkX graph + Initial graph upon which rewiring will occur. + rewire_method : netrw rewire class object + Algorithm for rewiring a network with step_rewire option + property1 : NetworkX function + Network description property that outputs a single value for a given network. Should work with any function that + summarizes a NetworkX graph object into a single value. For example, nx.average_clustering, nx.average_shortest_path_length, etc. + tmax : int + Number of rewiring steps to perform for each iteration. + numit : int + Number of rewiring iterations to perform on the initial graph. The given rewiring process will be performed numit + times on the initial graph to look at the distribution of outcomes for this rewiring process on the initial graph. + Returns + ------- + property_dict: dictionary + Dictionary of output where the keys are the property name and the values are a 2D numpy arry of the network property + calculated at each step and iteration of the rewiring process. Rows are single iteration over a rewiring process. + Columns show different iterations of the rewiring process from the initial graph. + + """ + property_dict = {} + property_dict[property1.__name__] = np.zeros((numit, tmax)) + rw = rewire_method() + + for i in range(numit): + G0 = deepcopy(init_graph) + propertyval = property1(G0) # calculate property of initial network + property_dict[property1.__name__][i, 0] = propertyval + for j in range(1, tmax): + G0 = rw.step_rewire(G0, copy_graph=False) # rewire + propertyval = property1(G0) # calculate property of the rewired network + property_dict[property1.__name__][i, j] = propertyval + + return property_dict diff --git a/netrw/analysis/rewiring_analysis.py b/netrw/analysis/rewiring_analysis.py new file mode 100644 index 0000000..abcc2dd --- /dev/null +++ b/netrw/analysis/rewiring_analysis.py @@ -0,0 +1,192 @@ +from copy import deepcopy + +import matplotlib.pyplot as plt +import networkx as nx +import numpy as np + +import netrw +from netrw.rewire import AlgebraicConnectivity, KarrerRewirer, NetworkXEdgeSwap + + +def various_properties_overtime( + init_graph, rewire_method, property_functions, function_names, tmax, numit +): + """ + Analyze the property values of a network as a function of rewire steps. + Looks at how a network property changes as a rewiring process occurs. + + Parameters + ---------- + init_graph : NetworkX graph + Initial graph upon which rewiring will occur. + rewire_method : netrw rewire class object + Algorithm for rewiring a network with step_rewire option + property_functions : list of functions with input being NetworkX graphs + function_names : list of strings describing the functions + Network description property that outputs a single value for a given network. Should work with any function that + summarizes a NetworkX graph object into a single value. For example, nx.average_clustering, nx.average_shortest_path_length, etc. + tmax : int + Number of rewiring steps to perform for each iteration. + numit : int + Number of rewiring iterations to perform on the initial graph. The given rewiring process will be performed numit + times on the initial graph to look at the distribution of outcomes for this rewiring process on the initial graph. + Returns + ------- + property_dict: dictionary + Dictionary of output where the keys are the iteration number and the values are a list of the network property calculated + at each step of the rewiring process. + """ + + all_properties = {} + + rw = rewire_method() + + for name in function_names: + + all_properties[name] = np.zeros((numit, tmax)) + + # loop over rewiring instances + for i in range(numit): + + G0 = deepcopy(init_graph) + + # calculate properties of initial network + for func, name in zip(property_functions, function_names): + + all_properties[name][i, 0] = func(G0) + + # loop over timesteps + for j in range(1, tmax): + + G0 = rw.step_rewire(G0, copy_graph=False) # rewire + + # calculate properties of the rewired network + + for name, func in zip(function_names, property_functions): + + all_properties[name][i, j] = func(G0) + + return all_properties + + +def calculate_statistics(all_properties): + """ + Find the mean, standard deviation, mean-std, and mean+std of the data from rewirings + + Inputs: + all_properties: dict of 2D np.arrays of shape numit x tmax with keys that are property names + + Outputs: + all_means: dict of mean values of each property at each timestep (keys are property names, values are 1D np.arrays of length tmax) + all_stds: likewise for standard deviations + all_lowers: likewise for mean-std + all_uppers: likewise for mean+std + + """ + + # find mean and standard deviation over different iterations of rewiring process + all_means = {} + all_stds = {} + all_lowers = {} + all_uppers = {} + + for name, data in all_properties.items(): + + all_means[name] = np.mean(data, axis=0) + all_stds[name] = np.std(data, axis=0) + all_lowers[name] = all_means[name] - all_stds[name] + all_uppers[name] = all_means[name] + all_stds[name] + + return all_means, all_stds, all_lowers, all_uppers + + +def average_local_clustering(G): + """ + Calculates average local clustering of networkx graph G + + Note: divides by the number of nodes with degree at least 2, + since local clustering is undefined for nodes with degree less than 2. + + """ + # get degrees and find how many nodes have degree at least 2 + k = np.array(list(dict(nx.degree(G)).values())) + n_kg1 = np.sum(k > 1) + + # find and sum up local clustering coefficient of all nodes + clu = nx.clustering(G) + tot = np.sum(list(nx.clustering(G).values())) + + # calculate average + barc = tot / n_kg1 + return barc + + +def average_shortest_path_length(G): + """ + Calculates average shortest path length of networkx graph G + + Note: divides by total number of shortest paths, namely the sum over components + of component-size-choose-2, so as to still get a meaningful result when + the graph is not connected. + """ + # find the sizes of connected components + C = list(nx.connected_components(G)) + Nv = list(map(len, C)) + + # calculate the total number of shortest paths + Npairs = np.sum([N * (N - 1) / 2 for N in Nv]) + + # sum up all shortest path lengths + total = np.sum( + [np.sum(list(v[1].values())) for v in nx.all_pairs_shortest_path_length(G)] + ) + + # calculate average + barl = total / Npairs + return barl + + +property_functions = [ + lambda G: G.number_of_nodes(), + lambda G: G.number_of_edges(), + lambda G: average_shortest_path_length(G), + lambda G: nx.number_connected_components(G), + lambda G: nx.assortativity.degree_assortativity_coefficient(G), + lambda G: np.sum(np.array(list(dict(nx.degree(G)).values())) ** 2) + / G.number_of_nodes(), + lambda G: np.min(np.array(list(dict(nx.degree(G)).values()))), + lambda G: np.max(np.array(list(dict(nx.degree(G)).values()))), + lambda G: average_local_clustering(G), +] + +function_names = [ + "Number of nodes", + "Number of edges", + "Average shortest path length", + "Number of components", + "Degree correlation coefficient", + "Second moment of degree distribution", + "Minimum degree", + "Maximum degree", + "Average local clustering coefficient", +] + + +# test run +init_graph = nx.fast_gnp_random_graph(100, 0.03) +rewire_method = NetworkXEdgeSwap +tmax = 100 +numit = 10 +all_properties = various_properties_overtime( + init_graph, rewire_method, property_functions, function_names, tmax, numit +) + + +# test plot +for name in function_names: + fi, ax = plt.subplots(1, figsize=(5, 2), dpi=200) + plt.plot(range(tmax), np.mean(all_properties[name], axis=0)) + plt.title(name) + plt.xlabel("$t$") + plt.tight_layout() + plt.savefig("figures/" + name) diff --git a/netrw/rewire/__init__.py b/netrw/rewire/__init__.py index 8903638..6728242 100644 --- a/netrw/rewire/__init__.py +++ b/netrw/rewire/__init__.py @@ -1,11 +1,14 @@ +from .algebraic_connectivity import AlgebraicConnectivity +from .assortative import DegreeAssortativeRewirer from .base import BaseRewirer -from .karrer import KarrerRewirer from .global_rewiring import GlobalRewiring +from .karrer import KarrerRewirer from .local_edge_rewire import LocalEdgeRewiring -from .assortative import DegreeAssortativeRewirer -from .algebraic_connectivity import AlgebraicConnectivity -from .randomized_weights import RandomizedWeightCM_swap -from .randomized_weights import RandomizedWeightCM_redistribution +from .networkXEdgeSwap import NetworkXEdgeSwap +from .randomized_weights import ( + RandomizedWeightCM_redistribution, + RandomizedWeightCM_swap, +) from .robust_rewiring import RobustRewirer from .spatial_small_worlds import SpatialSmallWorld diff --git a/netrw/rewire/algebraic_connectivity.py b/netrw/rewire/algebraic_connectivity.py index e741a4e..081431f 100644 --- a/netrw/rewire/algebraic_connectivity.py +++ b/netrw/rewire/algebraic_connectivity.py @@ -1,9 +1,11 @@ -from .base import BaseRewirer +import copy +import warnings + import networkx as nx import numpy as np -import copy from scipy import linalg as la -import warnings + +from .base import BaseRewirer class AlgebraicConnectivity(BaseRewirer): diff --git a/netrw/rewire/assortative.py b/netrw/rewire/assortative.py index ffede69..722b96b 100644 --- a/netrw/rewire/assortative.py +++ b/netrw/rewire/assortative.py @@ -1,16 +1,18 @@ -from . import BaseRewirer import copy import random + import networkx as nx import numpy as np +from . import BaseRewirer + class DegreeAssortativeRewirer(BaseRewirer): """ - Do degree-preserving rewiring that increases/decreases assortativity - as described in CHANGING CORRELATIONS IN NETWORKS: ASSORTATIVITY AND DISSORTATIVITY, - R. Xulvi-Brunet and I.M. Sokolov + Do degree-preserving rewiring that increases/decreases assortativity + as described in CHANGING CORRELATIONS IN NETWORKS: ASSORTATIVITY AND DISSORTATIVITY, + R. Xulvi-Brunet and I.M. Sokolov """ @@ -70,7 +72,9 @@ def step_rewire(self, G, p=0.5, assortative=True, copy_graph=True, verbose=False else: return G - def full_rewire(self, G, timesteps=1000, p=0.5, assortative=True, copy_graph=True, verbose=False): + def full_rewire( + self, G, timesteps=1000, p=0.5, assortative=True, copy_graph=True, verbose=False + ): """ Runs step_rewire for a number of steps (default 1000 for no reason) """ @@ -78,11 +82,15 @@ def full_rewire(self, G, timesteps=1000, p=0.5, assortative=True, copy_graph=Tru removed_edges = {} added_edges = {} for t in range(timesteps): - G,removed,added = self.step_rewire(G, p=p, assortative=assortative, copy_graph=copy_graph,verbose=True) + G, removed, added = self.step_rewire( + G, p=p, assortative=assortative, copy_graph=copy_graph, verbose=True + ) removed_edges[t] = removed added_edges[t] = added else: for t in range(timesteps): - G = self.step_rewire(G, p=p, assortative=assortative, copy_graph=copy_graph) + G = self.step_rewire( + G, p=p, assortative=assortative, copy_graph=copy_graph + ) return G diff --git a/netrw/rewire/global_rewiring.py b/netrw/rewire/global_rewiring.py index ca43daa..85a4911 100644 --- a/netrw/rewire/global_rewiring.py +++ b/netrw/rewire/global_rewiring.py @@ -1,8 +1,9 @@ -from .base import BaseRewirer import copy import random import warnings +from .base import BaseRewirer + class GlobalRewiring(BaseRewirer): """ diff --git a/netrw/rewire/karrer.py b/netrw/rewire/karrer.py index 1def5a5..bd23e2e 100644 --- a/netrw/rewire/karrer.py +++ b/netrw/rewire/karrer.py @@ -1,8 +1,10 @@ -from . import BaseRewirer import copy + import networkx as nx import numpy as np +from . import BaseRewirer + class KarrerRewirer(BaseRewirer): """Perturb the graph in the way described by Karrer et al. (2008). diff --git a/netrw/rewire/local_edge_rewire.py b/netrw/rewire/local_edge_rewire.py index 8afbf1a..edf07c2 100644 --- a/netrw/rewire/local_edge_rewire.py +++ b/netrw/rewire/local_edge_rewire.py @@ -1,9 +1,11 @@ -from . import BaseRewirer import copy import random + import networkx as nx import numpy as np +from . import BaseRewirer + class LocalEdgeRewiring(BaseRewirer): """Perturb one edge of node `i` in the way described by Klein & McCabe diff --git a/netrw/rewire/networkXEdgeSwap.py b/netrw/rewire/networkXEdgeSwap.py new file mode 100644 index 0000000..c9818a9 --- /dev/null +++ b/netrw/rewire/networkXEdgeSwap.py @@ -0,0 +1,39 @@ +import copy +import itertools as it +import random + +import networkx as nx + +from . import BaseRewirer + + +class NetworkXEdgeSwap(BaseRewirer): + """Perturb one edge of node `i` in the way described by Karrer et al. (2008). + Choose an edge incident on `i` at random; delete it and replace it with + a new edge that is not already present in the network (and is not + necessarily incident on `i`). + + Karrer, Brian, Elizaveta Levina, and + M. E. J. Newman. 2008. “Robustness of Community Structure in + Networks.” Physical Review E 77 + (4). https://doi.org/10.1103/PhysRevE.77.046119. + + """ + + def full_rewire(self, G, timesteps=1000, copy_graph=True): + + if copy_graph: + G = copy.deepcopy(G) + + nx.double_edge_swap(G, nswap=timesteps) + + return G + + def step_rewire(self, G, copy_graph=True): + + if copy_graph: + G = copy.deepcopy(G) + + nx.double_edge_swap(G, nswap=1) + + return G diff --git a/netrw/rewire/randomized_weights.py b/netrw/rewire/randomized_weights.py index cf61695..1945da5 100644 --- a/netrw/rewire/randomized_weights.py +++ b/netrw/rewire/randomized_weights.py @@ -1,10 +1,12 @@ -from .base import BaseRewirer import copy import itertools as it import random + import networkx as nx import numpy as np +from .base import BaseRewirer + class RandomizedWeightCM_swap(BaseRewirer): """ diff --git a/netrw/rewire/robust_rewiring.py b/netrw/rewire/robust_rewiring.py index e01b41a..079482f 100644 --- a/netrw/rewire/robust_rewiring.py +++ b/netrw/rewire/robust_rewiring.py @@ -1,15 +1,14 @@ +import copy +import random +import warnings +from operator import itemgetter import networkx as nx import numpy as np -from operator import itemgetter -import random -import copy + from .base import BaseRewirer -​ -​ -# In[14]: -​ -​ + + class RobustRewirer(BaseRewirer): """ Increases network robustness by building triangles around high degree nodes following algorithm described in: @@ -17,7 +16,10 @@ class RobustRewirer(BaseRewirer): * full_rewire rewires the graph N times """ - def step_rewire(self,G,copy_graph=False,timesteps=1,directed=False,verbose=False): + + def step_rewire( + self, G, copy_graph=False, timesteps=1, directed=False, verbose=False + ): if copy_graph: G = copy.deepcopy(G) @@ -32,54 +34,62 @@ def step_rewire(self,G,copy_graph=False,timesteps=1,directed=False,verbose=False added_edges = {} for t in range(timesteps): -​ A = nx.adjacency_matrix(G) degree_list = G.degree -​ + neighbors = [] for i in range(len(degree_list)): - sorted_degrees = sorted(list(degree_list(np.nonzero(A[i,:])[1])),key=itemgetter(1)) + sorted_degrees = sorted( + list(degree_list(np.nonzero(A[i, :])[1])), key=itemgetter(1) + ) if len(sorted_degrees) > 1: if sorted_degrees[-2][1] > 1 and sorted_degrees[-1][1] > 1: neighbors.append(i) -​ - index_i = neighbors[random.randint(0,len(neighbors)-1)] - sorted_degrees_i = sorted(list(degree_list(np.nonzero(A[index_i,:])[1])),key=itemgetter(1)) -​ + index_i = neighbors[random.randint(0, len(neighbors) - 1)] + sorted_degrees_i = sorted( + list(degree_list(np.nonzero(A[index_i, :])[1])), key=itemgetter(1) + ) + min_degree = sorted_degrees_i[0][1] max_degree = sorted_degrees_i[-1][1] -​ + j = [] k = [] -​ + for item in sorted_degrees_i: if item[1] == min_degree: j.append(item[0]) if item[1] == max_degree: k.append(item[0]) -​ - index_j = j[random.randint(0,len(j)-1)] - index_k = k[random.randint(0,len(k)-1)] -​ - m = sorted(list(degree_list(np.nonzero(A[index_j,:])[1])),key=itemgetter(1)) - n = sorted(list(degree_list(np.nonzero(A[index_k,:])[1])),key=itemgetter(1)) -​ - index_m = m[random.randint(0,len(m)-1)][0] - index_n = n[random.randint(0,len(n)-1)][0] -​ - if len(np.unique([index_i,index_j,index_k,index_m,index_n])) == 5: - G.remove_edge(index_j,index_m) - G.remove_edge(index_k,index_n) - G.add_edge(index_k,index_j) - G.add_edge(index_m,index_n) -​ + + index_j = j[random.randint(0, len(j) - 1)] + index_k = k[random.randint(0, len(k) - 1)] + + m = sorted( + list(degree_list(np.nonzero(A[index_j, :])[1])), key=itemgetter(1) + ) + n = sorted( + list(degree_list(np.nonzero(A[index_k, :])[1])), key=itemgetter(1) + ) + + index_m = m[random.randint(0, len(m) - 1)][0] + index_n = n[random.randint(0, len(n) - 1)][0] + + if len(np.unique([index_i, index_j, index_k, index_m, index_n])) == 5: + G.remove_edge(index_j, index_m) + G.remove_edge(index_k, index_n) + G.add_edge(index_k, index_j) + G.add_edge(index_m, index_n) + if verbose: return G, removed_edges, added_edges else: return G - def full_rewire(self,G,copy_graph=False,timesteps=-1,directed=False,verbose=False): + def full_rewire( + self, G, copy_graph=True, timesteps=-1, directed=False, verbose=False + ): if timesteps == -1: timesteps = int(len(G.nodes())) - G = self.step_rewire(G,copy_graph,timesteps,directed,verbose) + G = self.step_rewire(G, copy_graph, timesteps, directed, verbose) return G diff --git a/netrw/rewire/spatial_small_worlds.py b/netrw/rewire/spatial_small_worlds.py index e0354f2..0602748 100644 --- a/netrw/rewire/spatial_small_worlds.py +++ b/netrw/rewire/spatial_small_worlds.py @@ -1,7 +1,9 @@ +import copy +import random + import networkx as nx import numpy as np -import random -import copy + from .base import BaseRewirer @@ -22,7 +24,20 @@ class SpatialSmallWorld(BaseRewirer): """ - def step_rewire(self,G,p,dim,alpha,copy_graph=False,is_periodic=True,does_remove=True,manhattan_dist=True,timesteps=1,directed=False,verbose=False): + def step_rewire( + self, + G, + p, + dim, + alpha, + copy_graph=False, + is_periodic=True, + does_remove=True, + manhattan_dist=True, + timesteps=1, + directed=False, + verbose=False, + ): if copy_graph: G = copy.deepcopy(G) if nx.is_directed(G) and directed is True: @@ -44,75 +59,153 @@ def step_rewire(self,G,p,dim,alpha,copy_graph=False,is_periodic=True,does_remove if dimsize == 3: non_edge_list = list(nx.Graph(nx.non_edges(G)).edges()) if not is_periodic: - edge_p = [((edge_pair[0][0]-edge_pair[1][0])**2+(edge_pair[0][1]-edge_pair[1][1])**2+(edge_pair[0][2]-edge_pair[1][2])**2)**(1/2) for edge_pair in non_edge_list] + edge_p = [ + ( + (edge_pair[0][0] - edge_pair[1][0]) ** 2 + + (edge_pair[0][1] - edge_pair[1][1]) ** 2 + + (edge_pair[0][2] - edge_pair[1][2]) ** 2 + ) + ** (1 / 2) + for edge_pair in non_edge_list + ] unique_lengths = np.unique(edge_p) else: - edge_p = [(abs(edge_pair[0][0]-edge_pair[1][0]),abs(edge_pair[0][1]-edge_pair[1][1]),abs(edge_pair[0][2]-edge_pair[1][2])) for edge_pair in non_edge_list] - edge_p = [((dim[2] - dists[0]) if dists[0] > (dim[2])/2 else dists[0] , (dim[1] - dists[1]) if dists[1] > (dim[1])/2 else dists[1] , (dim[0] - dists[2]) if dists[2] > (dim[0])/2 else dists[2]) for dists in edge_p] + edge_p = [ + ( + abs(edge_pair[0][0] - edge_pair[1][0]), + abs(edge_pair[0][1] - edge_pair[1][1]), + abs(edge_pair[0][2] - edge_pair[1][2]), + ) + for edge_pair in non_edge_list + ] + edge_p = [ + ( + (dim[2] - dists[0]) + if dists[0] > (dim[2]) / 2 + else dists[0], + (dim[1] - dists[1]) + if dists[1] > (dim[1]) / 2 + else dists[1], + (dim[0] - dists[2]) + if dists[2] > (dim[0]) / 2 + else dists[2], + ) + for dists in edge_p + ] if manhattan_dist: edge_p = [(dists[0] + dists[1] + dists[2]) for dists in edge_p] else: - edge_p = [(dists[0]**2 + dists[1]**2 + dists[2]**2)**(1/2) for dists in edge_p] + edge_p = [ + (dists[0] ** 2 + dists[1] ** 2 + dists[2] ** 2) ** (1 / 2) + for dists in edge_p + ] unique_lengths = np.unique(edge_p) randomVal = random.choices( - unique_lengths, weights=(1 / np.power(unique_lengths,(alpha))), k=1) + unique_lengths, weights=(1 / np.power(unique_lengths, (alpha))), k=1 + ) indices = list(np.where(np.array(edge_p) == randomVal)[0]) - randomList = random.choices( - [non_edge_list[i] for i in indices], k=1) + randomList = random.choices([non_edge_list[i] for i in indices], k=1) edge_list = list(G.edges()) - rand_edge = edge_list[random.randint(0,len(edge_list)-1)] + rand_edge = edge_list[random.randint(0, len(edge_list) - 1)] if does_remove: - G.remove_edge(rand_edge[0],rand_edge[1]) - G.add_edge(randomList[0][0],randomList[0][1]) + G.remove_edge(rand_edge[0], rand_edge[1]) + G.add_edge(randomList[0][0], randomList[0][1]) elif dimsize == 2: non_edge_list = list(nx.Graph(nx.non_edges(G)).edges()) if not is_periodic: - edge_p = [((edge_pair[0][0]-edge_pair[1][0])**2+(edge_pair[0][1]-edge_pair[1][1])**2)**(1/2) for edge_pair in non_edge_list] + edge_p = [ + ( + (edge_pair[0][0] - edge_pair[1][0]) ** 2 + + (edge_pair[0][1] - edge_pair[1][1]) ** 2 + ) + ** (1 / 2) + for edge_pair in non_edge_list + ] unique_lengths = np.unique(edge_p) else: - edge_p = [(abs(edge_pair[0][0]-edge_pair[1][0]),abs(edge_pair[0][1]-edge_pair[1][1])) for edge_pair in non_edge_list] - edge_p = [(dim[1] - dists[0] if dists[0] > (dim[1])/2 else dists[0] , dim[0] - dists[1] if dists[1] > (dim[0])/2 else dists[1]) for dists in edge_p] + edge_p = [ + ( + abs(edge_pair[0][0] - edge_pair[1][0]), + abs(edge_pair[0][1] - edge_pair[1][1]), + ) + for edge_pair in non_edge_list + ] + edge_p = [ + ( + dim[1] - dists[0] if dists[0] > (dim[1]) / 2 else dists[0], + dim[0] - dists[1] if dists[1] > (dim[0]) / 2 else dists[1], + ) + for dists in edge_p + ] if manhattan_dist: edge_p = [(dists[0] + dists[1]) for dists in edge_p] else: - edge_p = [(dists[0]**2 + dists[1]**2)**(1/2) for dists in edge_p] + edge_p = [ + (dists[0] ** 2 + dists[1] ** 2) ** (1 / 2) + for dists in edge_p + ] unique_lengths = np.unique(edge_p) randomVal = random.choices( - unique_lengths, weights=(1 / np.power(unique_lengths,(alpha))), k=1) + unique_lengths, weights=(1 / np.power(unique_lengths, (alpha))), k=1 + ) indices = list(np.where(np.array(edge_p) == randomVal)[0]) - randomList = random.choices( - [non_edge_list[i] for i in indices], k=1) + randomList = random.choices([non_edge_list[i] for i in indices], k=1) edge_list = list(G.edges()) - rand_edge = edge_list[random.randint(0,len(edge_list)-1)] + rand_edge = edge_list[random.randint(0, len(edge_list) - 1)] if does_remove: - G.remove_edge(rand_edge[0],rand_edge[1]) - G.add_edge(randomList[0][0],randomList[0][1]) + G.remove_edge(rand_edge[0], rand_edge[1]) + G.add_edge(randomList[0][0], randomList[0][1]) if verbose: if does_remove: - removed_edges[t] = [rand_edge[0],rand_edge[1]] - added_edges[t] = [randomList[0][0],randomList[0][1]] + removed_edges[t] = [rand_edge[0], rand_edge[1]] + added_edges[t] = [randomList[0][0], randomList[0][1]] if verbose: return G, removed_edges, added_edges else: return G - def full_rewire(self,G,p,dim,alpha,copy_graph=False,is_periodic=True,does_remove=True,manhattan_dist=True,timesteps=-1,directed=False,verbose=False): + def full_rewire( + self, + G, + p, + dim, + alpha, + copy_graph=False, + is_periodic=True, + does_remove=True, + manhattan_dist=True, + timesteps=-1, + directed=False, + verbose=False, + ): if timesteps == -1: - timesteps = int(p*len(G.nodes())) - G = self.step_rewire(G,p,dim,alpha,copy_graph,is_periodic,does_remove,manhattan_dist,timesteps,directed,verbose) + timesteps = int(p * len(G.nodes())) + G = self.step_rewire( + G, + p, + dim, + alpha, + copy_graph, + is_periodic, + does_remove, + manhattan_dist, + timesteps, + directed, + verbose, + ) return G - def initialize_graph(self,dim): + def initialize_graph(self, dim): if len(dim) == 3: dimsize = 3 elif len(dim) == 2: dimsize = 2 else: raise ValueError("Lattice Dimension is not 2-3") - G = nx.grid_graph(dim=dim,periodic=False) + G = nx.grid_graph(dim=dim, periodic=False) return G - def plot(self,G,dim): + def plot(self, G, dim): if len(dim) == 3: dimsize = 3 elif len(dim) == 2: @@ -120,7 +213,7 @@ def plot(self,G,dim): else: raise ValueError("Lattice Dimension is not 2-3") if dimsize == 3: - pos = {(x,y,z):(x+5*z/7,y+5*z/7) for x,y,z in G.nodes()} + pos = {(x, y, z): (x + 5 * z / 7, y + 5 * z / 7) for x, y, z in G.nodes()} elif dimsize == 2: - pos = {(x,y):(x,y) for x,y in G.nodes()} - nx.draw(G,pos) + pos = {(x, y): (x, y) for x, y in G.nodes()} + nx.draw(G, pos) diff --git a/netrw/visualization/visualization.py b/netrw/visualization/visualization.py new file mode 100644 index 0000000..48c5996 --- /dev/null +++ b/netrw/visualization/visualization.py @@ -0,0 +1,16 @@ +import matplotlib.pyplot as plt +import networkx as nx +import numpy as np + + +def visualize_rewiring(G1, G2, pos): + A1 = nx.adjacency_matrix(G1) + A2 = nx.adjacency_matrix(G2) + A_dif = abs(A2 - A1) + G3 = nx.Graph(A_dif) + nx.draw(G3, pos, edge_color="r", node_color="b", node_size=0, width=8) + nx.draw(G2, pos, edge_color="b", node_color="b", node_size=80, width=5) + + +def visualize_graph(G, pos): + nx.draw(G, pos, edge_color="b", node_color="b", node_size=80, width=5) diff --git a/netrw/visualization/visualize_example_full_rewire.py b/netrw/visualization/visualize_example_full_rewire.py index 5e58b4e..371c247 100644 --- a/netrw/visualization/visualize_example_full_rewire.py +++ b/netrw/visualization/visualize_example_full_rewire.py @@ -1,16 +1,20 @@ import matplotlib.pyplot as plt -plt.rcParams['figure.facecolor'] = 'white' -plt.rcParams['axes.facecolor'] = 'white' -plt.rcParams['savefig.facecolor'] = 'white' -plt.rc('axes', axisbelow=True) - -def visualize_example_full_rewire(RewiringTechnique=LocalEdgeRewiring, - rewiring_technique_label='Local edge rewire', - list_of_graphs=[], - timesteps=100, - save_fig=False, - save_fig_folder='', - save_fig_filename=''): + +plt.rcParams["figure.facecolor"] = "white" +plt.rcParams["axes.facecolor"] = "white" +plt.rcParams["savefig.facecolor"] = "white" +plt.rc("axes", axisbelow=True) + + +def visualize_example_full_rewire( + RewiringTechnique=LocalEdgeRewiring, + rewiring_technique_label="Local edge rewire", + list_of_graphs=[], + timesteps=100, + save_fig=False, + save_fig_folder="", + save_fig_filename="", +): """ This is a useful function for visualizing outputs from repeated runs of `step_rewire`. Users can use this to get a sense of what is happening @@ -45,30 +49,43 @@ def visualize_example_full_rewire(RewiringTechnique=LocalEdgeRewiring, """ if list_of_graphs == []: - list_of_graphs = [nx.karate_club_graph(), - nx.ring_of_cliques(4, 16), - nx.random_geometric_graph(50, 0.2), - nx.erdos_renyi_graph(50, 0.05), - nx.erdos_renyi_graph(50, 0.30), - nx.barabasi_albert_graph(50, 2), - ] + list_of_graphs = [ + nx.karate_club_graph(), + nx.ring_of_cliques(4, 16), + nx.random_geometric_graph(50, 0.2), + nx.erdos_renyi_graph(50, 0.05), + nx.erdos_renyi_graph(50, 0.30), + nx.barabasi_albert_graph(50, 2), + ] # example params for node sizes, edge widths, etc. - ns = 100; lw = 2; ew = 2.5; n_ec = '.3' + ns = 100 + lw = 2 + ew = 2.5 + n_ec = ".3" # fig width and height - base_width = 5; base_height = 5 - - fig, ax = plt.subplots(len(list_of_graphs),2, - figsize=(base_width*2, - base_height*len(list_of_graphs)), - dpi=100) - - ax[(0,0)].text(1.1, 1.2, "Method: "+rewiring_technique_label, - ha='center', va='center', transform=ax[(0,0)].transAxes, - fontsize='xx-large') - - for ix,G0 in enumerate(list_of_graphs): + base_width = 5 + base_height = 5 + + fig, ax = plt.subplots( + len(list_of_graphs), + 2, + figsize=(base_width * 2, base_height * len(list_of_graphs)), + dpi=100, + ) + + ax[(0, 0)].text( + 1.1, + 1.2, + "Method: " + rewiring_technique_label, + ha="center", + va="center", + transform=ax[(0, 0)].transAxes, + fontsize="xx-large", + ) + + for ix, G0 in enumerate(list_of_graphs): pos = nx.kamada_kawai_layout(G0) G = G0.copy() @@ -77,31 +94,47 @@ def visualize_example_full_rewire(RewiringTechnique=LocalEdgeRewiring, G = RewiringTechnique().step_rewire(G) # draw original network - nx.draw_networkx_nodes(G0, pos, ax=ax[(ix,0)], node_size=ns, - node_color='w', edgecolors=n_ec, linewidths=lw) - nx.draw_networkx_edges(G0, pos, ax=ax[(ix,0)], edge_color='.5', - width=ew, alpha=0.35) + nx.draw_networkx_nodes( + G0, + pos, + ax=ax[(ix, 0)], + node_size=ns, + node_color="w", + edgecolors=n_ec, + linewidths=lw, + ) + nx.draw_networkx_edges( + G0, pos, ax=ax[(ix, 0)], edge_color=".5", width=ew, alpha=0.35 + ) # draw rewired network - nx.draw_networkx_nodes(G, pos, ax=ax[(ix,1)], node_size=ns, - node_color='w', edgecolors=n_ec, linewidths=lw) - nx.draw_networkx_edges(G, pos, ax=ax[(ix,1)], edge_color='.5', - width=ew, alpha=0.35) - - ax[(ix,0)].set_title('Original network') - ax[(ix,1)].set_title('Rewired network (n=%i timesteps)'%timesteps) + nx.draw_networkx_nodes( + G, + pos, + ax=ax[(ix, 1)], + node_size=ns, + node_color="w", + edgecolors=n_ec, + linewidths=lw, + ) + nx.draw_networkx_edges( + G, pos, ax=ax[(ix, 1)], edge_color=".5", width=ew, alpha=0.35 + ) + + ax[(ix, 0)].set_title("Original network") + ax[(ix, 1)].set_title("Rewired network (n=%i timesteps)" % timesteps) if save_fig: - if save_fig_filename=='': - save_fig_filename = rewiring_technique_label.lower().replace(' ','_') + if save_fig_filename == "": + save_fig_filename = rewiring_technique_label.lower().replace(" ", "_") - if save_fig_filename[-4:] != '.png' and save_fig_filename[-4:] != '.pdf': - save_fig_filename = save_fig_filename+'.png' + if save_fig_filename[-4:] != ".png" and save_fig_filename[-4:] != ".pdf": + save_fig_filename = save_fig_filename + ".png" fn = save_fig_folder + save_fig_filename print(fn) - plt.savefig(fn, dpi=300, bbox_inches='tight') + plt.savefig(fn, dpi=300, bbox_inches="tight") plt.close() - + else: plt.show() diff --git a/requirements.txt b/requirements.txt index 993f135..5993c77 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ networkx>=2.0.0 numpy>=1.10.0 scipy>=1.0.0 -matplotlib>3.3.2 +matplotlib>=3.3.2 +netrd>=0.2 diff --git a/setup.py b/setup.py index 058a530..fd99d53 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,16 @@ import setuptools setuptools.setup( - name='netrw', - version='0.0.1', - author='NetSI 2022 Collabathon Team', - author_email='b.klein@northeastern.edu', - description='Repository of network rewiring methods', - url='https://github.com/netsiphd/netrw', + name="netrw", + version="0.0.1", + author="NetSI 2022 Collabathon Team", + author_email="b.klein@northeastern.edu", + description="Repository of network rewiring methods", + url="https://github.com/netsiphd/netrw", packages=setuptools.find_packages(), - classifiers=['Programming Language :: Python :: 3', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent'] + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], ) - diff --git a/tests/test_karrer.py b/tests/test_karrer.py index 77ce7a9..9648163 100644 --- a/tests/test_karrer.py +++ b/tests/test_karrer.py @@ -1,5 +1,6 @@ import networkx as nx import numpy as np + from netrw.rewire import KarrerRewirer @@ -19,4 +20,4 @@ def test_same_return_type(): avg_degree /= iterations - assert np.linalg.norm(original_degree - avg_degree) < 1 \ No newline at end of file + assert np.linalg.norm(original_degree - avg_degree) < 1