Source code for graph_tool.spectral

#! /usr/bin/env python
# -*- coding: utf-8 -*-
#
# graph_tool -- a general graph manipulation python module
#
# Copyright (C) 2006-2024 Tiago de Paula Peixoto <tiago@skewed.de>
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""
``graph_tool.spectral``
-----------------------

This module contains several linear operators based on network structure, which
useful for spectral analysis.

Sparse matrices
+++++++++++++++

.. autosummary::
   :nosignatures:
   :toctree: autosummary

   adjacency
   laplacian
   incidence
   transition
   modularity_matrix
   hashimoto

Operator objects
++++++++++++++++

.. autosummary::
   :nosignatures:
   :toctree: autosummary
   :template: class.rst

   AdjacencyOperator
   LaplacianOperator
   IncidenceOperator
   TransitionOperator
   HashimotoOperator
   CompactHashimotoOperator

"""

from .. import _degree, _prop, Graph, GraphView, _limit_args, Vector_int64_t, \
    Vector_double, _parallel
from .. generation import label_self_loops
import numpy
import scipy.sparse
import scipy.sparse.linalg

from .. dl_import import dl_import
dl_import("from . import libgraph_tool_spectral")

__all__ = ["adjacency", "AdjacencyOperator", "laplacian", "LaplacianOperator",
           "incidence", "IncidenceOperator", "transition", "TransitionOperator",
           "modularity_matrix", "hashimoto", "HashimotoOperator",
           "CompactHashimotoOperator"]

def _operator(f):
    text = """.. admonition:: :class:`~scipy.sparse.linalg.LinearOperator` vs. sparse matrices

        For many linear algebra computations it is more efficient to pass
        ``operator=True`` to this function. In this case, it will return a
        :class:`scipy.sparse.linalg.LinearOperator` subclass, which implements
        matrix-vector and matrix-matrix multiplication, and is sufficient for
        the sparse linear algebra operations available in the scipy module
        :mod:`scipy.sparse.linalg`. This avoids copying the whole graph as a
        sparse matrix, and performs the multiplication operations in parallel
        (if enabled during compilation) --- see note below.

    @parallel@

        (The above is only applicable if ``operator == True``, and when the
        object returned is used to perform matrix-vector or matrix-matrix
        multiplications.)
    """
    f.__doc__ = f.__doc__.replace("@operator@", text)
    return f

[docs] @_parallel @_operator def adjacency(g, weight=None, vindex=None, operator=False, csr=True): r"""Return the adjacency matrix of the graph. Parameters ---------- g : :class:`~graph_tool.Graph` Graph to be used. weight : :class:`~graph_tool.EdgePropertyMap` (optional, default: ``None``) Edge property map with the edge weights. vindex : :class:`~graph_tool.VertexPropertyMap` (optional, default: ``None``) Vertex property map specifying the row/column indices. If not provided, the internal vertex index is used. operator : ``bool`` (optional, default: ``False``) If ``True``, a :class:`scipy.sparse.linalg.LinearOperator` subclass is returned, instead of a sparse matrix. csr : ``bool`` (optional, default: ``True``) If ``True``, and ``operator`` is ``False``, a :class:`scipy.sparse.csr_matrix` sparse matrix is returned, otherwise a :class:`scipy.sparse.coo_matrix` is returned instead. Returns ------- A : :class:`~scipy.sparse.csr_matrix` or :class:`AdjacencyOperator` The (sparse) adjacency matrix. Notes ----- For undirected graphs, the adjacency matrix is defined as .. math:: A_{ij} = \begin{cases} 1 & \text{if } (j, i) \in E, \\ 2 & \text{if } i = j \text{ and } (i, i) \in E, \\ 0 & \text{otherwise}, \end{cases} where :math:`E` is the edge set. For directed graphs, we have instead simply .. math:: A_{ij} = \begin{cases} 1 & \text{if } (j, i) \in E, \\ 0 & \text{otherwise}. \end{cases} In the case of weighted edges, the entry values are multiplied by the weight of the respective edge. In the case of networks with parallel edges, the entries in the matrix become simply the edge multiplicities (or twice them for the diagonal, for undirected graphs). .. note:: For directed graphs the definition above means that the entry :math:`A_{ij}` corresponds to the directed edge :math:`j\to i`. Although this is a typical definition in network and graph theory literature, many also use the transpose of this matrix. @operator@ Examples -------- .. testsetup:: import scipy.linalg from pylab import * >>> g = gt.collection.data["polblogs"] >>> A = gt.adjacency(g, operator=True) >>> N = g.num_vertices() >>> ew1 = scipy.sparse.linalg.eigs(A, k=N//2, which="LR", return_eigenvectors=False) >>> ew2 = scipy.sparse.linalg.eigs(A, k=N-N//2, which="SR", return_eigenvectors=False) >>> ew = np.concatenate((ew1, ew2)) >>> figure(figsize=(8, 2)) <...> >>> scatter(real(ew), imag(ew), c=sqrt(abs(ew)), linewidths=0, alpha=0.6) <...> >>> xlabel(r"$\operatorname{Re}(\lambda)$") Text(...) >>> ylabel(r"$\operatorname{Im}(\lambda)$") Text(...) >>> tight_layout() >>> savefig("adjacency-spectrum.svg") .. figure:: adjacency-spectrum.* :align: center Adjacency matrix spectrum for the political blogs network. References ---------- .. [wikipedia-adjacency] http://en.wikipedia.org/wiki/Adjacency_matrix """ if operator: return AdjacencyOperator(g, weight=weight, vindex=vindex) if vindex is None: if g.get_vertex_filter()[0] is not None: vindex = g.new_vertex_property("int64_t") vindex.fa = numpy.arange(g.num_vertices()) else: vindex = g.vertex_index E = g.num_edges() if g.is_directed() else 2 * g.num_edges() data = numpy.zeros(E, dtype="double") i = numpy.zeros(E, dtype="int32") j = numpy.zeros(E, dtype="int32") libgraph_tool_spectral.adjacency(g._Graph__graph, _prop("v", g, vindex), _prop("e", g, weight), data, i, j) if E > 0: V = max(g.num_vertices(), max(i.max() + 1, j.max() + 1)) else: V = g.num_vertices() m = scipy.sparse.coo_matrix((data, (i,j)), shape=(V, V)) if csr: m = m.tocsr() return m
[docs] class AdjacencyOperator(scipy.sparse.linalg.LinearOperator): def __init__(self, g, weight=None, vindex=None): r"""A :class:`scipy.sparse.linalg.LinearOperator` representing the adjacency matrix of a graph. See :func:`adjacency` for details.""" self.g = g self.weight = weight if vindex is None: if g.get_vertex_filter()[0] is not None: self.vindex = g.new_vertex_property("int64_t") self.vindex.fa = numpy.arange(g.num_vertices()) N = g.num_vertices() else: self.vindex = g.vertex_index N = g.num_vertices() else: self.vindex = vindex if vindex is vindex.get_graph().vertex_index: N = g.num_vertices() else: N = vindex.fa.max() + 1 self.shape = (N, N) self.dtype = numpy.dtype("float") def _matvec(self, x): y = numpy.zeros(self.shape[0]) x = numpy.asarray(x, dtype="float") libgraph_tool_spectral.adjacency_matvec(self.g._Graph__graph, _prop("v", self.g, self.vindex), _prop("e", self.g, self.weight), x, y) return y def _matmat(self, x): y = numpy.zeros((self.shape[0], x.shape[1])) x = numpy.asarray(x, dtype="float") libgraph_tool_spectral.adjacency_matvec(self.g._Graph__graph, _prop("v", self.g, self.vindex), _prop("e", self.g, self.weight), x, y) return y def _adjoint(self): if self.g.is_directed(): return AdjacencyOperator(GraphView(self.g, reversed=True), self.weight, self.vindex) else: return self
[docs] @_limit_args({"deg": ["total", "in", "out"]}) @_parallel @_operator def laplacian(g, deg="out", norm=False, weight=None, r=1, vindex=None, operator=False, csr=True): r"""Return the Laplacian (or Bethe Hessian if :math:`r > 1`) matrix of the graph. Parameters ---------- g : :class:`~graph_tool.Graph` Graph to be used. deg : str (optional, default: "out") Degree to be used, in case of a directed graph. norm : bool (optional, default: False) Whether to compute the normalized Laplacian. weight : :class:`~graph_tool.EdgePropertyMap` (optional, default: ``None``) Edge property map with the edge weights. r : ``double`` (optional, default: ``1.``) Regularization parameter. If :math:`r > 1`, and ``norm`` is ``False``, then this corresponds to the Bethe Hessian. (This parameter has an effect only for ``norm == False``.) vindex : :class:`~graph_tool.VertexPropertyMap` (optional, default: ``None``) Vertex property map specifying the row/column indices. If not provided, the internal vertex index is used. operator : ``bool`` (optional, default: ``False``) If ``True``, a :class:`scipy.sparse.linalg.LinearOperator` subclass is returned, instead of a sparse matrix. csr : ``bool`` (optional, default: ``True``) If ``True``, and ``operator`` is ``False``, a :class:`scipy.sparse.csr_matrix` sparse matrix is returned, otherwise a :class:`scipy.sparse.coo_matrix` is returned instead. Returns ------- L : :class:`~scipy.sparse.csr_matrix` or :class:`LaplacianOperator` The (sparse) Laplacian matrix. Notes ----- The weighted Laplacian matrix is defined as .. math:: \ell_{ij} = \begin{cases} \Gamma(v_i) & \text{if } i = j \\ -w_{ij} & \text{if } i \neq j \text{ and } (j, i) \in E \\ 0 & \text{otherwise}. \end{cases} Where :math:`\Gamma(v_i)=\sum_j A_{ij}w_{ij}` is sum of the weights of the edges incident on vertex :math:`v_i`. In case of :math:`r > 1`, the matrix returned is the Bethe Hessian [bethe-hessian]_: .. math:: \ell_{ij} = \begin{cases} \Gamma(v_i) + (r^2 - 1) & \text{if } i = j \\ -r w_{ij} & \text{if } i \neq j \text{ and } (j, i) \in E \\ 0 & \text{otherwise}. \end{cases} The normalized version is .. math:: \ell_{ij} = \begin{cases} 1 & \text{ if } i = j \text{ and } \Gamma(v_i) \neq 0 \\ -\frac{w_{ij}}{\sqrt{\Gamma(v_i)\Gamma(v_j)}} & \text{ if } i \neq j \text{ and } (j, i) \in E \\ 0 & \text{otherwise}. \end{cases} In the case of unweighted edges, it is assumed :math:`w_{ij} = 1`. For directed graphs, it is assumed :math:`\Gamma(v_i)=\sum_j A_{ij}w_{ij} + \sum_j A_{ji}w_{ji}` if ``deg=="total"``, :math:`\Gamma(v_i)=\sum_j A_{ji}w_{ji}` if ``deg=="out"`` or :math:`\Gamma(v_i)=\sum_j A_{ij}w_{ij}` if ``deg=="in"``. .. note:: For directed graphs the definition above means that the entry :math:`\ell_{i,j}` corresponds to the directed edge :math:`j\to i`. Although this is a typical definition in network and graph theory literature, many also use the transpose of this matrix. @operator@ Examples -------- .. testsetup:: import scipy.linalg from pylab import * >>> g = gt.collection.data["polblogs"] >>> L = gt.laplacian(g, operator=True) >>> N = g.num_vertices() >>> ew1 = scipy.sparse.linalg.eigs(L, k=N//2, which="LR", return_eigenvectors=False) >>> ew2 = scipy.sparse.linalg.eigs(L, k=N-N//2, which="SR", return_eigenvectors=False) >>> ew = np.concatenate((ew1, ew2)) >>> figure(figsize=(8, 2)) <...> >>> scatter(real(ew), imag(ew), c=sqrt(abs(ew)), linewidths=0, alpha=0.6) <...> >>> xlabel(r"$\operatorname{Re}(\lambda)$") Text(...) >>> ylabel(r"$\operatorname{Im}(\lambda)$") Text(...) >>> tight_layout() >>> savefig("laplacian-spectrum.svg") .. figure:: laplacian-spectrum.* :align: center Laplacian matrix spectrum for the political blogs network. >>> L = gt.laplacian(g, norm=True, operator=True) >>> ew1 = scipy.sparse.linalg.eigs(L, k=N//2, which="LR", return_eigenvectors=False) >>> ew2 = scipy.sparse.linalg.eigs(L, k=N-N//2, which="SR", return_eigenvectors=False) >>> ew = np.concatenate((ew1, ew2)) >>> figure(figsize=(8, 2)) <...> >>> scatter(real(ew), imag(ew), c=sqrt(abs(ew)), linewidths=0, alpha=0.6) <...> >>> xlabel(r"$\operatorname{Re}(\lambda)$") Text(...) >>> ylabel(r"$\operatorname{Im}(\lambda)$") Text(...) >>> tight_layout() >>> savefig("norm-laplacian-spectrum.svg") .. figure:: norm-laplacian-spectrum.* :align: center Normalized Laplacian matrix spectrum for the political blogs network. References ---------- .. [wikipedia-laplacian] http://en.wikipedia.org/wiki/Laplacian_matrix .. [bethe-hessian] Saade, Alaa, Florent Krzakala, and Lenka Zdeborová. "Spectral clustering of graphs with the bethe hessian." Advances in Neural Information Processing Systems 27 (2014): 406-414, :arxiv:`1406.1880`, https://proceedings.neurips.cc/paper/2014/hash/63923f49e5241343aa7acb6a06a751e7-Abstract.html """ if operator: return LaplacianOperator(g, deg=deg, norm=norm, weight=weight, r=r, vindex=vindex) if vindex is None: if g.get_vertex_filter()[0] is not None: vindex = g.new_vertex_property("int64_t") vindex.fa = numpy.arange(g.num_vertices()) else: vindex = g.vertex_index V = g.num_vertices() nself = int(label_self_loops(g, mark_only=True).a.sum()) E = g.num_edges() - nself if not g.is_directed(): E *= 2 N = E + g.num_vertices() data = numpy.zeros(N, dtype="double") i = numpy.zeros(N, dtype="int32") j = numpy.zeros(N, dtype="int32") if norm: libgraph_tool_spectral.norm_laplacian(g._Graph__graph, _prop("v", g, vindex), _prop("e", g, weight), deg, data, i, j) else: libgraph_tool_spectral.laplacian(g._Graph__graph, _prop("v", g, vindex), _prop("e", g, weight), deg, r, data, i, j) if E > 0: V = max(g.num_vertices(), max(i.max() + 1, j.max() + 1)) else: V = g.num_vertices() m = scipy.sparse.coo_matrix((data, (i, j)), shape=(V, V)) if csr: m = m.tocsr() return m
[docs] class LaplacianOperator(scipy.sparse.linalg.LinearOperator): @_limit_args({"deg": ["total", "in", "out"]}) def __init__(self, g, weight=None, deg="out", r=1, norm=False, vindex=None): r"""A :class:`scipy.sparse.linalg.LinearOperator` representing the laplacian matrix of a graph. See :func:`laplacian` for details.""" self.g = g self.weight = weight self.r = r if vindex is None: if g.get_vertex_filter()[0] is not None: self.vindex = g.new_vertex_property("int64_t") self.vindex.fa = numpy.arange(g.num_vertices()) N = g.num_vertices() else: self.vindex = g.vertex_index N = g.num_vertices() else: self.vindex = vindex if vindex is vindex.get_graph().vertex_index: N = g.num_vertices() else: N = vindex.fa.max() + 1 self.shape = (N, N) self.deg = deg self.d = self.g.degree_property_map(deg, weight) if norm: idx = self.d.a > 0 d = g.new_vp("double") d.a[idx] = 1./numpy.sqrt(self.d.a[idx]) self.d = d else: self.d = self.d.copy("double") self.norm = norm self.dtype = numpy.dtype("float") def _matvec(self, x): y = numpy.zeros(self.shape[0]) x = numpy.asarray(x, dtype="float") if self.norm: matvec = libgraph_tool_spectral.norm_laplacian_matvec matvec(self.g._Graph__graph, _prop("v", self.g, self.vindex), _prop("e", self.g, self.weight), _prop("v", self.g, self.d), x, y) else: matvec = libgraph_tool_spectral.laplacian_matvec matvec(self.g._Graph__graph, _prop("v", self.g, self.vindex), _prop("e", self.g, self.weight), _prop("v", self.g, self.d), self.r, x, y) return y def _matmat(self, x): y = numpy.zeros((self.shape[0], x.shape[1])) x = numpy.asarray(x, dtype="float") if self.norm: matmat = libgraph_tool_spectral.norm_laplacian_matmat matmat(self.g._Graph__graph, _prop("v", self.g, self.vindex), _prop("e", self.g, self.weight), _prop("v", self.g, self.d), x, y) else: matmat = libgraph_tool_spectral.laplacian_matmat matmat(self.g._Graph__graph, _prop("v", self.g, self.vindex), _prop("e", self.g, self.weight), _prop("v", self.g, self.d), self.r, x, y) return y def _adjoint(self): if self.g.is_directed(): deg = self.deg if deg == "in": deg = "out" elif deg == "out": deg = "in" return LaplacianOperator(GraphView(self.g, reversed=True), self.weight, deg=deg, norm=self.norm, vindex=self.vindex) else: return self
[docs] @_parallel @_operator def incidence(g, vindex=None, eindex=None, operator=False, csr=True): r"""Return the incidence matrix of the graph. Parameters ---------- g : :class:`~graph_tool.Graph` Graph to be used. vindex : :class:`~graph_tool.VertexPropertyMap` (optional, default: ``None``) Vertex property map specifying the row indices. If not provided, the internal vertex index is used. eindex : :class:`~graph_tool.EdgePropertyMap` (optional, default: ``None``) Edge property map specifying the column indices. If not provided, the internal edge index is used. operator : ``bool`` (optional, default: ``False``) If ``True``, a :class:`scipy.sparse.linalg.LinearOperator` subclass is returned, instead of a sparse matrix. csr : ``bool`` (optional, default: ``True``) If ``True``, and ``operator`` is ``False``, a :class:`scipy.sparse.csr_matrix` sparse matrix is returned, otherwise a :class:`scipy.sparse.coo_matrix` is returned instead. Returns ------- a : :class:`~scipy.sparse.csr_matrix` or :class:`IncidenceOperator` The (sparse) incidence matrix. Notes ----- For undirected graphs, the incidence matrix is defined as .. math:: b_{i,j} = \begin{cases} 1 & \text{if vertex } v_i \text{and edge } e_j \text{ are incident}, \\ 0 & \text{otherwise} \end{cases} For directed graphs, the definition is .. math:: b_{i,j} = \begin{cases} 1 & \text{if edge } e_j \text{ enters vertex } v_i, \\ -1 & \text{if edge } e_j \text{ leaves vertex } v_i, \\ 0 & \text{otherwise} \end{cases} @operator@ Examples -------- .. testsetup:: import scipy.linalg from pylab import * >>> g = gt.collection.data["polblogs"] >>> B = gt.incidence(g, operator=True) >>> N = g.num_vertices() >>> s1 = scipy.sparse.linalg.svds(B, k=N//2, which="LM", return_singular_vectors=False) >>> s2 = scipy.sparse.linalg.svds(B, k=N-N//2, which="SM", return_singular_vectors=False) >>> s = np.concatenate((s1, s2)) >>> s.sort() >>> figure(figsize=(8, 2)) <...> >>> plot(s, "s") [...] >>> xlabel(r"$i$") Text(...) >>> ylabel(r"$\lambda_i$") Text(...) >>> tight_layout() >>> savefig("polblogs-indidence-svd.svg") .. figure:: polblogs-indidence-svd.* :align: center Incidence singular values for the political blogs network. References ---------- .. [wikipedia-incidence] http://en.wikipedia.org/wiki/Incidence_matrix """ if operator: return IncidenceOperator(g, vindex=vindex, eindex=eindex) if vindex is None: if g.get_edge_filter()[0] is not None: vindex = g.new_vertex_property("int64_t") vindex.fa = numpy.arange(g.num_vertices()) else: vindex = g.vertex_index if eindex is None: if g.get_edge_filter()[0] is not None: eindex = g.new_edge_property("int64_t") eindex.fa = numpy.arange(g.num_edges()) else: eindex = g.edge_index E = g.num_edges() if E == 0: raise ValueError("Cannot construct incidence matrix for a graph with no edges.") data = numpy.zeros(2 * E, dtype="double") i = numpy.zeros(2 * E, dtype="int32") j = numpy.zeros(2 * E, dtype="int32") libgraph_tool_spectral.incidence(g._Graph__graph, _prop("v", g, vindex), _prop("e", g, eindex), data, i, j) m = scipy.sparse.coo_matrix((data, (i,j))) if csr: m = m.tocsr() return m
[docs] class IncidenceOperator(scipy.sparse.linalg.LinearOperator): def __init__(self, g, vindex=None, eindex=None, transpose=False): r"""A :class:`scipy.sparse.linalg.LinearOperator` representing the incidence matrix of a graph. See :func:`incidence` for details. """ self.g = g self.transpose = transpose self.dtype = numpy.dtype("float") if vindex is None: if g.get_vertex_filter()[0] is not None: self.vindex = g.new_vertex_property("int64_t") self.vindex.fa = numpy.arange(g.num_vertices()) N = g.num_vertices() else: self.vindex = g.vertex_index N = g.num_vertices() else: self.vindex = vindex if vindex is vindex.get_graph().vertex_index: N = g.num_vertices() else: N = vindex.fa.max() + 1 if eindex is None: if g.get_edge_filter()[0] is not None: self.eindex = g.new_edge_property("int64_t") self.eindex.fa = numpy.arange(g.num_edges()) E = g.num_edges() else: self.eindex = g.edge_index E = g.edge_index_range else: self.eindex = eindex if eindex is g.edge_index: E = g.edge_index_range else: E = self.eindex.fa.max() + 1 if not transpose: self.shape = (N, E) else: self.shape = (E, N) def _matvec(self, x): y = numpy.zeros(self.shape[0]) x = numpy.asarray(x, dtype="float") libgraph_tool_spectral.incidence_matvec(self.g._Graph__graph, _prop("v", self.g, self.vindex), _prop("e", self.g, self.eindex), x, y, self.transpose) return y def _matmat(self, x): y = numpy.zeros((self.shape[0], x.shape[1])) x = numpy.asarray(x, dtype="float") libgraph_tool_spectral.incidence_matmat(self.g._Graph__graph, _prop("v", self.g, self.vindex), _prop("e", self.g, self.eindex), x, y, self.transpose) return y def _adjoint(self): return IncidenceOperator(self.g, vindex=self.vindex, eindex=self.eindex, transpose=not self.transpose)
[docs] @_parallel @_operator def transition(g, weight=None, vindex=None, operator=False, csr=True): r"""Return the transition matrix of the graph. Parameters ---------- g : :class:`~graph_tool.Graph` Graph to be used. weight : :class:`~graph_tool.EdgePropertyMap` (optional, default: ``None``) Edge property map with the edge weights. vindex : :class:`~graph_tool.VertexPropertyMap` (optional, default: ``None``) Vertex property map specifying the row/column indices. If not provided, the internal vertex index is used. operator : ``bool`` (optional, default: ``False``) If ``True``, a :class:`scipy.sparse.linalg.LinearOperator` subclass is returned, instead of a sparse matrix. csr : ``bool`` (optional, default: ``True``) If ``True``, and ``operator`` is ``False``, a :class:`scipy.sparse.csr_matrix` sparse matrix is returned, otherwise a :class:`scipy.sparse.coo_matrix` is returned instead. Returns ------- T : :class:`~scipy.sparse.csr_matrix` or :class:`TransitionOperator` The (sparse) transition matrix. Notes ----- The transition matrix is defined as .. math:: T_{ij} = \frac{A_{ij}}{k_j} where :math:`k_i = \sum_j A_{ji}`, and :math:`A_{ij}` is the adjacency matrix. In the case of weighted edges, the values of the adjacency matrix are multiplied by the edge weights. .. note:: For directed graphs the definition above means that the entry :math:`T_{ij}` corresponds to the directed edge :math:`j\to i`. Although this is a typical definition in network and graph theory literature, many also use the transpose of this matrix. @operator@ Examples -------- .. testsetup:: import scipy.linalg from pylab import * >>> g = gt.collection.data["polblogs"] >>> T = gt.transition(g, operator=True) >>> N = g.num_vertices() >>> ew1 = scipy.sparse.linalg.eigs(T, k=N//2, which="LR", return_eigenvectors=False) >>> ew2 = scipy.sparse.linalg.eigs(T, k=N-N//2, which="SR", return_eigenvectors=False) >>> ew = np.concatenate((ew1, ew2)) >>> figure(figsize=(8, 2)) <...> >>> scatter(real(ew), imag(ew), c=sqrt(abs(ew)), linewidths=0, alpha=0.6) <...> >>> xlabel(r"$\operatorname{Re}(\lambda)$") Text(...) >>> ylabel(r"$\operatorname{Im}(\lambda)$") Text(...) >>> tight_layout() >>> savefig("transition-spectrum.svg") .. figure:: transition-spectrum.* :align: center Transition matrix spectrum for the political blogs network. References ---------- .. [wikipedia-transition] https://en.wikipedia.org/wiki/Stochastic_matrix """ if operator: return TransitionOperator(g, weight=weight, vindex=vindex) if vindex is None: if g.get_vertex_filter()[0] is not None: vindex = g.new_vertex_property("int64_t") vindex.fa = numpy.arange(g.num_vertices()) else: vindex = g.vertex_index E = g.num_edges() if g.is_directed() else 2 * g.num_edges() data = numpy.zeros(E, dtype="double") i = numpy.zeros(E, dtype="int32") j = numpy.zeros(E, dtype="int32") libgraph_tool_spectral.transition(g._Graph__graph, _prop("v", g, vindex), _prop("e", g, weight), data, i, j) if E > 0: V = max(g.num_vertices(), max(i.max() + 1, j.max() + 1)) else: V = g.num_vertices() m = scipy.sparse.coo_matrix((data, (i,j)), shape=(V, V)) if csr: m = m.tocsr() return m
[docs] class TransitionOperator(scipy.sparse.linalg.LinearOperator): def __init__(self, g, weight=None, transpose=False, inv_d=None, vindex=None): r"""A :class:`scipy.sparse.linalg.LinearOperator` representing the transition matrix of a graph. See :func:`transition` for details. """ self.g = g self.weight = weight if vindex is None: if g.get_vertex_filter()[0] is not None: self.vindex = g.new_vertex_property("int64_t") self.vindex.fa = numpy.arange(g.num_vertices()) N = g.num_vertices() else: self.vindex = g.vertex_index N = g.num_vertices() else: self.vindex = vindex if vindex is vindex.get_graph().vertex_index: N = g.num_vertices() else: N = vindex.fa.max() + 1 self.shape = (N, N) if inv_d is None: d = self.g.degree_property_map("out", weight) nd = g.new_vp("double") idx = d.a > 0 nd.a[idx] = 1/d.a[idx] self.d = nd else: self.d = inv_d.copy("double") self.dtype = numpy.dtype("float") self.transpose = transpose def _matvec(self, x): y = numpy.zeros(self.shape[0]) x = numpy.asarray(x, dtype="float") libgraph_tool_spectral.transition_matvec(self.g._Graph__graph, _prop("v", self.g, self.vindex), _prop("e", self.g, self.weight), _prop("v", self.g, self.d), x, y, self.transpose) return y def _matmat(self, x): y = numpy.zeros((self.shape[0], x.shape[1])) x = numpy.asarray(x, dtype="float") libgraph_tool_spectral.transition_matmat(self.g._Graph__graph, _prop("v", self.g, self.vindex), _prop("e", self.g, self.weight), _prop("v", self.g, self.d), x, y, self.transpose) return y def _adjoint(self): if self.g.is_directed(): g = GraphView(self.g, reversed=True) else: g = self.g return TransitionOperator(g, self.weight, inv_d=self.d, transpose=not self.transpose, vindex=self.vindex)
[docs] @_parallel @_operator def modularity_matrix(g, weight=None, vindex=None): r"""Return the modularity matrix of the graph. Parameters ---------- g : :class:`~graph_tool.Graph` Graph to be used. weight : :class:`~graph_tool.EdgePropertyMap` (optional, default: ``None``) Edge property map with the edge weights. index : :class:`~graph_tool.VertexPropertyMap` (optional, default: ``None``) Vertex property map specifying the row/column indices. If not provided, the internal vertex index is used. Returns ------- B : :class:`~scipy.sparse.linalg.LinearOperator` The (sparse) modularity matrix, represented as a :class:`~scipy.sparse.linalg.LinearOperator`. Notes ----- The modularity matrix is defined as .. math:: B_{ij} = A_{ij} - \frac{k^+_i k^-_j}{2E} where :math:`k^+_i = \sum_j A_{ji}`, :math:`k^-_i = \sum_j A_{ij}`, :math:`2E=\sum_{ij}A_{ij}` and :math:`A_{ij}` is the adjacency matrix. In the case of weighted edges, the values of the adjacency matrix are multiplied by the edge weights. Examples -------- .. testsetup:: import scipy.linalg from pylab import * >>> g = gt.collection.data["polblogs"] >>> B = gt.modularity_matrix(g) >>> N = g.num_vertices() >>> ew1 = scipy.sparse.linalg.eigs(B, k=N//2, which="LR", return_eigenvectors=False) >>> ew2 = scipy.sparse.linalg.eigs(B, k=N-N//2, which="SR", return_eigenvectors=False) >>> ew = np.concatenate((ew1, ew2)) >>> figure(figsize=(8, 2)) <...> >>> scatter(real(ew), imag(ew), c=sqrt(abs(ew)), linewidths=0, alpha=0.6) <...> >>> xlabel(r"$\operatorname{Re}(\lambda)$") Text(...) >>> ylabel(r"$\operatorname{Im}(\lambda)$") Text(...) >>> autoscale() >>> tight_layout() >>> savefig("modularity-spectrum.svg") .. figure:: modularity-spectrum.* :align: center Modularity matrix spectrum for the political blogs network. References ---------- .. [newman-modularity] M. E. J. Newman, M. Girvan, "Finding and evaluating community structure in networks", Phys. Rev. E 69, 026113 (2004). :doi:`10.1103/PhysRevE.69.026113` """ A = adjacency(g, weight=weight, vindex=vindex, operator=True) A_T = A.adjoint() if g.is_directed(): k_in = g.degree_property_map("in", weight=weight).fa else: k_in = g.degree_property_map("out", weight=weight).fa k_out = g.degree_property_map("out", weight=weight).fa N = g.num_vertices() E2 = float(k_out.sum()) def matvec(x): M = x.shape[0] if len(x.shape) > 1: x = x.reshape(M) nx = A.matvec(x) - k_out * numpy.dot(k_in, x) / E2 return nx def rmatvec(x): M = x.shape[0] if len(x.shape) > 1: x = x.reshape(M) nx = A_T.matvec(x) - k_in * numpy.dot(k_out, x) / E2 return nx B = scipy.sparse.linalg.LinearOperator((g.num_vertices(), g.num_vertices()), matvec=matvec, rmatvec=rmatvec, dtype="float") return B
[docs] @_parallel @_operator def hashimoto(g, index=None, compact=False, operator=False, csr=True): r"""Return the Hashimoto (or non-backtracking) matrix of a graph. Parameters ---------- g : :class:`~graph_tool.Graph` Graph to be used. index : :class:`~graph_tool.VertexPropertyMap` (optional, default: ``None``) Edge property map specifying the row/column indices. If not provided, the internal edge index is used. compact : ``boolean`` (optional, default: ``False``) If ``True``, a compact :math:`2|V|\times 2|V|` version of the matrix is returned. operator : ``bool`` (optional, default: ``False``) If ``True``, a :class:`scipy.sparse.linalg.LinearOperator` subclass is returned, instead of a sparse matrix. csr : ``bool`` (optional, default: ``True``) If ``True``, and ``operator`` is ``False``, a :class:`scipy.sparse.csr_matrix` sparse matrix is returned, otherwise a :class:`scipy.sparse.coo_matrix` is returned instead. Returns ------- H : :class:`~scipy.sparse.csr_matrix` or :class:`HashimotoOperator` or :class:`CompactHashimotoOperator` The (sparse) Hashimoto matrix. Notes ----- The Hashimoto (a.k.a. non-backtracking) matrix [hashimoto]_ is defined as .. math:: h_{k\to l,i\to j} = \begin{cases} 1 & \text{if } (k,l) \in E, (i,j) \in E, l=i, k\ne j,\\ 0 & \text{otherwise}, \end{cases} where :math:`E` is the edge set. It is therefore a :math:`2|E|\times 2|E|` asymmetric square matrix (or :math:`|E|\times |E|` for directed graphs), indexed over edge directions. If the option ``compact=True`` is passed, the matrix returned has shape :math:`2|V|\times 2|V|`, where :math:`|V|` is the number of vertices, and is given by .. math:: \boldsymbol h = \left(\begin{array}{c|c} \boldsymbol A & -\boldsymbol 1 \\ \hline \boldsymbol D-\boldsymbol 1 & \boldsymbol 0 \end{array}\right) where :math:`\boldsymbol A` is the adjacency matrix, and :math:`\boldsymbol D` is the diagonal matrix with the node degrees [krzakala_spectral]_. @operator@ Examples -------- .. testsetup:: import scipy.linalg from pylab import * >>> g = gt.collection.data["football"] >>> H = gt.hashimoto(g, operator=True) >>> N = 2 * g.num_edges() >>> ew1 = scipy.sparse.linalg.eigs(H, k=N//2, which="LR", return_eigenvectors=False) >>> ew2 = scipy.sparse.linalg.eigs(H, k=N-N//2, which="SR", return_eigenvectors=False) >>> ew = np.concatenate((ew1, ew2)) >>> figure(figsize=(8, 4)) <...> >>> scatter(real(ew), imag(ew), c=sqrt(abs(ew)), linewidths=0, alpha=0.6) <...> >>> xlabel(r"$\operatorname{Re}(\lambda)$") Text(...) >>> ylabel(r"$\operatorname{Im}(\lambda)$") Text(...) >>> tight_layout() >>> savefig("hashimoto-spectrum.svg") .. figure:: hashimoto-spectrum.* :align: center Hashimoto matrix spectrum for the network of American football teams. References ---------- .. [hashimoto] Hashimoto, Ki-ichiro. "Zeta functions of finite graphs and representations of p-adic groups." Automorphic forms and geometry of arithmetic varieties. 1989. 211-280. :DOI:`10.1016/B978-0-12-330580-0.50015-X` .. [krzakala_spectral] Florent Krzakala, Cristopher Moore, Elchanan Mossel, Joe Neeman, Allan Sly, Lenka Zdeborová, and Pan Zhang, "Spectral redemption in clustering sparse networks", PNAS 110 (52) 20935-20940, 2013. :doi:`10.1073/pnas.1312486110`, :arxiv:`1306.5550` """ if compact: if operator: return CompactHashimotoOperator(g) i = Vector_int64_t() j = Vector_int64_t() x = Vector_double() libgraph_tool_spectral.compact_nonbacktracking(g._Graph__graph, i, j, x) N = g.num_vertices(ignore_filter=True) m = scipy.sparse.coo_matrix((x, (i.a,j.a)), shape=(2 * N, 2 * N)) else: if operator: return HashimotoOperator(g, eindex=index) if index is None: if g.get_edge_filter()[0] is not None: index = g.new_edge_property("int64_t") index.fa = numpy.arange(g.num_edges()) E = index.fa.max() + 1 else: index = g.edge_index E = g.edge_index_range if not g.is_directed(): E *= 2 i = Vector_int64_t() j = Vector_int64_t() libgraph_tool_spectral.nonbacktracking(g._Graph__graph, _prop("e", g, index), i, j) data = numpy.ones(i.a.shape) m = scipy.sparse.coo_matrix((data, (i.a,j.a)), shape=(E, E)) if csr: m = m.tocsr() return m
[docs] class HashimotoOperator(scipy.sparse.linalg.LinearOperator): def __init__(self, g, eindex=None, transpose=False): r"""A :class:`scipy.sparse.linalg.LinearOperator` representing the hashimoto matrix of a graph. See :func:`hashimoto` for details. """ self.g = g if eindex is None: if g.get_edge_filter()[0] is not None: self.eindex = g.new_edge_property("int64_t") self.eindex.fa = numpy.arange(g.num_edges()) E = g.num_edges() else: self.eindex = g.edge_index E = g.edge_index_range else: self.eindex = eindex if eindex is g.edge_index: E = g.edge_index_range else: E = self.eindex.fa.max() + 1 if g.is_directed(): self.shape = (E, E) else: self.shape = (2 * E, 2 * E) self.dtype = numpy.dtype("float") self.transpose = transpose def _matvec(self, x): y = numpy.zeros(self.shape[0]) x = numpy.asarray(x, dtype="float") libgraph_tool_spectral.nonbacktracking_matvec(self.g._Graph__graph, _prop("e", self.g, self.eindex), x, y, self.transpose) return y def _matmat(self, x): y = numpy.zeros((self.shape[0], x.shape[1])) x = numpy.asarray(x, dtype="float") libgraph_tool_spectral.nonbacktracking_matmat(self.g._Graph__graph, _prop("e", self.g, self.eindex), x, y, self.transpose) return y def _adjoint(self): if self.g.is_directed(): g = GraphView(self.g, reversed=True) else: g = self.g return HashimotoOperator(g, self.eindex, transpose=not self.transpose)
[docs] class CompactHashimotoOperator(scipy.sparse.linalg.LinearOperator): def __init__(self, g, vindex=None, transpose=False): r"""A :class:`scipy.sparse.linalg.LinearOperator` representing the compact hashimoto matrix of a graph. See :func:`hashimoto` for details. """ self.g = g if vindex is None: if g.get_vertex_filter()[0] is not None: self.vindex = g.new_vertex_property("int64_t") self.vindex.fa = numpy.arange(g.num_vertices()) N = g.num_vertices() else: self.vindex = g.vertex_index N = g.num_vertices() else: self.vindex = vindex if vindex is vindex.get_graph().vertex_index: N = g.num_vertices() else: N = vindex.fa.max() + 1 self.shape = (2 * N, 2 * N) self.dtype = numpy.dtype("float") self.transpose = transpose def _matvec(self, x): y = numpy.zeros(self.shape[0]) x = numpy.asarray(x, dtype="float") libgraph_tool_spectral.compact_nonbacktracking_matvec(self.g._Graph__graph, _prop("v", self.g, self.vindex), x, y, self.transpose) return y def _matmat(self, x): y = numpy.zeros((self.shape[0], x.shape[1])) x = numpy.asarray(x, dtype="float") libgraph_tool_spectral.compact_nonbacktracking_matmat(self.g._Graph__graph, _prop("v", self.g, self.vindex), x, y, self.transpose) return y def _adjoint(self): if self.g.is_directed(): g = GraphView(self.g, reversed=True) else: g = self.g return CompactHashimotoOperator(g, self.vindex, transpose=not self.transpose)