use crate::error::ConsciousnessError;
use crate::simd::build_mi_matrix;
use crate::traits::PhiEngine;
use crate::types::{ComputeBudget, PhiBound, TransitionMatrix};
pub fn spectral_bounds(tpm: &TransitionMatrix) -> Result<PhiBound, ConsciousnessError> {
let n = tpm.n;
if n < 2 {
return Err(crate::error::ValidationError::EmptySystem.into());
}
let mi_matrix = build_mi_matrix(tpm.as_slice(), n);
let mut laplacian = vec![0.0f64; n * n];
let mut max_degree = 0.0f64;
for i in 0..n {
let mut degree = 0.0;
for j in 0..n {
degree += mi_matrix[i * n + j];
}
max_degree = max_degree.max(degree);
laplacian[i * n + i] = degree;
for j in 0..n {
laplacian[i * n + j] -= mi_matrix[i * n + j];
}
}
let fiedler = estimate_fiedler(&laplacian, n, 200);
let lower = if max_degree > 1e-15 {
fiedler / (2.0 * max_degree)
} else {
0.0
};
let upper = (2.0 * fiedler).sqrt();
Ok(PhiBound {
lower: lower.max(0.0),
upper,
confidence: 1.0, samples: 0,
method: "spectral-cheeger".into(),
})
}
fn estimate_fiedler(laplacian: &[f64], n: usize, max_iter: usize) -> 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 = gershgorin_max(laplacian, n);
let mut w = vec![0.0f64; n];
let mut prev_rayleigh = f64::MAX;
for _ in 0..max_iter {
for i in 0..n {
let mut sum = mu * v[i];
for j in 0..n {
sum -= laplacian[i * n + j] * v[j];
}
w[i] = sum;
}
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;
}
v.copy_from_slice(&w);
let rq = rayleigh_quotient(laplacian, &v, n);
if (rq - prev_rayleigh).abs() < 1e-10 {
return rq.max(0.0);
}
prev_rayleigh = rq;
}
rayleigh_quotient(laplacian, &v, n).max(0.0)
}
pub fn hoeffding_bound(
phi_estimate: f64,
k: u64,
phi_max: f64,
delta: f64,
) -> PhiBound {
assert!(delta > 0.0 && delta < 1.0);
assert!(k > 0);
let range = phi_max.max(phi_estimate);
let epsilon = range * ((2.0f64 / delta).ln() / (2.0 * k as f64)).sqrt();
PhiBound {
lower: (phi_estimate - epsilon).max(0.0),
upper: phi_estimate + epsilon,
confidence: 1.0 - delta,
samples: k,
method: "hoeffding".into(),
}
}
pub fn empirical_bernstein_bound(
phi_estimates: &[f64],
delta: f64,
) -> PhiBound {
assert!(!phi_estimates.is_empty());
assert!(delta > 0.0 && delta < 1.0);
let k = phi_estimates.len() as f64;
let mean: f64 = phi_estimates.iter().sum::<f64>() / k;
let variance: f64 = phi_estimates.iter()
.map(|&x| (x - mean).powi(2))
.sum::<f64>() / (k - 1.0).max(1.0);
let max_val = phi_estimates.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let min_val = phi_estimates.iter().cloned().fold(f64::INFINITY, f64::min);
let phi_min = min_val;
let log_term = (3.0 / delta).ln();
let range = max_val - min_val;
let epsilon = (2.0 * variance * log_term / k).sqrt()
+ 3.0 * range * log_term / (3.0 * (k - 1.0).max(1.0));
PhiBound {
lower: (phi_min - epsilon).max(0.0),
upper: phi_min + epsilon,
confidence: 1.0 - delta,
samples: phi_estimates.len() as u64,
method: "empirical-bernstein".into(),
}
}
pub fn compute_phi_with_bounds<E: PhiEngine>(
engine: &E,
tpm: &TransitionMatrix,
state: Option<usize>,
budget: &ComputeBudget,
delta: f64,
) -> Result<(crate::types::PhiResult, PhiBound), ConsciousnessError> {
let result = engine.compute_phi(tpm, state, budget)?;
let bound = if result.convergence.len() > 1 {
empirical_bernstein_bound(&result.convergence, delta)
} else {
spectral_bounds(tpm).unwrap_or(PhiBound {
lower: 0.0,
upper: result.phi * 2.0,
confidence: 0.5,
samples: 1,
method: "fallback".into(),
})
};
Ok((result, bound))
}
fn rayleigh_quotient(laplacian: &[f64], v: &[f64], n: usize) -> f64 {
let mut vtlv = 0.0f64;
for i in 0..n {
let mut lv_i = 0.0;
for j in 0..n {
lv_i += laplacian[i * n + j] * v[j];
}
vtlv += v[i] * lv_i;
}
vtlv
}
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 gershgorin_max(matrix: &[f64], n: usize) -> f64 {
let mut max_sum = 0.0f64;
for i in 0..n {
let mut row_sum = 0.0;
for j in 0..n {
row_sum += matrix[i * n + j].abs();
}
max_sum = max_sum.max(row_sum);
}
max_sum
}
#[cfg(test)]
mod tests {
use super::*;
use crate::phi::SpectralPhiEngine;
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 spectral_bounds_valid_interval() {
let tpm = and_gate_tpm();
let bound = spectral_bounds(&tpm).unwrap();
assert!(bound.lower >= 0.0);
assert!(bound.upper >= bound.lower);
assert_eq!(bound.confidence, 1.0);
}
#[test]
fn hoeffding_bound_narrows_with_samples() {
let b1 = hoeffding_bound(0.5, 100, 1.0, 0.05);
let b2 = hoeffding_bound(0.5, 10000, 1.0, 0.05);
assert!(b2.upper - b2.lower < b1.upper - b1.lower,
"more samples should give tighter bound");
}
#[test]
fn empirical_bernstein_produces_interval() {
let samples = vec![0.3, 0.35, 0.32, 0.31, 0.33, 0.34, 0.30, 0.36];
let bound = empirical_bernstein_bound(&samples, 0.05);
assert!(bound.lower >= 0.0);
assert!(bound.upper > bound.lower);
assert!((bound.confidence - 0.95).abs() < 1e-10);
}
#[test]
fn compute_with_bounds_works() {
let tpm = and_gate_tpm();
let engine = SpectralPhiEngine::default();
let budget = ComputeBudget::fast();
let (result, bound) = compute_phi_with_bounds(&engine, &tpm, Some(0), &budget, 0.05).unwrap();
assert!(result.phi >= 0.0);
assert!(bound.lower >= 0.0);
}
}