use crate::arena::PhiArena;
use crate::error::ConsciousnessError;
use crate::phi::partition_information_loss_pub;
use crate::simd::build_mi_matrix;
use crate::traits::PhiEngine;
use crate::types::{Bipartition, ComputeBudget, PhiAlgorithm, PhiResult, TransitionMatrix};
use ruvector_math::spectral::{ChebyshevExpansion, ScaledLaplacian};
use std::time::Instant;
pub struct ChebyshevPhiEngine {
pub degree: usize,
pub cutoff: f64,
}
impl ChebyshevPhiEngine {
pub fn new(degree: usize, cutoff: f64) -> Self {
Self { degree, cutoff }
}
}
impl Default for ChebyshevPhiEngine {
fn default() -> Self {
Self {
degree: 30,
cutoff: 0.1,
}
}
}
impl PhiEngine for ChebyshevPhiEngine {
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 adj = build_mi_matrix(tpm.as_slice(), n);
let scaled_lap = ScaledLaplacian::from_adjacency(&adj, n);
let filter = ChebyshevExpansion::low_pass(self.cutoff, self.degree);
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
let mut rng = StdRng::seed_from_u64(42);
let mut signal: Vec<f64> = (0..n).map(|_| rng.gen::<f64>() - 0.5).collect();
let inv_n = 1.0 / n as f64;
let mean: f64 = signal.iter().sum::<f64>() * inv_n;
for s in &mut signal {
*s -= mean;
}
let filtered = apply_chebyshev_filter(&scaled_lap, &signal, &filter, n);
let mut mask = 0u64;
for i in 0..n {
if filtered[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 {
self.degree as u64 * (n * n) as u64
}
}
fn apply_chebyshev_filter(
lap: &ScaledLaplacian,
signal: &[f64],
filter: &ChebyshevExpansion,
n: usize,
) -> Vec<f64> {
let coeffs = &filter.coefficients;
let k_max = coeffs.len();
if k_max == 0 {
return vec![0.0; n];
}
let mut result = vec![0.0f64; n];
let c0 = coeffs[0];
for i in 0..n {
result[i] = c0 * signal[i];
}
if k_max == 1 {
return result;
}
let t1 = lap.apply(signal);
let c1 = coeffs[1];
for i in 0..n {
result[i] += c1 * t1[i];
}
if k_max == 2 {
return result;
}
let mut prev = signal.to_vec();
let mut curr = t1;
let mut next_buf = vec![0.0f64; n];
for k in 2..k_max {
let next_lap = lap.apply(&curr);
let ck = coeffs[k];
for i in 0..n {
let next_i = 2.0 * next_lap[i] - prev[i];
result[i] += ck * next_i;
next_buf[i] = next_i;
}
std::mem::swap(&mut prev, &mut curr);
std::mem::swap(&mut curr, &mut next_buf);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
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)
}
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)
}
#[test]
fn chebyshev_phi_and_gate() {
let tpm = and_gate_tpm();
let budget = ComputeBudget::fast();
let result = ChebyshevPhiEngine::default()
.compute_phi(&tpm, Some(0), &budget)
.unwrap();
assert!(result.phi >= 0.0);
}
#[test]
fn chebyshev_phi_disconnected() {
let tpm = disconnected_tpm();
let budget = ComputeBudget::exact();
let result = ChebyshevPhiEngine::default()
.compute_phi(&tpm, Some(0), &budget)
.unwrap();
assert!(result.phi < 1e-3, "chebyshev disconnected should be ~0, got {}", result.phi);
}
}