use gam_linalg::faer_ndarray::FaerEigh;
use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
pub(super) fn profiled_direction(
x: ArrayView2<'_, f32>,
rows: &[usize],
previous: ArrayView1<'_, f32>,
) -> Result<Array1<f32>, String> {
let m = rows.len();
let p = x.ncols();
let q = m.min(p);
if q == 0 {
return Err("single-atom profiling requires a nonempty cluster and feature axis".into());
}
let primal = p <= m;
let long_axis = m.max(p);
let mut gram = Array2::<f64>::zeros((q, q));
for start in (0..long_axis).step_by(q) {
let end = (start + q).min(long_axis);
let tile = if primal {
Array2::from_shape_fn((end - start, p), |(i, j)| x[[rows[start + i], j]] as f64)
} else {
Array2::from_shape_fn((m, end - start), |(i, j)| x[[rows[i], start + j]] as f64)
};
if primal {
gram += &tile.t().dot(&tile);
} else {
gram += &tile.dot(&tile.t());
}
}
let (values, vectors) = gram
.eigh(faer::Side::Lower)
.map_err(|error| format!("single-atom scatter eigensolve failed: {error}"))?;
let leading = (0..q)
.max_by(|&a, &b| values[a].total_cmp(&values[b]))
.expect("nonempty scatter spectrum");
let largest = values[leading];
if !(largest.is_finite() && largest > 0.0) {
return Err(format!(
"single-atom scatter has no positive finite energy: {largest}"
));
}
let work = (long_axis as f64 + q as f64) * f64::EPSILON;
let trace = gram.diag().sum();
let resolution = work / (1.0 - work) * trace;
if !(resolution.is_finite() && resolution >= 0.0 && resolution < largest) {
return Err(
"single-atom scatter spectrum is unresolved at its arithmetic precision".into(),
);
}
if values
.iter()
.any(|&value| !value.is_finite() || value < -resolution)
{
return Err("single-atom scatter spectrum is not positive semidefinite".into());
}
let previous_coordinates = if primal {
previous.mapv(f64::from)
} else {
Array1::from_shape_fn(m, |i| {
(0..p)
.map(|j| x[[rows[i], j]] as f64 * previous[j] as f64)
.sum()
})
};
let mut coordinates = Array1::<f64>::zeros(q);
for index in 0..q {
if largest - values[index] <= resolution {
let eigenvector = vectors.column(index);
let mut weight = eigenvector.dot(&previous_coordinates);
if !primal {
weight /= values[index];
}
coordinates.scaled_add(weight, &eigenvector);
}
}
if coordinates.iter().all(|&value| value == 0.0) {
coordinates.assign(&vectors.column(leading));
}
let direction = if primal {
coordinates
} else {
Array1::from_shape_fn(p, |j| {
rows.iter()
.zip(coordinates.iter())
.map(|(&row, &weight)| x[[row, j]] as f64 * weight)
.sum()
})
};
let norm = direction.dot(&direction).sqrt();
if !(norm.is_finite() && norm > 0.0) {
return Err("single-atom maximizing direction is not finite and nonzero".into());
}
Ok(direction.mapv(|value| (value / norm) as f32))
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
#[test]
fn profiling_solves_the_direction_instead_of_taking_one_power_step() {
let x = array![[2.0_f32, 0.0], [0.0, 1.0], [-2.0, 0.0], [0.0, -1.0]];
let previous = array![0.5_f32.sqrt(), 0.5_f32.sqrt()];
let direction = profiled_direction(x.view(), &[0, 1, 2, 3], previous.view()).unwrap();
assert!((direction[0].abs() - 1.0).abs() < f32::EPSILON);
assert!(direction[1].abs() < f32::EPSILON);
let power_energy = (8.0 * 16.0 + 2.0) / 17.0;
let captured: f64 = x
.outer_iter()
.map(|row| row.dot(&direction).powi(2) as f64)
.sum();
assert!((captured - 8.0).abs() < f64::EPSILON);
assert!(captured > power_energy);
}
#[test]
fn dual_gram_recovers_the_same_direction_after_zero_feature_padding() {
let x = array![[2.0_f32, 1.0], [-1.0, 1.0], [1.0, 0.0]];
let previous = array![0.6_f32, 0.8];
let primal = profiled_direction(x.view(), &[0, 1, 2], previous.view()).unwrap();
let wide = Array2::from_shape_fn((3, 19), |(i, j)| if j < 2 { x[[i, j]] } else { 0.0 });
let wide_previous = Array1::from_shape_fn(19, |j| if j < 2 { previous[j] } else { 0.0 });
let dual = profiled_direction(wide.view(), &[0, 1, 2], wide_previous.view()).unwrap();
for i in 0..2 {
for j in 0..2 {
assert!((primal[i] * primal[j] - dual[i] * dual[j]).abs() <= 4.0 * f32::EPSILON);
}
}
assert!(dual.iter().skip(2).all(|&value| value == 0.0));
}
#[test]
fn isotropic_cluster_preserves_the_previous_maximizer() {
let x = array![[1.0_f32, 0.0], [0.0, 1.0], [-1.0, 0.0], [0.0, -1.0]];
let previous = array![0.6_f32, 0.8];
let direction = profiled_direction(x.view(), &[0, 1, 2, 3], previous.view()).unwrap();
for (&actual, &expected) in direction.iter().zip(previous.iter()) {
assert!((actual - expected).abs() < f32::EPSILON);
}
}
#[test]
fn dense_wide_and_tall_clusters_recover_the_known_leading_projector() {
for (n, p) in [(32usize, 2048usize), (512, 128)] {
let scale = (p as f64).sqrt();
let x = Array2::from_shape_fn((n, p), |(i, j)| {
let a = if i % 2 == 0 { 2.0 } else { -2.0 };
let b = if i % 4 < 2 { 1.0 } else { -1.0 };
((a + if j % 2 == 0 { b } else { -b }) / scale) as f32
});
let previous = Array1::from_elem(p, (1.0 / scale) as f32);
let rows: Vec<usize> = (0..n).collect();
let start = std::time::Instant::now();
let direction = profiled_direction(x.view(), &rows, previous.view()).unwrap();
let alignment = direction
.iter()
.map(|&v| v as f64 / scale)
.sum::<f64>()
.abs();
assert!(
(alignment - 1.0).abs() < 8.0 * f32::EPSILON as f64,
"N={n} P={p}: projector alignment {alignment}"
);
eprintln!("single-atom profile N={n} P={p}: {:?}", start.elapsed());
}
}
}