use crate::arena::PhiArena;
use crate::error::ConsciousnessError;
use crate::phi::partition_information_loss_pub;
use crate::simd::build_mi_edges;
use crate::traits::PhiEngine;
use crate::types::{Bipartition, ComputeBudget, PhiAlgorithm, PhiResult, TransitionMatrix};
use ruvector_solver::types::CsrMatrix;
use std::time::Instant;
pub fn build_sparse_mi_graph(tpm: &TransitionMatrix, threshold: f64) -> (CsrMatrix<f64>, usize) {
let n = tpm.n;
let (edges, _marginal) = build_mi_edges(tpm.as_slice(), n, threshold);
let mut entries: Vec<(usize, usize, f64)> = Vec::with_capacity(edges.len() * 2);
for (i, j, mi) in edges {
entries.push((i, j, mi));
entries.push((j, i, mi));
}
let csr = CsrMatrix::<f64>::from_coo(n, n, entries);
let nnz = csr.nnz();
(csr, nnz)
}
pub fn build_sparse_laplacian(mi_csr: &CsrMatrix<f64>, n: usize) -> CsrMatrix<f64> {
let mut entries: Vec<(usize, usize, f64)> = Vec::new();
for i in 0..n {
let mut degree = 0.0;
for (j, &w) in mi_csr.row_entries(i) {
degree += w;
entries.push((i, j, -w));
}
entries.push((i, i, degree));
}
CsrMatrix::<f64>::from_coo(n, n, entries)
}
pub fn sparse_fiedler_vector(
laplacian: &CsrMatrix<f64>,
n: usize,
max_iter: usize,
) -> Vec<f64> {
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
let mut rng = StdRng::seed_from_u64(42);
let mut v: Vec<f64> = (0..n).map(|_| rng.gen::<f64>() - 0.5).collect();
let inv_n = 1.0 / n as f64;
let mean: f64 = v.iter().sum::<f64>() * inv_n;
for vi in &mut v {
*vi -= mean;
}
normalize(&mut v);
let mu = estimate_largest_eigenvalue_sparse(laplacian, n);
let mut w = vec![0.0f64; n];
let mut lv = vec![0.0f64; n];
let mut prev_eigenvalue = 0.0f64;
for _ in 0..max_iter {
laplacian.spmv(&v, &mut lv);
for i in 0..n {
w[i] = mu * v[i] - lv[i];
}
let mean: f64 = w.iter().sum::<f64>() * inv_n;
for wi in &mut w {
*wi -= mean;
}
let norm = normalize(&mut w);
if norm < 1e-15 {
break;
}
let eigenvalue = {
laplacian.spmv(&w, &mut lv);
let mut numer = 0.0f64;
let mut denom = 0.0f64;
for i in 0..n {
numer += w[i] * lv[i];
denom += w[i] * w[i];
}
if denom > 1e-30 { numer / denom } else { 0.0 }
};
v.copy_from_slice(&w);
if (eigenvalue - prev_eigenvalue).abs() < 1e-10 {
break; }
prev_eigenvalue = eigenvalue;
}
v
}
fn normalize(v: &mut [f64]) -> f64 {
let norm: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
if norm > 1e-15 {
let inv = 1.0 / norm;
for vi in v.iter_mut() {
*vi *= inv;
}
}
norm
}
fn estimate_largest_eigenvalue_sparse(laplacian: &CsrMatrix<f64>, n: usize) -> f64 {
let mut max_deg = 0.0f64;
for i in 0..n {
for (j, &val) in laplacian.row_entries(i) {
if j == i {
max_deg = max_deg.max(val);
}
}
}
max_deg
}
pub struct SparseSpectralPhiEngine {
pub mi_threshold: f64,
pub max_iterations: usize,
}
impl SparseSpectralPhiEngine {
pub fn new(mi_threshold: f64, max_iterations: usize) -> Self {
Self { mi_threshold, max_iterations }
}
}
impl Default for SparseSpectralPhiEngine {
fn default() -> Self {
Self {
mi_threshold: 1e-6,
max_iterations: 100,
}
}
}
impl PhiEngine for SparseSpectralPhiEngine {
fn compute_phi(
&self,
tpm: &TransitionMatrix,
state: Option<usize>,
_budget: &ComputeBudget,
) -> Result<PhiResult, ConsciousnessError> {
crate::phi::validate_tpm(tpm)?;
let n = tpm.n;
let state_idx = state.unwrap_or(0);
let start = Instant::now();
let (mi_csr, nnz) = build_sparse_mi_graph(tpm, self.mi_threshold);
tracing::debug!(n, nnz, density = nnz as f64 / (n * n) as f64, "sparse MI graph built");
let laplacian = build_sparse_laplacian(&mi_csr, n);
let fiedler = sparse_fiedler_vector(&laplacian, n, self.max_iterations);
let mut mask = 0u64;
for i in 0..n {
if fiedler[i] >= 0.0 {
mask |= 1 << i;
}
}
let full = (1u64 << n) - 1;
if mask == 0 { mask = 1; }
if mask == full { mask = full - 1; }
let partition = Bipartition { mask, n };
let arena = PhiArena::with_capacity(n * 16);
let phi = partition_information_loss_pub(tpm, state_idx, &partition, &arena);
Ok(PhiResult {
phi,
mip: partition,
partitions_evaluated: 1,
total_partitions: (1u64 << n) - 2,
algorithm: PhiAlgorithm::Spectral,
elapsed: start.elapsed(),
convergence: vec![phi],
})
}
fn algorithm(&self) -> PhiAlgorithm {
PhiAlgorithm::Spectral
}
fn estimate_cost(&self, n: usize) -> u64 {
(n as u64) * (n as f64).log2() as u64 * self.max_iterations as u64
}
}
#[cfg(test)]
mod tests {
use super::*;
fn disconnected_tpm() -> TransitionMatrix {
#[rustfmt::skip]
let data = vec![
0.5, 0.5, 0.0, 0.0,
0.5, 0.5, 0.0, 0.0,
0.0, 0.0, 0.5, 0.5,
0.0, 0.0, 0.5, 0.5,
];
TransitionMatrix::new(4, data)
}
fn and_gate_tpm() -> TransitionMatrix {
#[rustfmt::skip]
let data = vec![
0.5, 0.25, 0.25, 0.0,
0.5, 0.25, 0.25, 0.0,
0.5, 0.25, 0.25, 0.0,
0.0, 0.0, 0.0, 1.0,
];
TransitionMatrix::new(4, data)
}
#[test]
fn sparse_mi_graph_construction() {
let tpm = and_gate_tpm();
let (csr, nnz) = build_sparse_mi_graph(&tpm, 1e-10);
assert!(nnz > 0, "should have non-zero MI edges");
assert!(nnz <= 4 * 4, "should not exceed dense");
}
#[test]
fn sparse_spectral_disconnected_near_zero() {
let tpm = disconnected_tpm();
let budget = ComputeBudget::exact();
let result = SparseSpectralPhiEngine::default()
.compute_phi(&tpm, Some(0), &budget)
.unwrap();
assert!(result.phi < 1e-4, "sparse spectral disconnected should be ~0, got {}", result.phi);
}
#[test]
fn sparse_spectral_and_gate() {
let tpm = and_gate_tpm();
let budget = ComputeBudget::fast();
let result = SparseSpectralPhiEngine::default()
.compute_phi(&tpm, Some(0), &budget)
.unwrap();
assert!(result.phi >= 0.0);
}
}