Source code for driada.network.quantum

from .matrix_utils import get_norm_laplacian, get_laplacian
import numpy as np
import scipy
from scipy.linalg import expm
import math


[docs] def renyi_divergence(A, B, q): """Calculate the quantum Rényi divergence between two density matrices. The quantum Rényi divergence generalizes the quantum relative entropy (Kullback-Leibler divergence) and quantifies distinguishability between quantum states. Parameters ---------- A : numpy.ndarray First density matrix (must be square, positive semi-definite, trace 1). B : numpy.ndarray Second density matrix (must be same shape as A). q : float Order parameter. Must be positive. q=1 gives quantum relative entropy. Returns ------- float The quantum Rényi divergence D_q(A||B) in bits. Raises ------ ValueError If q <= 0. If A and B have different shapes or are not square matrices. See Also -------- js_divergence : Quantum Jensen-Shannon divergence. manual_entropy : Shannon/von Neumann entropy calculation. Notes ----- The quantum Rényi divergence is defined as: - For q = 1: D_1(ρ||σ) = Tr(ρ(log₂ ρ - log₂ σ)) - For q ≠ 1: D_q(ρ||σ) = (1/(q-1)) log₂(Tr(ρ^q σ^(1-q))) Properties: - D_q(ρ||σ) ≥ 0 with equality iff ρ = σ - Not symmetric: D_q(ρ||σ) ≠ D_q(σ||ρ) in general - For classical (diagonal) states, reduces to classical Rényi divergence References ---------- Müller-Lennert, M., et al. (2013). On quantum Rényi entropies: A new generalization and some properties. J. Math. Phys. 54, 122203. Examples -------- >>> rho = np.array([[0.7, 0.1], [0.1, 0.3]]) >>> sigma = np.array([[0.5, 0.0], [0.0, 0.5]]) >>> div = renyi_divergence(rho, sigma, q=0.5)""" if q <= 0: raise ValueError("q must be > 0") elif q == 1: answer = np.trace(np.dot(A, (scipy.linalg.logm(A) - scipy.linalg.logm(B)) / np.log(2.0))) else: answer = ( (1 / (q - 1)) * np.log( np.trace( np.dot( scipy.linalg.fractional_matrix_power(A, q), scipy.linalg.fractional_matrix_power(B, 1 - q), ) ) ) / np.log(2.0) ) return answer
[docs] def get_density_matrix(A, t, norm=0): """Compute quantum density matrix from graph adjacency matrix. Constructs a quantum-like Gibbs state density matrix from a graph using the graph Laplacian, following De Domenico & Biamonte's formulation. Parameters ---------- A : numpy.ndarray Adjacency matrix of the graph (must be square). t : float Inverse temperature parameter β (also interpreted as time). Controls the "quantumness" of the state. Must be positive. norm : int, optional If 1, use normalized Laplacian. If 0, use regular Laplacian. Default is 0. Returns ------- numpy.ndarray Density matrix ρ = exp(-tL) / Z(t), where L is the Laplacian and Z(t) = Tr[exp(-tL)] is the partition function. Raises ------ ValueError If A is not square or t is not positive. See Also -------- renyi_divergence : Uses density matrices for divergence calculation. js_divergence : Uses density matrices for network comparison. ~driada.network.matrix_utils.get_laplacian : Computes graph Laplacian. ~driada.network.matrix_utils.get_norm_laplacian : Computes normalized Laplacian. Notes ----- This density matrix is formally proportional to the propagator of a diffusive process on the network. The construction treats the Laplacian as a Hamiltonian in a quantum Gibbs state: ρ = (1/Z) exp(-βH), where H = L References ---------- De Domenico, M., & Biamonte, J. (2016). Spectral entropies as information-theoretic tools for complex network comparison. Physical Review X, 6(4), 041062. Examples -------- >>> A = np.array([[0, 1, 1], [1, 0, 1], [1, 1, 0]]) >>> rho = get_density_matrix(A, t=1.0) >>> np.trace(rho) # Trace should be 1 np.float64(1.0)""" A = A.astype(float) if norm: X = get_norm_laplacian(A) else: X = get_laplacian(A) R = expm(-t * X) R = R / np.trace(R) # Normalize by trace to ensure Tr(ρ) = 1 return R
[docs] def manual_entropy(pr): """Calculate Shannon entropy from probability distribution. Computes entropy while handling numerical issues with zero and near-zero probabilities. Parameters ---------- pr : numpy.ndarray Probability distribution. Non-negative values that should sum to 1. Can also be eigenvalues of a density matrix for von Neumann entropy. Returns ------- float Shannon entropy in bits. See Also -------- scipy.stats.entropy : Alternative implementation. renyi_divergence : Generalized entropy measure. js_divergence : Uses this for von Neumann entropy calculation. Notes ----- The function filters out zero values and very small values (< 1e-15) to avoid numerical issues with logarithms. For quantum states, this computes the von Neumann entropy when given eigenvalues of a density matrix: S(ρ) = -Σᵢ λᵢ log₂(λᵢ). References ---------- Shannon, C.E. (1948). A mathematical theory of communication. Bell System Technical Journal, 27(3), 379-423. Examples -------- >>> pr = np.array([0.5, 0.5, 0.0]) >>> H = manual_entropy(pr) >>> np.isclose(H, 1.0) # Maximum entropy for 2 equiprobable states np.True_""" probs = np.trim_zeros(pr) probs = probs[np.where(probs > 1e-15)] return -np.real(np.sum(np.multiply(probs, np.log2(probs))))
[docs] def js_divergence(A, B, t, return_partial_entropies=True): """Calculate quantum Jensen-Shannon divergence between two graphs. Computes the quantum generalization of JS divergence using von Neumann entropy of density matrices derived from graph Laplacians. Parameters ---------- A : numpy.ndarray Adjacency matrix of first graph (must be square). B : numpy.ndarray Adjacency matrix of second graph (must be same shape as A). t : float Inverse temperature parameter β for density matrix computation. Must be positive. return_partial_entropies : bool, optional If True, return individual entropies along with JS divergence. Default is True. Returns ------- float or tuple If return_partial_entropies=False: Square root of QJSD value. If return_partial_entropies=True: tuple of (S(ρ_mix), S(ρ_A), S(ρ_B), sqrt(QJSD)). Raises ------ ValueError If A and B have different shapes or are not square matrices. See Also -------- renyi_divergence : Alternative quantum divergence measure. get_density_matrix : Used to compute density matrices. manual_entropy : Used for von Neumann entropy calculation. Notes ----- The quantum Jensen-Shannon divergence is defined as: QJSD(ρ_A, ρ_B) = S((ρ_A + ρ_B)/2) - (S(ρ_A) + S(ρ_B))/2 where S(ρ) = -Tr(ρ log₂ ρ) is the von Neumann entropy. **Important**: This function returns the SQUARE ROOT of QJSD, which is a proper metric on the quantum state space. To get the divergence itself, square the returned value. The function quantifies the distinguishability between two network structures in a quantum information context. Returns 0 if calculation results in negative values due to numerical precision issues (which can occur for very similar graphs). References ---------- Lamberti, P., et al. (2008). Jensen-Shannon divergence as a measure of distinguishability between mixed quantum states. Physical Review A. Examples -------- >>> A = np.array([[0, 1], [1, 0]]) >>> B = np.array([[0, 1], [1, 0]]) >>> js_div = js_divergence(A, B, t=1.0, return_partial_entropies=False) >>> js_div # Should be 0 for identical graphs 0.0""" X = get_density_matrix(A, t) Y = get_density_matrix(B, t) mixed = np.trim_zeros(np.linalg.eigvalsh((X + Y) / 2)) raw1 = np.trim_zeros(np.linalg.eigvalsh(X)) raw2 = np.trim_zeros(np.linalg.eigvalsh(Y)) first = manual_entropy(mixed) ent1, ent2 = manual_entropy(raw1), manual_entropy(raw2) second = 0.5 * (ent1 + ent2) try: JSD = math.sqrt(first - second) except (ValueError, ArithmeticError): JSD = 0 # Handle negative values or other math errors if not return_partial_entropies: return JSD else: return manual_entropy(mixed), ent1, ent2, JSD