Source code for att.topology.spectral

"""Spectral distance matrices for topology-aware persistent homology.

Constructs kNN graph Laplacians and derives effective-resistance distance
matrices that respect intrinsic geometry better than Euclidean distances
in high-dimensional spaces (Direction 3).
"""

from __future__ import annotations

import numpy as np
from scipy.sparse import csr_matrix, diags
from scipy.sparse.linalg import eigsh
from sklearn.neighbors import NearestNeighbors


[docs] def knn_graph_laplacian( cloud: np.ndarray, k: int = 15, symmetrize: str = "or", ) -> csr_matrix: """Build the graph Laplacian from a kNN adjacency graph. Parameters ---------- cloud : (n, d) point cloud. k : int Number of nearest neighbours (excluding self). symmetrize : str "or" (union — default) or "and" (intersection) for making the directed kNN graph undirected. Returns ------- L : (n, n) sparse CSR Laplacian (L = D - W), symmetric positive semi-definite. """ n = cloud.shape[0] k_eff = min(k, n - 1) nn = NearestNeighbors(n_neighbors=k_eff + 1, algorithm="auto") nn.fit(cloud) dists, indices = nn.kneighbors(cloud) # Build sparse weight matrix (Gaussian kernel with adaptive bandwidth) rows, cols, vals = [], [], [] for i in range(n): for j_pos in range(1, k_eff + 1): # skip self at 0 j = indices[i, j_pos] d = dists[i, j_pos] # Heat kernel with local bandwidth = distance to k-th neighbour sigma_i = dists[i, k_eff] w = np.exp(-(d ** 2) / (sigma_i ** 2 + 1e-15)) rows.append(i) cols.append(j) vals.append(w) W = csr_matrix((vals, (rows, cols)), shape=(n, n)) # Symmetrize if symmetrize == "or": W = (W + W.T) / 2.0 else: W = W.minimum(W.T) # Laplacian: L = D - W deg = np.array(W.sum(axis=1)).ravel() D = diags(deg) L = D - W return L.tocsr()
[docs] def spectral_distance_matrix( cloud: np.ndarray, k: int = 15, n_eigenvectors: int | None = None, ) -> np.ndarray: """Compute effective-resistance distance matrix from kNN graph Laplacian. The effective resistance between nodes i and j is: R(i,j) = (e_i - e_j)^T L^+ (e_i - e_j) which can be computed efficiently via the spectral decomposition of L. Parameters ---------- cloud : (n, d) point cloud. k : int Number of nearest neighbours for the graph. n_eigenvectors : int or None Number of Laplacian eigenvectors to use (truncated approximation). None = use min(n-1, 100). Returns ------- D : (n, n) symmetric distance matrix with zero diagonal. """ n = cloud.shape[0] L = knn_graph_laplacian(cloud, k=k) if n_eigenvectors is None: n_eigenvectors = min(n - 1, 100) n_eigenvectors = min(n_eigenvectors, n - 1) # Compute smallest eigenvalues/vectors of L (skip the trivial zero eigenvalue) n_eig = min(n_eigenvectors + 1, n - 1) eigenvalues, eigenvectors = eigsh(L.astype(np.float64), k=n_eig, which="SM") # Skip the zero eigenvalue (connected component) # Eigenvalues near zero are the trivial ones tol = 1e-10 nonzero_mask = eigenvalues > tol eigenvalues = eigenvalues[nonzero_mask] eigenvectors = eigenvectors[:, nonzero_mask] if len(eigenvalues) == 0: # Degenerate case: all points equivalent return np.zeros((n, n)) # Scaled eigenvectors: phi_k / sqrt(lambda_k) scaled = eigenvectors / np.sqrt(eigenvalues)[np.newaxis, :] # Effective resistance: R(i,j) = ||scaled[i] - scaled[j]||^2 # Efficiently compute all pairwise squared distances sq_norms = np.sum(scaled ** 2, axis=1) D = sq_norms[:, np.newaxis] + sq_norms[np.newaxis, :] - 2.0 * (scaled @ scaled.T) # Clean up numerical noise D = np.maximum(D, 0.0) np.fill_diagonal(D, 0.0) # Symmetrize D = (D + D.T) / 2.0 # Return sqrt for use as a metric (effective resistance distance) return np.sqrt(D)