use crate::flow::FlowNetwork;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SpectralAnalysis {
pub eigenvalues: Vec<f64>,
pub spectral_gap: f64,
pub algebraic_connectivity: f64,
pub mixing_time_bound: f64,
pub components: usize,
}
impl SpectralAnalysis {
pub fn from_network(network: &FlowNetwork) -> Self {
let n = network.modules.len();
if n == 0 {
return Self {
eigenvalues: vec![],
spectral_gap: 0.0,
algebraic_connectivity: 0.0,
mixing_time_bound: f64::INFINITY,
components: 0,
};
}
let laplacian = network.laplacian();
let eigenvalues = power_iteration_eigenvalues(&laplacian, n.min(20));
let components = eigenvalues.iter().filter(|&&e| e.abs() < 1e-8).count().max(1);
let spectral_gap = eigenvalues
.iter()
.filter(|&&e| e > 1e-8)
.cloned()
.next()
.unwrap_or(0.0);
let mixing_time_bound = if spectral_gap > 1e-12 {
(1.0 / spectral_gap) * (100.0_f64).ln()
} else {
f64::INFINITY
};
Self {
eigenvalues,
spectral_gap,
algebraic_connectivity: spectral_gap,
mixing_time_bound,
components,
}
}
pub fn from_adjacency(adj: &ndarray::Array2<f64>) -> Self {
let n = adj.nrows();
if n == 0 {
return Self {
eigenvalues: vec![],
spectral_gap: 0.0,
algebraic_connectivity: 0.0,
mixing_time_bound: f64::INFINITY,
components: 0,
};
}
let mut laplacian = ndarray::Array2::<f64>::zeros((n, n));
for i in 0..n {
let mut row_sum = 0.0;
for j in 0..n {
row_sum += adj[[i, j]];
}
laplacian[[i, i]] = row_sum;
for j in 0..n {
laplacian[[i, j]] -= adj[[i, j]];
}
}
let eigenvalues = power_iteration_eigenvalues(&laplacian, n.min(20));
let components = eigenvalues.iter().filter(|&&e| e.abs() < 1e-8).count().max(1);
let spectral_gap = eigenvalues
.iter()
.filter(|&&e| e > 1e-8)
.cloned()
.next()
.unwrap_or(0.0);
let mixing_time_bound = if spectral_gap > 1e-12 {
(1.0 / spectral_gap) * (100.0_f64).ln()
} else {
f64::INFINITY
};
Self {
eigenvalues,
spectral_gap,
algebraic_connectivity: spectral_gap,
mixing_time_bound,
components,
}
}
pub fn heat_kernel_trace(&self, t: f64) -> f64 {
self.eigenvalues.iter().map(|&lambda| (-t * lambda).exp()).sum()
}
}
fn power_iteration_eigenvalues(mat: &ndarray::Array2<f64>, max_eigs: usize) -> Vec<f64> {
let n = mat.nrows();
if n == 0 {
return vec![];
}
let mut a = mat.clone();
let iterations = 50;
for _ in 0..iterations {
let (q, r) = qr_decompose(&a);
a = r.dot(&q);
}
let mut eigs: Vec<f64> = (0..n).map(|i| a[[i, i]]).collect();
eigs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
eigs.truncate(max_eigs);
eigs
}
fn qr_decompose(a: &ndarray::Array2<f64>) -> (ndarray::Array2<f64>, ndarray::Array2<f64>) {
let n = a.nrows();
let m = a.ncols();
let mut q = ndarray::Array2::<f64>::zeros((n, m));
let mut r = ndarray::Array2::<f64>::zeros((m, m));
for j in 0..m {
let mut v: Vec<f64> = (0..n).map(|i| a[[i, j]]).collect();
for i in 0..j {
let dot: f64 = (0..n).map(|k| q[[k, i]] * v[k]).sum();
r[[i, j]] = dot;
for k in 0..n {
v[k] -= dot * q[[k, i]];
}
}
let norm: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
r[[j, j]] = norm;
if norm > 1e-15 {
for k in 0..n {
q[[k, j]] = v[k] / norm;
}
}
}
(q, r)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::flow::EntropyFlow;
fn cycle_network() -> FlowNetwork {
FlowNetwork::new(vec![
EntropyFlow { source: "A".into(), sink: "B".into(), rate: 1.0 },
EntropyFlow { source: "B".into(), sink: "C".into(), rate: 1.0 },
EntropyFlow { source: "C".into(), sink: "D".into(), rate: 1.0 },
EntropyFlow { source: "D".into(), sink: "A".into(), rate: 1.0 },
])
}
#[test]
fn spectral_gap_positive_for_connected() {
let net = cycle_network();
let analysis = SpectralAnalysis::from_network(&net);
assert!(
analysis.spectral_gap > 0.01,
"connected graph should have positive spectral gap: {}",
analysis.spectral_gap
);
}
#[test]
fn one_component_for_connected() {
let net = cycle_network();
let analysis = SpectralAnalysis::from_network(&net);
assert_eq!(analysis.components, 1);
}
#[test]
fn mixing_time_finite_for_connected() {
let net = cycle_network();
let analysis = SpectralAnalysis::from_network(&net);
assert!(analysis.mixing_time_bound.is_finite());
}
#[test]
fn eigenvalues_non_negative() {
let net = cycle_network();
let analysis = SpectralAnalysis::from_network(&net);
for &e in &analysis.eigenvalues {
assert!(e >= -1e-6, "Laplacian eigenvalues must be ≥ 0, got {e}");
}
}
#[test]
fn smallest_eigenvalue_is_zero() {
let net = cycle_network();
let analysis = SpectralAnalysis::from_network(&net);
assert!(
analysis.eigenvalues[0].abs() < 0.1,
"smallest Laplacian eigenvalue should be ≈ 0"
);
}
#[test]
fn heat_kernel_trace_decreases() {
let net = cycle_network();
let analysis = SpectralAnalysis::from_network(&net);
let trace_t1 = analysis.heat_kernel_trace(0.1);
let trace_t10 = analysis.heat_kernel_trace(1.0);
assert!(
trace_t10 <= trace_t1 + 1e-10,
"heat kernel trace should decrease with time"
);
}
#[test]
fn spectral_analysis_from_adjacency() {
let mut adj = ndarray::Array2::<f64>::zeros((3, 3));
adj[[0, 1]] = 1.0;
adj[[1, 0]] = 1.0;
adj[[1, 2]] = 1.0;
adj[[2, 1]] = 1.0;
let analysis = SpectralAnalysis::from_adjacency(&adj);
assert_eq!(analysis.components, 1);
assert!(analysis.eigenvalues[0].abs() < 0.5);
}
}