# Source code for netrd.distance.quantum_jsd

"""
quantum_jsd.py
--------------------------

Graph distance based on the quantum $q$-Jenson-Shannon divergence.

De Domenico, Manlio, and Jacob Biamonte. 2016. “Spectral Entropies as
Information-Theoretic Tools for Complex Network Comparison.” Physical Review X
6 (4). https://doi.org/10.1103/PhysRevX.6.041062.

author: Stefan McCabe & Brennan Klein
email:
Submitted as part of the 2019 NetSI Collabathon.

"""

import warnings
import networkx as nx
import numpy as np
from scipy.linalg import expm
from .base import BaseDistance
from ..utilities import undirected, unweighted

[docs]class QuantumJSD(BaseDistance):
"""Compares the spectral entropies of the density matrices."""

[docs]    @undirected
@unweighted
def dist(self, G1, G2, beta=0.1, q=None):
r"""Square root of the quantum :math:q-Jensen-Shannon divergence between two
graphs.

The generalized Jensen-Shannon divergence compares two graphs b √(H0 - 0.5 * (H1 + H2))y the
spectral entropies of their quantum-statistical-mechanical density
matrices. It can be written as

.. math::
\mathcal{J}_q(\mathbf{\rho} || \mathbf{\sigma}) =
S_q\left( \frac{\mathbf{\rho} + \mathbf{\sigma}}{2} \right) -
\frac{1}{2} [S_q(\mathbf{\rho}) + S_q(\mathbf{\sigma})],

where :math:\mathbf{\rho} and :math:\mathbf{\sigma} are density
matrices and :math:q is the order parameter.

The density matrix

.. math::
\mathbf{\rho} = \frac{e^{-\beta\mathbf{L}}}{Z},

where

.. math::
Z = \sum_{i=1}^{N}e^{-\beta\lambda_i(\mathbf{L})}

and :math:\lambda_i(\mathbf{L}) is the :math:ith eigenvalue of the Laplacian
matrix :math:L, represents an imaginary diffusion process over the network
with time parameter :math:\beta > 0.

For these density matrices and the mixture matrix, we calculate the
Rényi entropy of order :math:q

.. math::
S_q = \frac{1}{1-q} \log_2 \sum_{i=1}^{N}\lambda_i(\mathbf{\rho})^q,

or, if :math:q=1, the Von Neumann entropy

.. math::
S_1 = - \sum_{i=1}^{N}\lambda_i(\mathbf{\rho})\log_2\lambda_i(\mathbf{\rho}).

Note that this implementation is not exact because the matrix
exponentiation is performed using the Padé approximation and
because of imprecision in the calculation of the eigenvalues of the
density matrix.

Parameters
----------

G1, G2 (nx.Graph)
two networkx graphs to be compared

beta (float)
time parameter for diffusion propagator

q (float)
order parameter for Rényi entropy. If None or 1, use the Von
Neumann entropy (i.e., Shannon entropy) instead.

Returns
-------

dist (float)
the distance between G1 and G2.

References
----------

.. [1] De Domenico, Manlio, and Jacob Biamonte. 2016. "Spectral
Entropies as Information-Theoretic Tools for Complex Network
Comparison." Physical Review X 6
(4). https://doi.org/10.1103/PhysRevX.6.041062.

"""
if beta <= 0:
raise ValueError("beta must be positive.")

if q and q >= 2:
warnings.warn("JSD is only a metric for 0 ≤ q < 2.", RuntimeWarning)

def density_matrix(A, beta):
"""
Create the density matrix encoding probabilities for entropies.
This is done using a fictive diffusion process with time parameter
:math:beta.
"""
L = np.diag(np.sum(A, axis=1)) - A
rho = expm(-1 * beta * L)
rho = rho / np.trace(rho)

return rho

def renyi_entropy(X, q=None):
"""
Calculate the Rényi entropy with order :math:q, or the Von Neumann
entropy if :math:q is None or 1.
"""
# Note that where there are many zero eigenvalues (i.e., large
# values of beta) in the density matrix, floating-point precision
# issues mean that there will be negative eigenvalues and the
# eigenvalues will not sum to precisely one. To avoid encountering
# nans in np.log2, we remove all eigenvalues that are close
# to zero within 1e-6 tolerance. As for the eigenvalues not summing
# to exactly one, this is a small source of error in the
# calculation.
eigs = np.linalg.eigvalsh(X)
zero_eigenvalues = np.isclose(np.abs(eigs), 0, atol=1e-6)
eigs = eigs[np.logical_not(zero_eigenvalues)]

if q is None or q == 1:
# plain Von Neumann entropy
H = -1 * np.sum(eigs * np.log2(eigs))
else:
prefactor = 1 / (1 - q)
H = prefactor * np.log2((eigs**q).sum())
return H

A1 = nx.to_numpy_array(G1)
A2 = nx.to_numpy_array(G2)

rho1 = density_matrix(A1, beta)
rho2 = density_matrix(A2, beta)
mix = (rho1 + rho2) / 2

H0 = renyi_entropy(mix, q)
H1 = renyi_entropy(rho1, q)
H2 = renyi_entropy(rho2, q)

dist = np.sqrt(H0 - 0.5 * (H1 + H2))

self.results['density_matrix_1'] = rho1
self.results['density_matrix_2'] = rho2
self.results['mixture_matrix'] = mix
self.results['entropy_1'] = H1
self.results['entropy_2'] = H2
self.results['entropy_mixture'] = H0
self.results['dist'] = dist
return dist