//! PIST Spectral — Rust Port //! //! Mirrors `formal/SilverSight/PIST/Spectral.lean`. //! Minimal fixed-point spectral feature extraction: //! - isqrt (integer square root) //! - power iteration for dominant eigenvalue //! - SpectralProfile //! - Fiedler value via Laplacian use crate::q16::*; // ── Integer square root ──────────────────────────────────────────── /// Integer square root via Newton's method. Returns floor(√n). pub fn isqrt(n: i64) -> i64 { if n <= 0 { return 0; } let mut x = n / 2 + 1; for _ in 0..64 { let x_new = (x + n / x) / 2; if x_new >= x { return x; } x = x_new; } x } // ── Matrix helpers ───────────────────────────────────────────────── type IntMat = Vec>; fn get_entry(mat: &IntMat, i: usize, j: usize) -> i64 { mat.get(i).and_then(|row| row.get(j).copied()).unwrap_or(0) } fn row_sum(mat: &IntMat, i: usize, n: usize) -> i64 { (0..n).map(|j| get_entry(mat, i, j)).sum() } fn symmetrize(mat: &IntMat, n: usize) -> IntMat { (0..n).map(|i| { (0..n).map(|j| { (get_entry(mat, i, j) + get_entry(mat, j, i)) / 2 }).collect() }).collect() } fn build_laplacian(sym: &IntMat, n: usize) -> IntMat { (0..n).map(|i| { let deg = row_sum(sym, i, n); (0..n).map(|j| { if i == j { deg } else { -get_entry(sym, i, j) } }).collect() }).collect() } fn build_ata(mat: &IntMat, n: usize) -> IntMat { (0..n).map(|i| { (0..n).map(|j| { (0..n).map(|k| get_entry(mat, k, i) * get_entry(mat, k, j)).sum() }).collect() }).collect() } // ── SpectralProfile ──────────────────────────────────────────────── #[derive(Debug, Clone)] pub struct SpectralProfile { pub dominant_eigenvalue: f64, pub fiedler_value: f64, pub spectral_gap: f64, pub condition_number: f64, } // ── Power iteration (f64 arithmetic) ─────────────────────────────── /// Dominant eigenvalue of a square Int matrix via power iteration. pub fn power_iteration(mat: &IntMat, max_iter: usize) -> f64 { let n = mat.len(); if n == 0 { return 0.0; } let mut v: Vec = (0..n).map(|i| (i as f64 + 1.0)).collect(); for _ in 0..max_iter { // mat × v (as f64) let mv: Vec = (0..n).map(|i| { (0..n).map(|j| get_entry(mat, i, j) as f64 * v[j]).sum() }).collect(); // Rayleigh quotient let v_dot_mv: f64 = v.iter().zip(mv.iter()).map(|(vi, mvi)| vi * mvi).sum(); let v_dot_v: f64 = v.iter().map(|x| x * x).sum(); if v_dot_v < 1e-15 { break; } // Copy for next iteration, normalized let norm = (mv.iter().map(|x| x * x).sum::()).sqrt(); if norm < 1e-15 { break; } for (vi, mvi) in v.iter_mut().zip(mv.iter()) { *vi = mvi / norm; } // Check convergence let eig = v_dot_mv / v_dot_v; let mx = (0..n).map(|i| { (0..n).map(|j| get_entry(mat, i, j) as f64 * v[j]).sum::() }).collect::>(); let resid: f64 = mx.iter().zip(v.iter()).map(|(mxi, vi)| (mxi - eig * vi).abs()).sum::() / n as f64; if resid < 1e-8 { return eig; } } // Final Rayleigh quotient let mv: Vec = (0..n).map(|i| { (0..n).map(|j| get_entry(mat, i, j) as f64 * v[j]).sum() }).collect(); let num: f64 = v.iter().zip(mv.iter()).map(|(vi, mvi)| vi * mvi).sum(); let den: f64 = v.iter().map(|x| x * x).sum(); if den > 0.0 { num / den } else { 0.0 } } fn rayleigh_quotient(mat: &IntMat, v: &[f64]) -> f64 { let n = mat.len(); let mv: Vec = (0..n).map(|i| { (0..n).map(|j| get_entry(mat, i, j) as f64 * v[j]).sum() }).collect(); let num: f64 = v.iter().zip(mv.iter()).map(|(vi, mvi)| vi * mvi).sum(); let den: f64 = v.iter().map(|x| x * x).sum(); if den > 0.0 { num / den } else { 0.0 } } /// Fiedler value (smallest non-zero eigenvalue of Laplacian) via shifted inverse iteration. pub fn fiedler_value(mat: &IntMat) -> f64 { let n = mat.len(); if n < 2 { return 0.0; } let sym = symmetrize(mat, n); let lap = build_laplacian(&sym, n); // Power iteration for dominant eigenvalue let lambda_max = power_iteration(&lap, 100); // Shift-invert: solve (L - μI)⁻¹ where μ = lambda_max * 0.9 // to find the smallest eigenvalue near the upper end let mu = lambda_max * 0.9; let mut v: Vec = (0..n).map(|i| if i % 2 == 0 { 1.0 } else { -1.0 }).collect(); for _ in 0..50 { // (L - μI) × v let mv: Vec = (0..n).map(|i| { let row: f64 = (0..n).map(|j| { let l_ij = if i == j { row_sum(&lap, i, n) as f64 } else { -get_entry(&lap, i, j) as f64 }; l_ij * v[j] }).sum(); row - mu * v[i] }).collect(); // Normalize let norm = mv.iter().map(|x| x * x).sum::().sqrt(); if norm < 1e-10 { break; } for vi in v.iter_mut() { *vi /= norm; } } // Compute Rayleigh quotient with the converged vector rayleigh_quotient(&lap, &v.iter().copied().collect::>()) } /// Full spectral profile for an n×n Int matrix. pub fn compute_profile(mat: &IntMat) -> SpectralProfile { let n = mat.len(); if n == 0 { return SpectralProfile { dominant_eigenvalue: 0.0, fiedler_value: 0.0, spectral_gap: 0.0, condition_number: 0.0, }; } let sym = symmetrize(mat, n); let lap = build_laplacian(&sym, n); let lambda_max = power_iteration(&lap, 100); let fv = fiedler_value(mat); SpectralProfile { dominant_eigenvalue: lambda_max, fiedler_value: fv, spectral_gap: lambda_max - fv, condition_number: if fv.abs() > 1e-10 { lambda_max / fv } else { f64::INFINITY }, } } #[cfg(test)] mod tests { use super::*; #[test] fn test_isqrt() { assert_eq!(isqrt(0), 0); assert_eq!(isqrt(1), 1); assert_eq!(isqrt(4), 2); assert_eq!(isqrt(9), 3); assert_eq!(isqrt(16), 4); assert_eq!(isqrt(2), 1); // floor(√2) assert_eq!(isqrt(10), 3); // floor(√10) } #[test] fn test_symmetrize() { let mat: IntMat = vec![ vec![1, 2], vec![3, 4], ]; let sym = symmetrize(&mat, 2); assert_eq!(sym[0][1], sym[1][0]); // symmetric assert_eq!(sym[0][1], (2 + 3) / 2); } #[test] fn test_power_iteration_small() { // 2×2 identity → eigenvalue should be 1 let mat: IntMat = vec![ vec![1, 0], vec![0, 1], ]; let eig = power_iteration(&mat, 100); assert!((eig - 1.0).abs() < 0.1); } #[test] fn test_spectral_profile() { // 3×3 matrix with known structure let mat: IntMat = vec![ vec![2, 1, 0], vec![1, 2, 1], vec![0, 1, 2], ]; let profile = compute_profile(&mat); assert!(profile.dominant_eigenvalue > 0.0); assert!(profile.spectral_gap >= 0.0); } }