"""
Communicability.
"""

import networkx as nx
from networkx.utils import not_implemented_for

__all__ = ["communicability", "communicability_exp"]


@not_implemented_for("directed")
@not_implemented_for("multigraph")
@nx._dispatchable
def communicability(G):
    r"""Returns communicability between all pairs of nodes in `G`.

    The communicability between pairs of nodes in `G` is the sum of
    walks of different lengths starting at node ``u`` and ending at node ``v``.

    Parameters
    ----------
    G : graph

    Returns
    -------
    comm : dictionary of dictionaries
        Dictionary of dictionaries keyed by nodes with communicability
        as the value.

    Raises
    ------
    NetworkXError
       If the graph is not undirected and simple.

    See Also
    --------
    communicability_exp:
       Communicability between all pairs of nodes in `G` using matrix exponentiation
    communicability_betweenness_centrality:
       Communicability betweenness centrality for each node in `G`.

    Notes
    -----
    This algorithm uses a spectral decomposition of the adjacency matrix.
    Let $G=(V,E)$ be a simple undirected graph.  Using the connection between
    the powers of the adjacency matrix and the number of walks in the graph,
    the communicability between nodes ``u`` and ``v`` based on the graph spectrum
    is:

    .. math::
        C(u,v)=\sum_{j=1}^{n}\phi_{j}(u)\phi_{j}(v)e^{\lambda_{j}}

    where $\phi_{j}(u)$ is the $u\rm{th}$ element of the $j\rm{th}$ orthonormal
    eigenvector of the adjacency matrix associated with the eigenvalue
    $\lambda_{j}$ [1]_.

    References
    ----------
    .. [1] Ernesto Estrada, Naomichi Hatano,
       "Communicability in complex networks",
       Phys. Rev. E 77, 036111 (2008).
       https://arxiv.org/abs/0707.0756

    Examples
    --------
    >>> G = nx.Graph([(0, 1), (1, 2), (1, 5), (5, 4), (2, 4), (2, 3), (4, 3), (3, 6)])
    >>> c = nx.communicability(G)
    """
    import numpy as np

    A = nx.to_numpy_array(G, weight=None)
    w, vec = np.linalg.eigh(A)
    communicability = (vec * np.exp(w)) @ vec.T

    # Convert to dict-of-dict keyed by nodes to the communicability value
    return {
        u: {v: float(communicability[i][j]) for j, v in enumerate(G)}
        for i, u in enumerate(G)
    }


@not_implemented_for("directed")
@not_implemented_for("multigraph")
@nx._dispatchable
def communicability_exp(G):
    r"""Returns communicability between all pairs of nodes in `G`.

    Communicability between pair of node ``(u, v)`` of node in `G` is the sum of
    walks of different lengths starting at node ``u`` and ending at node ``v``.

    Parameters
    ----------
    G : graph

    Returns
    -------
    comm : dictionary of dictionaries
        Dictionary of dictionaries keyed by nodes with communicability
        as the value.

    Raises
    ------
    NetworkXError
        If the graph is not undirected and simple.

    See Also
    --------
    communicability:
       Communicability between pairs of nodes in `G` via spectral decomposition
    communicability_betweenness_centrality:
       Communicability betweenness centrality for each node in G.

    Notes
    -----
    This algorithm uses matrix exponentiation of the adjacency matrix.

    Let $G=(V,E)$ be a simple undirected graph. Using the connection between
    the powers  of the adjacency matrix and the number of walks in the graph,
    the communicability between nodes ``u`` and ``v`` is:

    .. math::
        C(u,v) = (e^A)_{uv},

    where ``A`` is the adjacency matrix of `G` [1]_.

    References
    ----------
    .. [1] Ernesto Estrada, Naomichi Hatano,
       "Communicability in complex networks",
       Phys. Rev. E 77, 036111 (2008).
       https://arxiv.org/abs/0707.0756

    Examples
    --------
    >>> G = nx.Graph([(0, 1), (1, 2), (1, 5), (5, 4), (2, 4), (2, 3), (4, 3), (3, 6)])
    >>> c = nx.communicability_exp(G)
    """
    import scipy as sp

    nodelist = list(G)  # ordering of nodes in matrix
    A = nx.to_numpy_array(G, nodelist)
    # convert to 0-1 matrix
    A[A != 0.0] = 1
    # communicability matrix
    expA = sp.linalg.expm(A)
    mapping = dict(zip(nodelist, range(len(nodelist))))
    c = {}
    for u in G:
        c[u] = {}
        for v in G:
            c[u][v] = float(expA[mapping[u], mapping[v]])
    return c
