Ejemplo n.º 1
0
 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')
Ejemplo n.º 2
0
    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)
Ejemplo n.º 3
0
    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)
Ejemplo n.º 4
0
    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))
Ejemplo n.º 5
0
    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))
Ejemplo n.º 6
0
    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)
Ejemplo n.º 7
0
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
Ejemplo n.º 8
0
    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)
Ejemplo n.º 9
0
    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
Ejemplo n.º 10
0
    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)
Ejemplo n.º 11
0
    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)
Ejemplo n.º 12
0
    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)
Ejemplo n.º 13
0
    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)
Ejemplo n.º 14
0
    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)
Ejemplo n.º 15
0
    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))
Ejemplo n.º 16
0
    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)
Ejemplo n.º 17
0
    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))
Ejemplo n.º 18
0
    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)
Ejemplo n.º 19
0
    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)