use crate::ffi;
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub struct Sigma(f64);
impl Sigma {
pub fn new(v: f64) -> Self {
Sigma(v)
}
pub fn as_f64(&self) -> f64 {
self.0
}
}
impl From<f64> for Sigma {
fn from(v: f64) -> Self {
Sigma(v)
}
}
impl std::fmt::Display for Sigma {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "σ={:.6}", self.0)
}
}
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub struct SpectralDimension(f64);
impl SpectralDimension {
pub fn new(v: f64) -> Self {
SpectralDimension(v)
}
pub fn as_f64(&self) -> f64 {
self.0
}
}
impl std::fmt::Display for SpectralDimension {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "d_s={:.4}", self.0)
}
}
pub fn heat_return_probability(eigenvalues: &[f64], sigma: Sigma) -> f64 {
let n = eigenvalues.len();
if n == 0 {
return 0.0;
}
let s = sigma.as_f64();
let z: f64 = eigenvalues.iter().map(|&lambda| (-lambda * s).exp()).sum();
z / n as f64
}
pub fn spectral_dimension(eigenvalues: &[f64], sigma: Sigma) -> SpectralDimension {
if eigenvalues.is_empty() {
return SpectralDimension(0.0);
}
let s = sigma.as_f64();
let mut z = 0.0_f64;
let mut w = 0.0_f64;
for &lambda in eigenvalues {
let weight = (-lambda * s).exp();
z += weight;
w += lambda * weight;
}
if z == 0.0 {
return SpectralDimension(0.0);
}
SpectralDimension(2.0 * s * w / z)
}
pub fn laplacian_from_edges(n: usize, edges: &[(usize, usize)]) -> Vec<f64> {
let mut l = vec![0.0_f64; n * n];
for &(u, v) in edges {
l[u * n + v] -= 1.0;
l[v * n + u] -= 1.0;
l[u * n + u] += 1.0;
l[v * n + v] += 1.0;
}
l
}
pub fn graph_spectral_dimension(
n: usize,
edges: &[(usize, usize)],
sigma: Sigma,
) -> Result<SpectralDimension, i32> {
let lap = laplacian_from_edges(n, edges);
let (evals, _evecs) = ffi::eigensystem(n, &lap)?;
Ok(spectral_dimension(&evals, sigma))
}
#[cfg(test)]
mod tests {
use super::*;
fn torus_2d(l: usize) -> (usize, Vec<(usize, usize)>) {
let n = l * l;
let mut edges = Vec::new();
let idx = |i: usize, j: usize| -> usize { (i % l) * l + (j % l) };
for i in 0..l {
for j in 0..l {
let u = idx(i, j);
edges.push((u, idx(i + 1, j)));
edges.push((u, idx(i, j + 1)));
}
}
(n, edges)
}
fn ring(n: usize) -> (usize, Vec<(usize, usize)>) {
let edges: Vec<(usize, usize)> = (0..n).map(|i| (i, (i + 1) % n)).collect();
(n, edges)
}
fn torus_3d(l: usize) -> (usize, Vec<(usize, usize)>) {
let n = l * l * l;
let mut edges = Vec::new();
let idx =
|i: usize, j: usize, k: usize| -> usize { (i % l) * l * l + (j % l) * l + (k % l) };
for i in 0..l {
for j in 0..l {
for k in 0..l {
let u = idx(i, j, k);
edges.push((u, idx(i + 1, j, k)));
edges.push((u, idx(i, j + 1, k)));
edges.push((u, idx(i, j, k + 1)));
}
}
}
(n, edges)
}
fn log_grid(lo: f64, hi: f64, steps: usize) -> Vec<f64> {
let (a, b) = (lo.ln(), hi.ln());
(0..steps)
.map(|i| (a + (b - a) * i as f64 / (steps - 1) as f64).exp())
.collect()
}
#[test]
fn return_probability_at_zero_sigma_is_one() {
let evals = [0.0, 1.0, 2.5, 4.0];
let p = heat_return_probability(&evals, Sigma::new(0.0));
assert!((p - 1.0).abs() < 1e-12, "P(0)={p}");
}
#[test]
fn empty_spectrum_is_zero() {
assert_eq!(heat_return_probability(&[], Sigma::new(1.0)), 0.0);
assert_eq!(spectral_dimension(&[], Sigma::new(1.0)).as_f64(), 0.0);
}
#[test]
fn single_nonzero_mode_pure_power_law() {
let evals = [3.0];
let ds = spectral_dimension(&evals, Sigma::new(2.0)).as_f64();
assert!((ds - 12.0).abs() < 1e-12, "d_s={ds}, want 2·2·3=12");
}
#[test]
fn laplacian_is_symmetric_and_rows_sum_to_zero() {
let (n, edges) = (3, vec![(0_usize, 1_usize), (1, 2)]);
let l = laplacian_from_edges(n, &edges);
for i in 0..n {
let row_sum: f64 = (0..n).map(|j| l[i * n + j]).sum();
assert!(row_sum.abs() < 1e-12, "row {i} sums to {row_sum}");
for j in 0..n {
assert_eq!(l[i * n + j], l[j * n + i], "asymmetry at ({i},{j})");
}
}
assert_eq!(l[0], 1.0);
assert_eq!(l[1 * n + 1], 2.0);
assert_eq!(l[2 * n + 2], 1.0);
}
#[test]
fn torus_2d_recovers_dimension_two() {
let l = 24;
let (n, edges) = torus_2d(l);
let lap = laplacian_from_edges(n, &edges);
let (evals, _) = ffi::eigensystem(n, &lap).expect("dsyev converges");
let window = [2.0_f64, 4.0, 6.0, 8.0, 10.0, 12.0];
for &s in &window {
let ds = spectral_dimension(&evals, Sigma::new(s)).as_f64();
println!(" [2D l={l}] σ={s:>5.1} d_s={ds:.4}");
assert!(
(ds - 2.0).abs() < 0.2,
"σ={s}: d_s={ds} not within 0.2 of 2.0 on 2D torus",
);
}
}
#[test]
fn ring_recovers_dimension_one() {
let n = 400;
let (n, edges) = ring(n);
let lap = laplacian_from_edges(n, &edges);
let (evals, _) = ffi::eigensystem(n, &lap).expect("dsyev converges");
let window = [3.0_f64, 8.0, 15.0, 30.0, 60.0];
for &s in &window {
let ds = spectral_dimension(&evals, Sigma::new(s)).as_f64();
println!(" [ring n={n}] σ={s:>5.1} d_s={ds:.4}");
assert!(
(ds - 1.0).abs() < 0.15,
"σ={s}: d_s={ds} not within 0.15 of 1.0 on ring",
);
}
}
#[test]
fn torus_3d_recovers_dimension_three() {
let l = 14;
let (n, edges) = torus_3d(l);
let lap = laplacian_from_edges(n, &edges);
let (evals, _) = ffi::eigensystem(n, &lap).expect("dsyev converges");
let window = [3.0_f64, 3.5, 4.0, 4.5, 5.0, 6.0];
for &s in &window {
let ds = spectral_dimension(&evals, Sigma::new(s)).as_f64();
println!(" [3D l={l}] σ={s:>5.1} d_s={ds:.4}");
assert!(
(ds - 3.0).abs() < 0.3,
"σ={s}: d_s={ds} not within 0.3 of 3.0 on 3D torus",
);
}
}
#[test]
fn dimensional_flow_curve_on_3d_torus() {
let l = 14;
let (n, edges) = torus_3d(l);
let lap = laplacian_from_edges(n, &edges);
let (evals, _) = ffi::eigensystem(n, &lap).expect("dsyev converges");
let grid = log_grid(0.05, 500.0, 18);
println!("\n d_s(σ) flow — 3D torus, N={n}, degree 6");
println!(" {:>12} {:>10} {:>10}", "σ", "P(σ)", "d_s(σ)");
let mut peak = 0.0_f64;
for &s in &grid {
let p = heat_return_probability(&evals, Sigma::new(s));
let ds = spectral_dimension(&evals, Sigma::new(s)).as_f64();
peak = peak.max(ds);
println!(" {s:>12.4} {p:>10.6} {ds:>10.4}");
}
let ds_large = spectral_dimension(&evals, Sigma::new(500.0)).as_f64();
assert!(peak > 2.5, "flow never approached d≈3 (peak d_s={peak})");
assert!(
ds_large < 0.5,
"d_s did not collapse at large σ (d_s(500)={ds_large})",
);
}
}