use log::{debug, info, trace};
use smartcore::linalg::basic::arrays::Array;
use smartcore::linalg::basic::matrix::DenseMatrix;
use crate::builder::ArrowSpaceBuilder;
use crate::core::{ArrowItem, ArrowSpace};
use crate::graph::{GraphFactory, GraphLaplacian};
use crate::search::taumode::TauMode;
pub trait EigenMaps {
fn eigenmaps(
&mut self,
builder: &ArrowSpaceBuilder,
centroids: &DenseMatrix<f64>,
n_items: usize,
) -> GraphLaplacian;
fn compute_taumode(&mut self, gl: &GraphLaplacian);
fn search(&self, item: &[f64], gl: &GraphLaplacian, k: usize, alpha: f64) -> Vec<(usize, f64)>;
}
impl EigenMaps for ArrowSpace {
fn eigenmaps(
&mut self,
builder: &ArrowSpaceBuilder,
centroids: &DenseMatrix<f64>,
n_items: usize,
) -> GraphLaplacian {
let (n_centroids, n_features) = centroids.shape();
info!(
"EigenMaps::eigenmaps: Building Laplacian from {} centroids × {} features",
n_centroids, n_features
);
debug!(
"λ-graph parameters: eps={}, k={}, topk={}, p={}, sigma={:?}, normalize={}",
builder.lambda_eps,
builder.lambda_k,
builder.lambda_topk,
builder.lambda_p,
builder.lambda_sigma,
builder.normalise
);
let gl = GraphFactory::build_laplacian_matrix_from_k_cluster(
¢roids,
builder.lambda_eps,
builder.lambda_k,
builder.lambda_topk,
builder.lambda_p,
builder.lambda_sigma,
builder.normalise,
builder.sparsity_check,
n_items,
);
if builder.prebuilt_spectral {
trace!("Building spectral Laplacian for ArrowSpace");
GraphFactory::build_spectral_laplacian(self, &gl);
debug!(
"Spectral Laplacian built with signals shape: {:?}",
self.signals.shape()
);
}
info!(
"Laplacian construction complete: {}×{} matrix, {} non-zeros, {:.2}% sparse",
gl.shape().0,
gl.shape().1,
gl.nnz(),
GraphLaplacian::sparsity(&gl.matrix) * 100.0
);
gl
}
fn compute_taumode(&mut self, gl: &GraphLaplacian) {
info!(
"EigenMaps::compute_taumode: Computing λ values for {} items using {:?}",
self.nitems, self.taumode
);
debug!(
"Laplacian: {} nodes, {} non-zeros",
gl.nnodes,
gl.matrix.nnz()
);
TauMode::compute_taumode_lambdas_parallel(self, gl, self.taumode);
let lambda_stats = {
let min = self.lambdas.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let max = self
.lambdas
.iter()
.fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let mean = self.lambdas.iter().sum::<f64>() / self.lambdas.len() as f64;
(min, max, mean)
};
info!(
"λ computation complete: min={:.6}, max={:.6}, mean={:.6}",
lambda_stats.0, lambda_stats.1, lambda_stats.2
);
}
fn search(&self, item: &[f64], gl: &GraphLaplacian, k: usize, alpha: f64) -> Vec<(usize, f64)> {
info!(
"EigenMaps::search: k={}, alpha={:.2}, query_dim={}",
k,
alpha,
item.len()
);
debug_assert!(
self.lambdas[0..self.nitems.min(4)]
.iter()
.any(|&v| v != 0.0)
|| self.nitems == 0,
"call compute_taumode(...) before search to populate lambdas"
);
trace!("Preparing query λ with projection and taumode policy");
let q_lambda = self.prepare_query_item(item, gl);
let q = ArrowItem::new(item, q_lambda);
let results = self.search_lambda_aware(&q, k, alpha);
info!(
"Search complete: {} results returned, top_score={:.6}",
results.len(),
results.first().map(|(_, s)| *s).unwrap_or(0.0)
);
results
}
}