def test_symmetrize(self): W = sparse.random(100, 100, random_state=42) for method in ['average', 'maximum', 'fill', 'tril', 'triu']: # Test that the regular and sparse versions give the same result. W1 = utils.symmetrize(W, method=method) W2 = utils.symmetrize(W.toarray(), method=method) np.testing.assert_equal(W1.toarray(), W2) self.assertRaises(ValueError, utils.symmetrize, W, 'sum')
def __init__(self, N=64, seed=None, **kwargs): self.seed = seed rs = np.random.RandomState(seed) position = np.sort(rs.uniform(size=N), axis=0) weight = N * np.diff(position) weight_end = N * (1 + position[0] - position[-1]) inds_i = np.arange(0, N - 1) inds_j = np.arange(1, N) W = sparse.csc_matrix((weight, (inds_i, inds_j)), shape=(N, N)) W = W.tolil() W[0, N - 1] = weight_end W = utils.symmetrize(W, method='triu') angle = position * 2 * np.pi coords = np.stack([np.cos(angle), np.sin(angle)], axis=1) plotting = {'limits': np.array([-1, 1, -1, 1])} super(RandomRing, self).__init__(W=W, coords=coords, plotting=plotting, **kwargs)
def __init__(self, N1=16, N2=None, **kwargs): if N2 is None: N2 = N1 N = N1 * N2 # Filling up the weight matrix this way is faster than # looping through all the grid points: diag_1 = np.ones(N - 1) diag_1[(N2 - 1)::N2] = 0 diag_2 = np.ones(N - N2) W = sparse.diags(diagonals=[diag_1, diag_2], offsets=[-1, -N2], shape=(N, N), format='csr', dtype='float') W = utils.symmetrize(W, method='tril') x = np.kron(np.ones((N1, 1)), (np.arange(N2)/float(N2)).reshape(N2, 1)) y = np.kron(np.ones((N2, 1)), np.arange(N1)/float(N1)).reshape(N, 1) y = np.sort(y, axis=0)[::-1] coords = np.concatenate((x, y), axis=1) plotting = {"limits": np.array([-1. / N2, 1 + 1. / N2, 1. / N1, 1 + 1. / N1])} super(Grid2d, self).__init__(W=W, gtype='2d-grid', coords=coords, plotting=plotting, **kwargs)
def _get_upper_bound(self): r"""Return an upper bound on the eigenvalues of the Laplacian.""" if self.lap_type == 'normalized': return 2 # Equal iff the graph is bipartite. elif self.lap_type == 'combinatorial': bounds = [] # Equal for full graphs. bounds += [self.n_vertices * np.max(self.W)] # Gershgorin circle theorem. Equal for regular bipartite graphs. # Special case of the below bound. bounds += [2 * np.max(self.dw)] # Anderson, Morley, Eigenvalues of the Laplacian of a graph. # Equal for regular bipartite graphs. if self.n_edges > 0: sources, targets, _ = self.get_edge_list() bounds += [np.max(self.dw[sources] + self.dw[targets])] # Merris, A note on Laplacian graph eigenvalues. if not self.is_directed(): W = self.W else: W = utils.symmetrize(self.W, method='average') m = W.dot(self.dw) / self.dw # Mean degree of adjacent vertices. bounds += [np.max(self.dw + m)] # Good review: On upper bounds for Laplacian graph eigenvalues. return min(bounds) else: raise ValueError('Unknown Laplacian type ' '{}'.format(self.lap_type))
def __init__(self, N=64, Nc=2, regular=False, n_try=50, distribute=False, connected=True, seed=None, **kwargs): self.Nc = Nc self.regular = regular self.n_try = n_try self.distribute = distribute self.seed = seed self.logger = utils.build_logger(__name__) if connected: for x in range(self.n_try): W, coords = self._create_weight_matrix(N, distribute, regular, Nc) self.W = W if self.is_connected(recompute=True): break elif x == self.n_try - 1: self.logger.warning('Graph is not connected.') else: W, coords = self._create_weight_matrix(N, distribute, regular, Nc) W = sparse.lil_matrix(W) W = utils.symmetrize(W, method='average') gtype = 'regular sensor' if self.regular else 'sensor' plotting = {'limits': np.array([0, 1, 0, 1])} super(Sensor, self).__init__(W=W, coords=coords, gtype=gtype, plotting=plotting, **kwargs)
def _handle_directed(G): # FIXME: plot edge direction. For now we just symmetrize the weight matrix. if not G.is_directed(): return G else: from pygsp import graphs G2 = graphs.Graph(utils.symmetrize(G.W)) G2.coords = G.coords G2.plotting = G.plotting return G2
def __init__(self, N1=16, N2=None, diagonal=0.0, **kwargs): if N2 is None: N2 = N1 self.N1 = N1 self.N2 = N2 N = N1 * N2 # Filling up the weight matrix this way is faster than # looping through all the grid points: diag_1 = np.ones(N - 1) diag_1[(N2 - 1)::N2] = 0 diag_2 = np.ones(N - N2) W = sparse.diags(diagonals=[diag_1, diag_2], offsets=[-1, -N2], shape=(N, N), format='csr', dtype='float') if min(N1, N2) > 1 and diagonal != 0.0: # Connecting node with they diagonal neighbours diag_3 = np.full(N - N2 - 1, diagonal) diag_4 = np.full(N - N2 + 1, diagonal) diag_3[N2 - 1::N2] = 0 diag_4[0::N2] = 0 D = sparse.diags(diagonals=[diag_3, diag_4], offsets=[-N2 - 1, -N2 + 1], shape=(N, N), format='csr', dtype='float') W += D W = utils.symmetrize(W, method='tril') x = np.kron(np.ones((N1, 1)), (np.arange(N2) / float(N2)).reshape(N2, 1)) y = np.kron(np.ones((N2, 1)), np.arange(N1) / float(N1)).reshape(N, 1) y = np.sort(y, axis=0)[::-1] coords = np.concatenate((x, y), axis=1) plotting = { "limits": np.array([-1. / N2, 1 + 1. / N2, 1. / N1, 1 + 1. / N1]) } super(Grid2d, self).__init__(W, coords=coords, plotting=plotting, **kwargs)
def _get_nc_connection(self, W, param_nc): Wtmp = W W = np.zeros(np.shape(W)) for i in range(np.shape(W)[0]): l = Wtmp[i] for j in range(param_nc): val = np.max(l) ind = np.argmax(l) W[i, ind] = val l[ind] = 0 W = utils.symmetrize(W, method='average') return W
def __init__(self, N=64, angles=None, seed=None, **kwargs): self.seed = seed if angles is None: rs = np.random.RandomState(seed) angles = np.sort(rs.uniform(0, 2 * np.pi, size=N), axis=0) else: angles = np.asanyarray(angles) angles.sort() # Need to be sorted to take the difference. N = len(angles) if np.any(angles < 0) or np.any(angles >= 2 * np.pi): raise ValueError('Angles should be in [0, 2 pi]') self.angles = angles if N < 3: # Asymmetric graph needed for 2 as 2 distances connect them. raise ValueError('There should be at least 3 vertices.') rows = range(0, N - 1) cols = range(1, N) weights = np.diff(angles) # Close the loop. rows = np.concatenate((rows, [0])) cols = np.concatenate((cols, [N - 1])) weights = np.concatenate( (weights, [2 * np.pi + angles[0] - angles[-1]])) W = sparse.coo_matrix((weights, (rows, cols)), shape=(N, N)) W = utils.symmetrize(W, method='triu') # Width as the expected angle. All angles are equal to that value when # the ring is uniformly sampled. width = 2 * np.pi / N assert (W.data.mean() - width) < 1e-10 # TODO: why this kernel ? It empirically produces eigenvectors closer # to the sines and cosines. W.data = width / W.data coords = np.stack([np.cos(angles), np.sin(angles)], axis=1) plotting = {'limits': np.array([-1, 1, -1, 1])} # TODO: save angle and 2D position as graph signals super(RandomRing, self).__init__(W, coords=coords, plotting=plotting, **kwargs)
def __init__(self, N1=16, N2=None, diagonal=0.0, **kwargs): if N2 is None: N2 = N1 self.N1 = N1 self.N2 = N2 N = N1 * N2 # Filling up the weight matrix this way is faster than # looping through all the grid points: diag_1 = np.ones(N - 1) diag_1[(N2 - 1)::N2] = 0 diag_2 = np.ones(N - N2) W = sparse.diags(diagonals=[diag_1, diag_2], offsets=[-1, -N2], shape=(N, N), format='csr', dtype='float') if min(N1, N2) > 1 and diagonal != 0.0: # Connecting node with they diagonal neighbours diag_3 = np.full(N - N2 - 1, diagonal) diag_4 = np.full(N - N2 + 1, diagonal) diag_3[N2 - 1::N2] = 0 diag_4[0::N2] = 0 D = sparse.diags(diagonals=[diag_3, diag_4], offsets=[-N2 - 1, -N2 + 1], shape=(N, N), format='csr', dtype='float') W += D W = utils.symmetrize(W, method='tril') x = np.kron(np.ones((N1, 1)), (np.arange(N2)/float(N2)).reshape(N2, 1)) y = np.kron(np.ones((N2, 1)), np.arange(N1)/float(N1)).reshape(N, 1) y = np.sort(y, axis=0)[::-1] coords = np.concatenate((x, y), axis=1) plotting = {"limits": np.array([-1. / N2, 1 + 1. / N2, 1. / N1, 1 + 1. / N1])} super(Grid2d, self).__init__(W, coords=coords, plotting=plotting, **kwargs)
def __init__(self, N=64, angles=None, seed=None, **kwargs): self.seed = seed if angles is None: rs = np.random.RandomState(seed) angles = np.sort(rs.uniform(0, 2*np.pi, size=N), axis=0) else: angles = np.asanyarray(angles) angles.sort() # Need to be sorted to take the difference. N = len(angles) if np.any(angles < 0) or np.any(angles >= 2*np.pi): raise ValueError('Angles should be in [0, 2 pi]') self.angles = angles if N < 3: # Asymmetric graph needed for 2 as 2 distances connect them. raise ValueError('There should be at least 3 vertices.') rows = range(0, N-1) cols = range(1, N) weights = np.diff(angles) # Close the loop. rows = np.concatenate((rows, [0])) cols = np.concatenate((cols, [N-1])) weights = np.concatenate((weights, [2*np.pi + angles[0] - angles[-1]])) W = sparse.coo_matrix((weights, (rows, cols)), shape=(N, N)) W = utils.symmetrize(W, method='triu') # Width as the expected angle. All angles are equal to that value when # the ring is uniformly sampled. width = 2 * np.pi / N assert (W.data.mean() - width) < 1e-10 # TODO: why this kernel ? It empirically produces eigenvectors closer # to the sines and cosines. W.data = width / W.data coords = np.stack([np.cos(angles), np.sin(angles)], axis=1) plotting = {'limits': np.array([-1, 1, -1, 1])} # TODO: save angle and 2D position as graph signals super(RandomRing, self).__init__(W, coords=coords, plotting=plotting, **kwargs)
def __init__(self, Xin, NNtype='knn', use_flann=False, center=True, rescale=True, k=10, sigma=None, epsilon=0.01, plotting={}, symmetrize_type='average', dist_type='euclidean', order=0, **kwargs): self.Xin = Xin self.NNtype = NNtype self.use_flann = use_flann self.center = center self.rescale = rescale self.k = k self.sigma = sigma self.epsilon = epsilon self.symmetrize_type = symmetrize_type self.dist_type = dist_type self.order = order N, d = np.shape(self.Xin) Xout = self.Xin if k >= N: raise ValueError('The number of neighbors (k={}) must be smaller ' 'than the number of nodes ({}).'.format(k, N)) if self.center: Xout = self.Xin - np.kron(np.ones((N, 1)), np.mean(self.Xin, axis=0)) if self.rescale: bounding_radius = 0.5 * np.linalg.norm(np.amax(Xout, axis=0) - np.amin(Xout, axis=0), 2) scale = np.power(N, 1. / float(min(d, 3))) / 10. Xout *= scale / bounding_radius # Translate distance type string to corresponding Minkowski order. dist_translation = {"euclidean": 2, "manhattan": 1, "max_dist": np.inf, "minkowski": order } if self.NNtype == 'knn': spi = np.zeros((N * k)) spj = np.zeros((N * k)) spv = np.zeros((N * k)) if self.use_flann: pfl = _import_pfl() pfl.set_distance_type(dist_type, order=order) flann = pfl.FLANN() # Default FLANN parameters (I tried changing the algorithm and # testing performance on huge matrices, but the default one # seems to work best). NN, D = flann.nn(Xout, Xout, num_neighbors=(k + 1), algorithm='kdtree') else: kdt = spatial.KDTree(Xout) D, NN = kdt.query(Xout, k=(k + 1), p=dist_translation[dist_type]) if self.sigma is None: self.sigma = np.mean(D[:, 1:]) # Discard distance to self. for i in range(N): spi[i * k:(i + 1) * k] = np.kron(np.ones((k)), i) spj[i * k:(i + 1) * k] = NN[i, 1:] spv[i * k:(i + 1) * k] = np.exp(-np.power(D[i, 1:], 2) / float(self.sigma)) elif self.NNtype == 'radius': kdt = spatial.KDTree(Xout) D, NN = kdt.query(Xout, k=None, distance_upper_bound=epsilon, p=dist_translation[dist_type]) if self.sigma is None: # Discard distance to self. self.sigma = np.mean([np.mean(d[1:]) for d in D]) count = 0 for i in range(N): count = count + len(NN[i]) spi = np.zeros((count)) spj = np.zeros((count)) spv = np.zeros((count)) start = 0 for i in range(N): leng = len(NN[i]) - 1 spi[start:start + leng] = np.kron(np.ones((leng)), i) spj[start:start + leng] = NN[i][1:] spv[start:start + leng] = np.exp(-np.power(D[i][1:], 2) / float(self.sigma)) start = start + leng else: raise ValueError('Unknown NNtype {}'.format(self.NNtype)) W = sparse.csc_matrix((spv, (spi, spj)), shape=(N, N)) # Sanity check if np.shape(W)[0] != np.shape(W)[1]: raise ValueError('Weight matrix W is not square') # Enforce symmetry. Note that checking symmetry with # np.abs(W - W.T).sum() is as costly as the symmetrization itself. W = utils.symmetrize(W, method=symmetrize_type) super(NNGraph, self).__init__(W, plotting=plotting, coords=Xout, **kwargs)
def __init__(self, Xin, NNtype='knn', use_flann=False, center=True, rescale=True, k=10, sigma=0.1, epsilon=0.01, plotting={}, symmetrize_type='average', dist_type='euclidean', order=0, **kwargs): self.Xin = Xin self.NNtype = NNtype self.use_flann = use_flann self.center = center self.rescale = rescale self.k = k self.sigma = sigma self.epsilon = epsilon self.symmetrize_type = symmetrize_type self.dist_type = dist_type self.order = order N, d = np.shape(self.Xin) Xout = self.Xin if self.center: Xout = self.Xin - np.kron(np.ones( (N, 1)), np.mean(self.Xin, axis=0)) if self.rescale: bounding_radius = 0.5 * np.linalg.norm( np.amax(Xout, axis=0) - np.amin(Xout, axis=0), 2) scale = np.power(N, 1. / float(min(d, 3))) / 10. Xout *= scale / bounding_radius # Translate distance type string to corresponding Minkowski order. dist_translation = { "euclidean": 2, "manhattan": 1, "max_dist": np.inf, "minkowski": order } if self.NNtype == 'knn': spi = np.zeros((N * k)) spj = np.zeros((N * k)) spv = np.zeros((N * k)) if self.use_flann: pfl = _import_pfl() pfl.set_distance_type(dist_type, order=order) flann = pfl.FLANN() # Default FLANN parameters (I tried changing the algorithm and # testing performance on huge matrices, but the default one # seems to work best). NN, D = flann.nn(Xout, Xout, num_neighbors=(k + 1), algorithm='kdtree') else: kdt = spatial.KDTree(Xout) D, NN = kdt.query(Xout, k=(k + 1), p=dist_translation[dist_type]) for i in range(N): spi[i * k:(i + 1) * k] = np.kron(np.ones((k)), i) spj[i * k:(i + 1) * k] = NN[i, 1:] spv[i * k:(i + 1) * k] = np.exp(-np.power(D[i, 1:], 2) / float(self.sigma)) elif self.NNtype == 'radius': kdt = spatial.KDTree(Xout) D, NN = kdt.query(Xout, k=None, distance_upper_bound=epsilon, p=dist_translation[dist_type]) count = 0 for i in range(N): count = count + len(NN[i]) spi = np.zeros((count)) spj = np.zeros((count)) spv = np.zeros((count)) start = 0 for i in range(N): leng = len(NN[i]) - 1 spi[start:start + leng] = np.kron(np.ones((leng)), i) spj[start:start + leng] = NN[i][1:] spv[start:start + leng] = np.exp(-np.power(D[i][1:], 2) / float(self.sigma)) start = start + leng else: raise ValueError('Unknown NNtype {}'.format(self.NNtype)) W = sparse.csc_matrix((spv, (spi, spj)), shape=(N, N)) # Sanity check if np.shape(W)[0] != np.shape(W)[1]: raise ValueError('Weight matrix W is not square') # Enforce symmetry. Note that checking symmetry with # np.abs(W - W.T).sum() is as costly as the symmetrization itself. W = utils.symmetrize(W, method=symmetrize_type) super(NNGraph, self).__init__(W=W, plotting=plotting, coords=Xout, **kwargs)
def compute_laplacian(self, lap_type='combinatorial'): r"""Compute a graph Laplacian. For undirected graphs, the combinatorial Laplacian is defined as .. math:: L = D - W, where :math:`W` is the weighted adjacency matrix and :math:`D` the weighted degree matrix. The normalized Laplacian is defined as .. math:: L = I - D^{-1/2} W D^{-1/2}, where :math:`I` is the identity matrix. For directed graphs, the Laplacians are built from a symmetrized version of the weighted adjacency matrix that is the average of the weighted adjacency matrix and its transpose. As the Laplacian is defined as the divergence of the gradient, it is not affected by the orientation of the edges. For both Laplacians, the diagonal entries corresponding to disconnected nodes (i.e., nodes with degree zero) are set to zero. Once computed, the Laplacian is accessible by the attribute :attr:`L`. Parameters ---------- lap_type : {'combinatorial', 'normalized'} The kind of Laplacian to compute. Default is combinatorial. Examples -------- Combinatorial and normalized Laplacians of an undirected graph. >>> graph = graphs.Graph([ ... [0, 2, 0], ... [2, 0, 1], ... [0, 1, 0], ... ]) >>> graph.compute_laplacian('combinatorial') >>> graph.L.toarray() array([[ 2., -2., 0.], [-2., 3., -1.], [ 0., -1., 1.]]) >>> graph.compute_laplacian('normalized') >>> graph.L.toarray() array([[ 1. , -0.81649658, 0. ], [-0.81649658, 1. , -0.57735027], [ 0. , -0.57735027, 1. ]]) Combinatorial and normalized Laplacians of a directed graph. >>> graph = graphs.Graph([ ... [0, 2, 0], ... [2, 0, 1], ... [0, 0, 0], ... ]) >>> graph.compute_laplacian('combinatorial') >>> graph.L.toarray() array([[ 2. , -2. , 0. ], [-2. , 2.5, -0.5], [ 0. , -0.5, 0.5]]) >>> graph.compute_laplacian('normalized') >>> graph.L.toarray() array([[ 1. , -0.89442719, 0. ], [-0.89442719, 1. , -0.4472136 ], [ 0. , -0.4472136 , 1. ]]) The Laplacian is defined as the divergence of the gradient. See :meth:`compute_differential_operator` for details. >>> graph = graphs.Path(20) >>> graph.compute_differential_operator() >>> L = graph.D.dot(graph.D.T) >>> np.all(L.toarray() == graph.L.toarray()) True The Laplacians have a bounded spectrum. >>> G = graphs.Sensor(50) >>> G.compute_laplacian('combinatorial') >>> G.compute_fourier_basis() >>> -1e-10 < G.e[0] < 1e-10 < G.e[-1] < 2*np.max(G.dw) True >>> G.compute_laplacian('normalized') >>> G.compute_fourier_basis() >>> -1e-10 < G.e[0] < 1e-10 < G.e[-1] < 2 True """ if lap_type != self.lap_type: # Those attributes are invalidated when the Laplacian is changed. # Alternative: don't allow the user to change the Laplacian. self._lmax = None self._U = None self._e = None self._coherence = None self._D = None self.lap_type = lap_type if not self.is_directed(): W = self.W else: W = utils.symmetrize(self.W, method='average') if lap_type == 'combinatorial': D = sparse.diags(self.dw) self.L = D - W elif lap_type == 'normalized': d = np.zeros(self.n_vertices) disconnected = (self.dw == 0) np.power(self.dw, -0.5, where=~disconnected, out=d) D = sparse.diags(d) self.L = sparse.identity(self.n_vertices) - D * W * D self.L[disconnected, disconnected] = 0 self.L.eliminate_zeros() else: raise ValueError('Unknown Laplacian type {}'.format(lap_type))
def __init__(self, N=1024, k=5, z=None, M=None, p=0.7, q=None, directed=False, self_loops=False, connected=False, n_try=10, seed=None, **kwargs): self.k = k self.directed = directed self.self_loops = self_loops self.connected = connected self.n_try = n_try self.seed = seed rng = np.random.default_rng(seed) if z is None: z = rng.integers(0, k, N) z.sort() # Sort for nice spy plot of W, where blocks are apparent. self.z = z if M is None: self.p = p p = np.asanyarray(p) if p.size == 1: p = p * np.ones(k) if p.shape != (k, ): raise ValueError('Optional parameter p is neither a scalar ' 'nor a vector of length k.') if q is None: q = 0.3 / k self.q = q q = np.asanyarray(q) if q.size == 1: q = q * np.ones((k, k)) if q.shape != (k, k): raise ValueError('Optional parameter q is neither a scalar ' 'nor a matrix of size k x k.') M = q M.flat[::k + 1] = p # edit the diagonal terms self.M = M if (M < 0).any() or (M > 1).any(): raise ValueError('Probabilities should be in [0, 1].') # TODO: higher memory, lesser computation alternative. # Along the lines of np.random.uniform(size=(N, N)) < p. # Or similar to sparse.random(N, N, p, data_rvs=lambda n: np.ones(n)). while (n_try is None) or (n_try > 0): nb_row, nb_col = 0, 0 csr_data, csr_i, csr_j = [], [], [] for _ in range(N**2): if nb_row != nb_col or self_loops: if nb_row >= nb_col or directed: if rng.uniform() < M[z[nb_row], z[nb_col]]: csr_data.append(1) csr_i.append(nb_row) csr_j.append(nb_col) if nb_row < N - 1: nb_row += 1 else: nb_row = 0 nb_col += 1 W = sparse.csr_matrix((csr_data, (csr_i, csr_j)), shape=(N, N)) if not directed: W = utils.symmetrize(W, method='tril') if not connected: break if Graph(W).is_connected(): break if n_try is not None: n_try -= 1 if connected and n_try == 0: raise ValueError('The graph could not be connected after {} ' 'trials. Increase the connection probability ' 'or the number of trials.'.format(self.n_try)) self.info = { 'node_com': z, 'comm_sizes': np.bincount(z), 'world_rad': np.sqrt(N) } super(StochasticBlockModel, self).__init__(W, **kwargs)
def __init__(self, N=1024, k=5, z=None, M=None, p=0.7, q=None, directed=False, self_loops=False, connected=False, n_try=10, seed=None, **kwargs): self.k = k self.directed = directed self.self_loops = self_loops self.connected = connected self.n_try = n_try self.seed = seed rs = np.random.RandomState(seed) if z is None: z = rs.randint(0, k, N) z.sort() # Sort for nice spy plot of W, where blocks are apparent. self.z = z if M is None: self.p = p p = np.asanyarray(p) if p.size == 1: p = p * np.ones(k) if p.shape != (k,): raise ValueError('Optional parameter p is neither a scalar ' 'nor a vector of length k.') if q is None: q = 0.3 / k self.q = q q = np.asanyarray(q) if q.size == 1: q = q * np.ones((k, k)) if q.shape != (k, k): raise ValueError('Optional parameter q is neither a scalar ' 'nor a matrix of size k x k.') M = q M.flat[::k+1] = p # edit the diagonal terms self.M = M if (M < 0).any() or (M > 1).any(): raise ValueError('Probabilities should be in [0, 1].') # TODO: higher memory, lesser computation alternative. # Along the lines of np.random.uniform(size=(N, N)) < p. # Or similar to sparse.random(N, N, p, data_rvs=lambda n: np.ones(n)). while (n_try is None) or (n_try > 0): nb_row, nb_col = 0, 0 csr_data, csr_i, csr_j = [], [], [] for _ in range(N**2): if nb_row != nb_col or self_loops: if nb_row >= nb_col or directed: if rs.uniform() < M[z[nb_row], z[nb_col]]: csr_data.append(1) csr_i.append(nb_row) csr_j.append(nb_col) if nb_row < N-1: nb_row += 1 else: nb_row = 0 nb_col += 1 W = sparse.csr_matrix((csr_data, (csr_i, csr_j)), shape=(N, N)) if not directed: W = utils.symmetrize(W, method='tril') if not connected: break if Graph(W).is_connected(): break if n_try is not None: n_try -= 1 if connected and n_try == 0: raise ValueError('The graph could not be connected after {} ' 'trials. Increase the connection probability ' 'or the number of trials.'.format(self.n_try)) self.info = {'node_com': z, 'comm_sizes': np.bincount(z), 'world_rad': np.sqrt(N)} super(StochasticBlockModel, self).__init__(W, **kwargs)
def __init__(self, N=1024, k=5, z=None, M=None, p=0.7, q=None, directed=False, self_loops=False, connected=False, max_iter=10, seed=None, **kwargs): rs = np.random.RandomState(seed) if z is None: z = rs.randint(0, k, N) z.sort() # Sort for nice spy plot of W, where blocks are apparent. if M is None: p = np.asarray(p) if p.size == 1: p = p * np.ones(k) if p.shape != (k, ): raise ValueError('Optional parameter p is neither a scalar ' 'nor a vector of length k.') if q is None: q = 0.3 / k q = np.asarray(q) if q.size == 1: q = q * np.ones((k, k)) if q.shape != (k, k): raise ValueError('Optional parameter q is neither a scalar ' 'nor a matrix of size k x k.') M = q M.flat[::k + 1] = p # edit the diagonal terms if (M < 0).any() or (M > 1).any(): raise ValueError('Probabilities should be in [0, 1].') # TODO: higher memory, lesser computation alternative. # Along the lines of np.random.uniform(size=(N, N)) < p. # Or similar to sparse.random(N, N, p, data_rvs=lambda n: np.ones(n)). for nb_iter in range(max_iter): nb_row, nb_col = 0, 0 csr_data, csr_i, csr_j = [], [], [] for _ in range(N**2): if nb_row != nb_col or self_loops: if nb_row >= nb_col or directed: if rs.uniform() < M[z[nb_row], z[nb_col]]: csr_data.append(1) csr_i.append(nb_row) csr_j.append(nb_col) if nb_row < N - 1: nb_row += 1 else: nb_row = 0 nb_col += 1 W = sparse.csr_matrix((csr_data, (csr_i, csr_j)), shape=(N, N)) if not directed: W = utils.symmetrize(W, method='tril') if not connected: break else: self.W = W if self.is_connected(recompute=True): break if nb_iter == max_iter - 1: raise ValueError('The graph could not be connected after {} ' 'trials. Increase the connection probability ' 'or the number of trials.'.format(max_iter)) self.info = { 'node_com': z, 'comm_sizes': np.bincount(z), 'world_rad': np.sqrt(N) } gtype = 'StochasticBlockModel' super(StochasticBlockModel, self).__init__(gtype=gtype, W=W, **kwargs)