diff --git a/README.md b/README.md index 9339c7e..ac9a86c 100644 --- a/README.md +++ b/README.md @@ -17,22 +17,68 @@ * ---------------------- -This is multired-0.1. +This is multired-0.2 - -This is a Python implmementation of the algorithm for structural -reduction of multi-layer networks based on the Von Neumann Entropy -and on the Quantum Jensen-Shannon divergence of graphs, as explained -in: +This is a Python implementation of the algorithm for structural reduction of multi-layer networks based on the Von Neumann Entropy and on the Quantum Jensen-Shannon divergence of graphs, as explained in: M. De Domenico. V. Nicosia, A. Arenas, V. Latora "Structural reducibility of multilayer networks", Nat. Commun. 6, 6864 (2015) doi:10.1038/ncomms7864 -If you happen to find any use of this code please do not forget to -cite that paper ;-) +If you happen to find any use of this code please do not forget to cite that paper ;-) + + +-------------------- + INFO +-------------------- + +The package "multired" provides the class "MultiplexRed", which +implements the algorithm to reduce a multilayer network described in +the paper cited above. + +In order to use it, you just need to + + from multired import MultiplexRed + m = MultiplexRed(layer_list_filename, verbose=True) + part = m.compute_partitions() + m.dump_partitions() + m.draw_dendrogram() + +where ``layer_list_filename`` is a text file containing the list of the files describing the multiplex layers (see example in multired/sample_data/file_list) + +The constructor requires as its first argument the path of a file which in turn contains a list of files (one for each line) where the graph of each layer is to be found. + +The class provides one set of methods which perform the exact evaluation of the Von Neumann entropy, and another set of methods (those whose name end with the suffix "_approx") which rely on a polynomial approximation of the Von Neumann entropy. By default the approximation is based on a 10th order polynomial fit of x log(x) in [0,1], but the order of the polynomial can be set through the parameter "fit_degree" of the constructor. + +Several sample scripts can be found in the "test/" directory. You also find a sample data set in the folder "sample_data/". +That is the 4-layer Noordin Top Terrorist multiplex network, originally provided in: + + N. Roberts, S. F. Everton, Roberts and Everton "Terrorist + Data: Noordin Top Terrorist Network" (Subset) (2011). + +and extensively studied in: + + F. Battiston, V. Nicosia, V. Latora, + "Structural measures for multiplex networks", + Phys. Rev. E 89, 032804 (2014). + +Please consider citing those papers if you use that data set in a +scientific work. + +-------------------- + DEPENDENCIES +-------------------- + +The dependencies are listed in requirements.txt + +The module has been tested on a Debian GNU/Linux system, using: + + - Python 3.x, + - SciPy + - Numpy + - matplotlib -My plan is to provide also a C version in the future, but I cannot -guarantee that I will do so anytime soon. +but it will almost surely work on other platforms and/or with other versions of those packages. +If you would like to report a working configuration, just email me (the address is at the beginning of this file). diff --git a/multired/__init__.py b/multired/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/multired.py b/multired/mred.py similarity index 70% rename from python/multired.py rename to multired/mred.py index bf2c392..95391db 100644 --- a/python/multired.py +++ b/multired/mred.py @@ -1,65 +1,68 @@ # # -# multired.py +# multired.py +# # -# # Copyright (C) 2015 Vincenzo (Enzo) Nicosia -# -# +# +# # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# +# (at your option) any later version. +# # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # long with this program. If not, see . -# -# +# +# # # This module provides the class multiplex_red, which implements the # algorithm for the structural reduction of multi-layer networks # based on the Von Neumann entropy and Quantum Jensen-Shannon -# divergence of graphs. +# divergence of graphs. # # If you use this code please cite the paper: # -# M. De Domenico, V. Nicosia, A. Arenas, V. Latora, -# "Structural reducibility of multilayer networks" +# M. De Domenico, V. Nicosia, A. Arenas, V. Latora, +# "Structural reducibility of multilayer networks" # Nat. Commun. 6, 6864 (2015) doi:10.1038/ncomms7864 # # -------------------------------------------- -# -# -- 2015/04/23 -- release 0.1 -# -- 2015/05/11 -- release 0.1.1 -- removed the last full matrices -# - +# +# -- 2015/04/23 -- release 0.1 +# -- 2015/05/11 -- release 0.1.1 -- removed the last full matrices +# import sys -import math +import math import numpy as np from scipy.sparse import csr_matrix, eye -from scipy.linalg import eigh, eig +from scipy.linalg import eigh import copy +import os from scipy.cluster.hierarchy import linkage, dendrogram has_matplotlib = False try: import matplotlib + has_matplotlib = True except ImportError: has_matplotlib = False +__all__ = ["MultiplexRed", "Layer", "XlogxFit"] + -class XLogx_fit: - def __init__(self, degree, npoints= 100, xmax=1): +class XlogxFit(object): + def __init__(self, degree, npoints=100, xmax=1): if xmax > 1: xmax = 1 self.degree = degree @@ -68,17 +71,19 @@ def __init__(self, degree, npoints= 100, xmax=1): y.insert(0, 0) self.fit = np.polyfit(x, y, degree) - def __getitem__ (self, index): + def __getitem__(self, index): if index <= self.degree: return self.fit[index] else: - print "Error!!! Index %d is larger than the degree of the fitting polynomial (%d)" \ - % (index, degree) + print( + "Error!!! Index %d is larger than the degree of the fitting polynomial (%d)" + % (index, self.degree) + ) sys.exit(-1) -class layer: - def __init__ (self, layerfile= None, matrix=None): +class Layer(object): + def __init__(self, layerfile=None, matrix=None): self.N = 0 self.num_layer = -1 self.fname = layerfile @@ -91,28 +96,29 @@ def __init__ (self, layerfile= None, matrix=None): self._jj = [] self._ww = [] self._matrix_called = False - if layerfile != None: + + if layerfile is not None: try: min_N = 10e10 with open(layerfile, "r") as lines: for l in lines: - if l[0] == '#': + if l[0] == "#": continue elems = l.strip(" \n").split(" ") s = int(elems[0]) d = int(elems[1]) self._ii.append(s) self._jj.append(d) - #if s > self.N: - # self.N = s - #if d > self.N: - # self.N = d + if s < min_N: min_N = s - if d < min_N: + if d < min_N: min_N = d - if len(elems) >2 : ## A weight is specified - val = [float(x) if "e" in x or "." in x else int(x) for x in [elems[2]]][0] + if len(elems) > 2: # A weight is specified + val = [ + float(x) if "e" in x or "." in x else int(x) + for x in [elems[2]] + ][0] self._ww.append(float(val)) else: self._ww.append(int(1)) @@ -122,27 +128,31 @@ def __init__ (self, layerfile= None, matrix=None): self.N = m1 else: self.N = m2 - - except (IOError): - print "Unable to find/open file %s -- Exiting!!!" % layerfile + + except IOError: + print("Unable to find/open file %s -- Exiting!!!" % layerfile) exit(-2) - elif matrix != None: + + elif matrix is not None: self.adj_matr = copy.copy(matrix) - self.N, _x = matrix.shape + self.N, _x = matrix.shape K = self.adj_matr.sum(0).reshape((1, self.N)).tolist()[0] - D = csr_matrix((K, (range(self.N), range(self.N)) ), shape=(self.N, self.N)) + D = csr_matrix((K, (range(self.N), range(self.N))), shape=(self.N, self.N)) self.laplacian = csr_matrix(D - self.adj_matr) K = self.laplacian.diagonal().sum() self.resc_laplacian = csr_matrix(self.laplacian / K) self._matrix_called = True else: - print "The given matrix is BLANK" + print("The given matrix is BLANK") + def make_matrices(self, N): - self.N = N - self.adj_matr = csr_matrix((self._ww, (self._ii, self._jj)), shape=(self.N, self.N)) + self.N = N + self.adj_matr = csr_matrix( + (self._ww, (self._ii, self._jj)), shape=(self.N, self.N) + ) self.adj_matr = self.adj_matr + self.adj_matr.transpose() K = self.adj_matr.sum(0).reshape((1, self.N)).tolist()[0] - D = csr_matrix((K, (range(self.N), range(self.N)) ), shape=(self.N, self.N)) + D = csr_matrix((K, (range(self.N), range(self.N))), shape=(self.N, self.N)) self.laplacian = csr_matrix(D - self.adj_matr) K = self.laplacian.diagonal().sum() self.resc_laplacian = csr_matrix(self.laplacian / K) @@ -151,45 +161,46 @@ def make_matrices(self, N): def dump_info(self): N, M = self.adj_matr.shape K = self.adj_matr.nnz - sys.stderr.write("Layer File: %s\nNodes: %d Edges: %d\nEntropy: %g Approx. Entropy: %g\n" % \ - (self.fname, N, K, self.entropy, self.entropy_approx) ) + sys.stderr.write( + "Layer File: %s\nNodes: %d Edges: %d\nEntropy: %g Approx. Entropy: %g\n" + % (self.fname, N, K, self.entropy, self.entropy_approx) + ) def compute_VN_entropy(self): eigvals = eigh(self.resc_laplacian.todense()) self.entropy = 0 for l_i in eigvals[0]: - if (l_i > 10e-20): - self.entropy -= l_i * math.log (l_i) - + if l_i > 10e-20: + self.entropy -= l_i * math.log(l_i) def compute_VN_entropy_approx(self, poly): p = poly.degree - h = - poly[p] * self.N + h = -poly[p] * self.N M = csr_matrix(eye(self.N)) - for i in range(p-1, -1, -1): - M = M * self.resc_laplacian - h += - poly[i] * sum(M.diagonal()) + for i in range(p - 1, -1, -1): + M = M * self.resc_laplacian + h += -poly[i] * sum(M.diagonal()) self.entropy_approx = h def aggregate(self, other_layer): - if self.adj_matr != None: + if self.adj_matr is not None: self.adj_matr = self.adj_matr + other_layer.adj_matr else: self.adj_matr = copy.copy(other_layer.adj_matr) K = self.adj_matr.sum(0).reshape((1, self.N)).tolist()[0] - D = csr_matrix((K, (range(self.N), range(self.N)) ), shape=(self.N, self.N)) + D = csr_matrix((K, (range(self.N), range(self.N))), shape=(self.N, self.N)) self.laplacian = csr_matrix(D - self.adj_matr) K = self.laplacian.diagonal().sum() self.resc_laplacian = csr_matrix(self.laplacian / K) self._matrix_called = True def dump_laplacian(self): - print self.laplacian + print(self.laplacian) + -class multiplex_red: - - def __init__ (self, multiplexfile, directed = None, fit_degree=10, verbose=False): +class MultiplexRed(object): + def __init__(self, multiplexfile, directed=None, fit_degree=10, verbose=False): self.layers = [] self.N = 0 self.M = 0 @@ -203,43 +214,49 @@ def __init__ (self, multiplexfile, directed = None, fit_degree=10, verbose=False self.q_vals = None self.q_vals_approx = None self.fit_degree = fit_degree - self.poly = XLogx_fit(self.fit_degree) + self.poly = XlogxFit(self.fit_degree) self.verb = verbose self.cuts = None self.cuts_approx = None try: with open(multiplexfile, "r") as lines: + + wkspFldr = os.path.dirname(multiplexfile) for l in lines: - if (self.verb): - sys.stderr.write("Loading layer %d from file %s" % (len(self.layers), l)) - A = layer(l.strip(" \n")) - #if A.N > self.N: - # self.N = A.N+1 + if wkspFldr not in l: + l = wkspFldr + "/" + l + if self.verb: + sys.stderr.write( + "Loading layer %d from file %s" % (len(self.layers), l) + ) + A = Layer(l.strip(" \n")) self.layers.append(A) n = 0 N = max([x.N for x in self.layers]) self.N = N + 1 + for l in self.layers: l.make_matrices(self.N) l.num_layer = n n += 1 self.M = len(self.layers) - except ( IOError): - print "Unable to find/open file %s -- Exiting!!!" % layer_file + except IOError: + print("Unable to find/open file -- Exiting!!!") exit(-2) def dump_info(self): i = 0 + for l in self.layers: sys.stderr.write("--------\nLayer: %d\n" % i) l.dump_info() i += 1 - def compute_aggregated(self): self.aggr = copy.copy(self.layers[0]) self.aggr.entropy = 0 self.aggr.entropy_approx = 0 + for l in self.layers[1:]: self.aggr.aggregate(l) @@ -251,37 +268,36 @@ def compute_layer_entropies_approx(self): for l in self.layers: l.compute_VN_entropy_approx(self.poly) - def compute_multiplex_entropy(self, force_compute=False): - ### The entropy of a multiplex is defined as the sum of the entropies of its layers + # The entropy of a multiplex is defined as the sum of the entropies of its layers for l in self.layers: - if l.entropy == None: + if l.entropy is None: l.compute_VN_entropy() self.entropy += l.entropy def compute_multiplex_entropy_approx(self, force_compute=False): - ### The entropy of a multiplex is defined as the sum of the entropies of its layers + # The entropy of a multiplex is defined as the sum of the entropies of its layers for l in self.layers: - if l.entropy_approx == None: + if l.entropy_approx is None: l.compute_VN_entropy_approx(self.poly) self.entropy_approx += l.entropy_approx def compute_JSD_matrix(self): - if (self.verb): + if self.verb: sys.stderr.write("Computing JSD matrix\n") self.JSD = np.zeros((self.M, self.M)) + for i in range(len(self.layers)): - for j in range(i+1, len(self.layers)): + for j in range(i + 1, len(self.layers)): li = self.layers[i] lj = self.layers[j] if not li.entropy: li.compute_VN_entropy() if not lj.entropy: lj.compute_VN_entropy() - # m_sigma = (li.resc_laplacian + lj.resc_laplacian)/2.0 - # m_sigma_entropy = mr.compute_VN_entropy_LR(m_sigma) - m_sigma_matr = (li.adj_matr + lj.adj_matr)/2.0 - m_sigma = layer(matrix=m_sigma_matr) + + m_sigma_matr = (li.adj_matr + lj.adj_matr) / 2.0 + m_sigma = Layer(matrix=m_sigma_matr) m_sigma.compute_VN_entropy() d = m_sigma.entropy - 0.5 * (li.entropy + lj.entropy) d = math.sqrt(d) @@ -290,68 +306,76 @@ def compute_JSD_matrix(self): pass def compute_JSD_matrix_approx(self): - if (self.verb): + if self.verb: sys.stderr.write("Computing JSD matrix (approx)\n") self.JSD_approx = np.zeros((self.M, self.M)) + for i in range(len(self.layers)): - for j in range(i+1, len(self.layers)): + for j in range(i + 1, len(self.layers)): li = self.layers[i] lj = self.layers[j] if not li.entropy_approx: li.compute_VN_entropy_approx(self.poly) if not lj.entropy_approx: lj.compute_VN_entropy_approx(self.poly) - m_sigma_matr = (li.adj_matr + lj.adj_matr)/2.0 - m_sigma = layer(matrix=m_sigma_matr) + m_sigma_matr = (li.adj_matr + lj.adj_matr) / 2.0 + m_sigma = Layer(matrix=m_sigma_matr) m_sigma.compute_VN_entropy_approx(self.poly) - d = m_sigma.entropy_approx - 0.5 * (li.entropy_approx + lj.entropy_approx) + d = m_sigma.entropy_approx - 0.5 * ( + li.entropy_approx + lj.entropy_approx + ) d = math.sqrt(d) self.JSD_approx[i][j] = d self.JSD_approx[j][i] = d - + def dump_JSD(self, force_compute=False): - if self.JSD == None: + if self.JSD is None: if force_compute: self.compute_JSD_matrix() else: - print "Error!!! call to dump_JSD but JSD matrix has not been computed!!!" + print( + "Error!!! call to dump_JSD but JSD matrix has not been computed!!!" + ) sys.exit(1) - idx = 0 - for i in range(self.len): - for j in range(i+1, self.len): - print i, j, self.JSD[idx] - idx += 1 + + # the len field does not exist in MultiplexRed + # idx = 0 + # for i in range(self.len): + # for j in range(i + 1, self.len): + # print(i, j, self.JSD[idx]) + # idx += 1 def dump_JSD_approx(self, force_compute=False): - if self.JSD_approx == None: + if self.JSD_approx is None: if force_compute: self.compute_JSD_matrix_approx() else: - print "Error!!! call to dump_JSD_approx but JSD approximate matrix has not been computed!!!" + print( + "Error!!! call to dump_JSD_approx but JSD approximate matrix has not been computed!!!" + ) sys.exit(1) idx = 0 for i in range(self.M): - for j in range(i+1, self.M): - print i, j, self.JSD_approx[idx] + for j in range(i + 1, self.M): + print(i, j, self.JSD_approx[idx]) idx += 1 - def reduce(self, method="ward"): - if (self.verb): + if self.verb: sys.stderr.write("Performing '%s' reduction\n" % method) - if self.JSD == None: + if self.JSD is None: self.compute_JSD_matrix() self.Z = linkage(self.JSD, method=method) return self.Z def reduce_approx(self, method="ward"): - if (self.verb): + if self.verb: sys.stderr.write("Performing '%s' reduction (approx)\n" % method) - if self.JSD_approx == None: + if self.JSD_approx is None: self.compute_JSD_matrix_approx() self.Z_approx = linkage(self.JSD_approx, method=method) return self.Z_approx - + def get_linkage(self): return self.Z @@ -375,15 +399,16 @@ def get_q_profile(self): mylayers = copy.copy(self.layers) rem_layers = copy.copy(self.layers) q_vals = [] - if self.Z == None: + if self.Z is None: self.reduce() q = self.__compute_q(rem_layers) q_vals.append(q) n = len(self.layers) + for l1, l2, _d, _x in self.Z: - l_new = layer(matrix=mylayers[int(l1)].adj_matr) - l_new.num_layer = n - n += 1 + l_new = Layer(matrix=mylayers[int(l1)].adj_matr) + l_new.num_layer = n + n += 1 l_new.aggregate(mylayers[int(l2)]) rem_layers.remove(mylayers[int(l1)]) rem_layers.remove(mylayers[int(l2)]) @@ -393,18 +418,19 @@ def get_q_profile(self): q_vals.append(q) self.q_vals = q_vals return q_vals - pass - def __compute_q_approx(self, layers): H_avg = 0 + if not self.aggr: self.compute_aggregated() self.aggr.compute_VN_entropy_approx(self.poly) + for l in layers: if not l.entropy_approx: l.compute_VN_entropy_approx(self.poly) H_avg += l.entropy_approx + H_avg /= len(layers) q = 1.0 - H_avg / self.aggr.entropy_approx return q @@ -413,15 +439,18 @@ def get_q_profile_approx(self): mylayers = copy.copy(self.layers) rem_layers = copy.copy(self.layers) q_vals = [] - if self.Z_approx == None: + + if self.Z_approx is None: self.reduce_approx() + q = self.__compute_q_approx(rem_layers) q_vals.append(q) n = len(self.layers) + for l1, l2, _d, _x in self.Z_approx: - l_new = layer(matrix=mylayers[int(l1)].adj_matr) - l_new.num_layer = n - n += 1 + l_new = Layer(matrix=mylayers[int(l1)].adj_matr) + l_new.num_layer = n + n += 1 l_new.aggregate(mylayers[int(l2)]) rem_layers.remove(mylayers[int(l1)]) rem_layers.remove(mylayers[int(l2)]) @@ -429,31 +458,35 @@ def get_q_profile_approx(self): mylayers.append(l_new) q = self.__compute_q_approx(rem_layers) q_vals.append(q) + self.q_vals_approx = q_vals return q_vals - + def compute_partitions(self): - if (self.verb): + if self.verb: sys.stderr.write("Getting partitions...\n") - if self.Z == None: + if self.Z is None: self.reduce() - if self.q_vals == None: + if self.q_vals is None: self.get_q_profile() + sets = {} M = len(self.layers) + for i in range(len(self.layers)): sets[i] = [i] - best_pos = self.q_vals.index(max(self.q_vals)) + j = 0 - cur_part = sets.values() + cur_part = list(sets.values()) self.cuts = [copy.deepcopy(cur_part)] - while j < M-1: + + while j < M - 1: l1, l2, _x, _y = self.Z[j] l1 = int(l1) l2 = int(l2) val = sets[l1] val.extend(sets[l2]) - sets[M+j] = val + sets[M + j] = val r1 = cur_part.index(sets[l1]) cur_part.pop(r1) r2 = cur_part.index(sets[l2]) @@ -462,31 +495,33 @@ def compute_partitions(self): j += 1 self.cuts.append(copy.deepcopy(cur_part)) self.cuts.append(copy.deepcopy(cur_part)) - return zip(self.q_vals, self.cuts) + return zip(self.q_vals, self.cuts) def compute_partitions_approx(self): - if (self.verb): + if self.verb: sys.stderr.write("Getting partitions (approx)...\n") - if self.Z_approx == None: + if self.Z_approx is None: self.reduce_approx() - if self.q_vals_approx == None: + if self.q_vals_approx is None: self.get_q_profile_approx() + sets = {} M = len(self.layers) + for i in range(len(self.layers)): sets[i] = [i] - best_pos = self.q_vals_approx.index(max(self.q_vals_approx)) + j = 0 - cur_part = sets.values() + cur_part = list(sets.values()) self.cuts_approx = [copy.deepcopy(cur_part)] - while j < M-1: + while j < M - 1: l1, l2, _x, _y = self.Z_approx[j] l1 = int(l1) l2 = int(l2) val = sets[l1] val.extend(sets[l2]) - sets[M+j] = val + sets[M + j] = val r1 = cur_part.index(sets[l1]) cur_part.pop(r1) r2 = cur_part.index(sets[l2]) @@ -494,14 +529,17 @@ def compute_partitions_approx(self): cur_part.append(val) j += 1 self.cuts_approx.append(copy.deepcopy(cur_part)) + self.cuts_approx.append(copy.deepcopy(cur_part)) return zip(self.q_vals_approx, self.cuts_approx) - def draw_dendrogram(self, force = False): + def draw_dendrogram(self, force=False): if not has_matplotlib: - sys.stderr.write("No matplotlib module found in draw_dendrogram...Exiting!!!\n") + sys.stderr.write( + "No matplotlib module found in draw_dendrogram...Exiting!!!\n" + ) sys.exit(3) - if self.Z == None: + if self.Z is None: if not force: sys.stderr.write("Please call reduce() first or specify 'force=True'") else: @@ -510,13 +548,17 @@ def draw_dendrogram(self, force = False): matplotlib.pyplot.draw() matplotlib.pyplot.show() - def draw_dendrogram_approx(self, force = False): + def draw_dendrogram_approx(self, force=False): if not has_matplotlib: - sys.stderr.write("No matplotlib module found in draw_dendrogram_approx...Exiting!!!\n") + sys.stderr.write( + "No matplotlib module found in draw_dendrogram_approx...Exiting!!!\n" + ) sys.exit(3) - if self.Z_approx == None: + if self.Z_approx is None: if not force: - sys.stderr.write("Please call reduce_approx() first or specify 'force=True'") + sys.stderr.write( + "Please call reduce_approx() first or specify 'force=True'" + ) else: self.reduce_approx() dendrogram(self.Z_approx, no_plot=False) @@ -526,13 +568,9 @@ def draw_dendrogram_approx(self, force = False): def dump_partitions(self): part = zip(self.q_vals, self.cuts) for q, p in part: - print q, "->", p + print(q, "->", p) def dump_partitions_approx(self): part = zip(self.q_vals_approx, self.cuts_approx) for q, p in part: - print q, "->", p - - - - + print(q, "->", p) diff --git a/multired/test/__init__.py b/multired/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/multired/test/mred_test.py b/multired/test/mred_test.py new file mode 100644 index 0000000..6f88202 --- /dev/null +++ b/multired/test/mred_test.py @@ -0,0 +1,34 @@ +import unittest +from multired.mred import MultiplexRed + + +class MyTestCase(unittest.TestCase): + def test_simple(self): + m = MultiplexRed("../../sample_data/file_list", verbose=True) + part = m.compute_partitions() + m.dump_partitions() + m.draw_dendrogram() + + def test_simple_approx(self): + m = MultiplexRed("../../sample_data/file_list", verbose=True, fit_degree=20) + part = m.compute_partitions_approx() + m.dump_partitions_approx() + m.draw_dendrogram_approx() + + def test_approx(self): + m = MultiplexRed("../../sample_data/file_list") + m.compute_layer_entropies_approx() + m.compute_JSD_matrix_approx() + m.reduce_approx() + + def test_base(self): + m = MultiplexRed("../../sample_data/file_list") + m.compute_layer_entropies() + m.compute_JSD_matrix() + m.reduce() + part = m.compute_partitions() + m.dump_partitions() + + +if __name__ == '__main__': + unittest.main() diff --git a/python/README.md b/python/README.md deleted file mode 100644 index cc196b5..0000000 --- a/python/README.md +++ /dev/null @@ -1,98 +0,0 @@ -# multired -/** -* -* Copyright (C) 2015 Vincenzo (Enzo) Nicosia -* -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, but -* WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* long with this program. If not, see . -* -*/ - -This is multired-0.1. - - -This is a Python implementation of the algorithm for structural -reduction of multi-layer networks based on the Von Neumann Entropy -and on the Quantum Jensen-Shannon divergence of graphs, as explained -in: - - M. De Domenico. V. Nicosia, A. Arenas, V. Latora - "Structural reducibility of multilayer networks", - Nat. Commun. 6, 6864 (2015) doi:10.1038/ncomms7864 - -If you happen to find any use of this code please do not forget to -cite that paper ;-) - - --------------------- - INFO --------------------- - -The module "multired.py" provides the class "multiplex_red", which -implements the algorithm to reduce a multilayer network described in -the paper cited above. - -In order to use it, you just need to - - import multired as mr - -in your python script and create a multiplex_red object. Please make -sure that "multired.py" is in PYTHONPATH. The constructor requires as -its first argument the path of a file which in turn contains a list of -files (one for each line) where the graph of each layer is to be -found. - -The class provides one set of methods which perform the exact -evaluation of the Von Neumann entropy, and another set of methods -(those whose name end with the suffix "_approx") which rely on a -polynomial approximation of the Von Neumann entropy. By default the -approximation is based on a 10th order polynomial fit of x log(x) in -[0,1], but the order of the polynomial can be set through the -parameter "fit_degree" of the constructor. - -Several sample scripts can be found in the "test/" directory. You also -find a sample data set in the folder "sample_data/". That is the 4-layer -Noordin Top Terrorist multiplex network, originally provided in: - - N. Roberts, S. F. Everton, Roberts and Everton "Terrorist - Data: Noordin Top Terrorist Network" (Subset) (2011). - -and extensively studied in: - - F. Battiston, V. Nicosia, V. Latora, - "Structural measures for multiplex networks", - Phys. Rev. E 89, 032804 (2014). - -Please consider citing those papers if you use that data set in a -scientific work. - --------------------- - DEPENDENCIES --------------------- - -The only strict dependencies are a recent version of Python, Numpy and -SciPy. The methods "draw_dendrogram" and "draw_dendrogram_approx" will -work only if matplotlib is installed. - -The module has been tested on a Debian GNU/Linux system, using: - - - Python 2.7.8, - - SciPy 0.13.3 - - Numpy 1.8.2 - - matplotlib 1.3.1 - -but it will almost surely work on other platforms and/or with other -versions of those packages. If you would like to report a working -configuration, just email me (the address is at the beginning of this -file). diff --git a/python/test/simple_test.py b/python/test/simple_test.py deleted file mode 100644 index 7c5493d..0000000 --- a/python/test/simple_test.py +++ /dev/null @@ -1,45 +0,0 @@ -# -# -# simple_test.py -# -# -# Copyright (C) 2015 Vincenzo (Enzo) Nicosia -# -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# long with this program. If not, see . -# - - -import multired as mr -import sys - - -if len(sys.argv) < 2: - print "Usage: %s " % sys.argv[0] - sys.exit(1) - -print "Loading layers...", -m = mr.multiplex_red(sys.argv[1], verbose=True) -print "[DONE]" - -print "Getting partitons...", -part = m.compute_partitions() -print "[DONE]" - -print "Partitions:..." -m.dump_partitions() - -m.draw_dendrogram() - - diff --git a/python/test/simple_test_approx.py b/python/test/simple_test_approx.py deleted file mode 100644 index 4558ec2..0000000 --- a/python/test/simple_test_approx.py +++ /dev/null @@ -1,39 +0,0 @@ -# -# -# simple_test_approx.py -# -# -# Copyright (C) 2015 Vincenzo (Enzo) Nicosia -# -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# long with this program. If not, see . -# - - -import multired as mr -import sys - - -if len(sys.argv) < 2: - print "Usage: %s " % sys.argv[0] - sys.exit(1) - -m = mr.multiplex_red(sys.argv[1], verbose=True, fit_degree=20) -part = m.compute_partitions_approx() - -print "Partitions:..." -m.dump_partitions_approx() - -m.draw_dendrogram_approx() - diff --git a/python/test/test.py b/python/test/test.py deleted file mode 100644 index 4a1d87a..0000000 --- a/python/test/test.py +++ /dev/null @@ -1,57 +0,0 @@ -# -# -# test.py -# -# -# Copyright (C) 2015 Vincenzo (Enzo) Nicosia -# -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# long with this program. If not, see . -# - - -import multired as mr -import sys - - -if len(sys.argv) < 2: - print "Usage: %s " % sys.argv[0] - sys.exit(1) - -print "Loading layers...", -m = mr.multiplex_red(sys.argv[1]) -print "[DONE]" - -print "Computing layer entropies...", -m.compute_layer_entropies() -print "[DONE]" - -print "Computing JSD matrix...", -m.compute_JSD_matrix() -print "[DONE]" - -print "Performing reduction...", -m.reduce() -print "[DONE]" - -print "Getting partitons...", -part = m.compute_partitions() -print "[DONE]" - -print "Partitions:...", -m.dump_partitions() -print "[DONE]" - - - diff --git a/python/test/test_approx.py b/python/test/test_approx.py deleted file mode 100644 index 6538c19..0000000 --- a/python/test/test_approx.py +++ /dev/null @@ -1,56 +0,0 @@ -# -# -# test_approx.py -# -# -# Copyright (C) 2015 Vincenzo (Enzo) Nicosia -# -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# long with this program. If not, see . -# - -import multired as mr -import sys - - -if len(sys.argv) < 2: - print "Usage: %s " % sys.argv[0] - sys.exit(1) - -print "Loading layers...", -m = mr.multiplex_red(sys.argv[1]) -print "[DONE]" - -print "Computing layer entropies (approx)...", -m.compute_layer_entropies_approx() -print "[DONE]" - -print "Computing JSD matrix (approx)...", -m.compute_JSD_matrix_approx() -print "[DONE]" - -print "Performing reduction (approx)...", -m.reduce_approx() -print "[DONE]" - - -print "Getting partitons...", -part = m.compute_partitions_approx() -print "[DONE]" - -print "Partitions:..." -m.dump_partitions_approx() -print "[DONE]" - - diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4a2a08b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +numpy +scipy +matplotlib \ No newline at end of file diff --git a/python/sample_data/README b/sample_data/README similarity index 88% rename from python/sample_data/README rename to sample_data/README index dd4a910..aec7cd8 100644 --- a/python/sample_data/README +++ b/sample_data/README @@ -11,10 +11,7 @@ mentioned in: The four layers represent, respectively, communications, financial, operation and trust relatioships among a group of 78 terrorists. -You can test multired on this data set by using, e.g., : - - python simple_test.py file_list - +You can test multired on this data set by running the unittest in multired/test/mred.py If you use this data set in a research work, please cite that paper. diff --git a/python/sample_data/communication.txt b/sample_data/communication.txt similarity index 100% rename from python/sample_data/communication.txt rename to sample_data/communication.txt diff --git a/python/sample_data/file_list b/sample_data/file_list similarity index 100% rename from python/sample_data/file_list rename to sample_data/file_list diff --git a/python/sample_data/financial.txt b/sample_data/financial.txt similarity index 100% rename from python/sample_data/financial.txt rename to sample_data/financial.txt diff --git a/python/sample_data/operationalaggregated.txt b/sample_data/operationalaggregated.txt similarity index 100% rename from python/sample_data/operationalaggregated.txt rename to sample_data/operationalaggregated.txt diff --git a/python/sample_data/trustaggregated.txt b/sample_data/trustaggregated.txt similarity index 100% rename from python/sample_data/trustaggregated.txt rename to sample_data/trustaggregated.txt diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..17bfcd1 --- /dev/null +++ b/setup.py @@ -0,0 +1,53 @@ +from setuptools import setup, find_packages +from codecs import open +from os import path + + +__author__ = "Vincenzo (Enzo) Nicosia" +__license__ = "GPL" +__email__ = "katolaz@yahoo.it" + + +here = path.abspath(path.dirname(__file__)) + +# Get the long description from the README file +with open(path.join(here, "README.md"), encoding="utf-8") as f: + long_description = f.read() + +with open(path.join(here, "requirements.txt"), encoding="utf-8") as f: + requirements = f.read().splitlines() + + +setup( + name="multired", + version="0.2", + license="GPL", + description="Structural reduction of multilayer networks", + url="https://github.com/KatolaZ/multired", + author="Vincenzo (Enzo) Nicosia", + author_email="katolaz@yahoo.it", + classifiers=[ + # How mature is this project? Common values are + # 3 - Alpha + # 4 - Beta + # 5 - Production/Stable + "Development Status :: 4 - Beta", + # Indicate who your project is intended for + "Intended Audience :: Developers", + "Topic :: Software Development :: Build Tools", + # Pick your license as you wish (should match "license" above) + "License :: OSI Approved :: GPL", + "Operating System :: POSIX :: Other", + "Operating System :: MacOS", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + ], + keywords=" ", + install_requires=requirements, + long_description=long_description, + long_description_content_type="text/markdown", + extras_require={}, + packages=find_packages( + exclude=["*.test", "*.test.*", "test.*", "test", "multired.test", "multired.test.*"] + ), +)