use std::sync::Arc;
use std::time::Instant;
use ndarray::{Array1, Array2};
use gam_sae::basis::{PeriodicHarmonicEvaluator, SaeBasisEvaluator};
use gam_sae::candidate_index::{
IndexConfig, RandomProjectionFrameSketch, SaeCandidateIndex,
};
use gam_sae::encode::{AtlasConfig, EncodeAtlas};
use gam_sae::manifold::{SaeAtomBasisKind, SaeManifoldAtom};
fn atom_plane(k: usize, p: usize) -> (Array1<f64>, Array1<f64>) {
let kf = k as f64;
let mut u = Array1::from_shape_fn(p, |j| ((0.7 * kf + 1.3 * j as f64 + 0.1).sin()) + 0.15 * ((kf * 0.031 + j as f64 * 0.017).cos()));
let un = u.dot(&u).sqrt().max(1e-12);
u.mapv_inplace(|x| x / un);
let mut v = Array1::from_shape_fn(p, |j| ((0.29 * kf + 0.91 * j as f64 + 0.4).cos()) + 0.11 * ((kf * 0.047 + j as f64 * 0.023).sin()));
let proj = v.dot(&u);
v = &v - &(&u * proj);
let vn = v.dot(&v).sqrt().max(1e-12);
v.mapv_inplace(|x| x / vn);
(u, v)
}
fn build_dictionary(
k: usize,
p: usize,
) -> (Vec<SaeManifoldAtom>, EncodeAtlas, SaeCandidateIndex, RandomProjectionFrameSketch) {
let evaluator = Arc::new(PeriodicHarmonicEvaluator::new(3).unwrap());
let n_seed = 16usize;
let seed: Array2<f64> = Array2::from_shape_fn((n_seed, 1), |(i, _)| i as f64 / n_seed as f64);
let (seed_phi, seed_jet) = evaluator.evaluate(seed.view()).unwrap();
let m = seed_phi.ncols();
let mut atoms: Vec<SaeManifoldAtom> = Vec::with_capacity(k);
let mut blocks: Vec<Array2<f64>> = Vec::with_capacity(k);
for atom_k in 0..k {
let (u, v) = atom_plane(atom_k, p);
let mut decoder = Array2::<f64>::zeros((m, p));
for c in 0..p {
decoder[[2, c]] = u[c];
decoder[[1, c]] = v[c];
}
blocks.push(decoder.t().to_owned());
let atom = SaeManifoldAtom::new(
"circle",
SaeAtomBasisKind::Periodic,
1,
seed_phi.clone(),
seed_jet.clone(),
decoder,
Array2::<f64>::eye(m),
)
.unwrap()
.with_basis_evaluator(evaluator.clone());
atoms.push(atom);
}
let sketch_dim = 24usize.min(p);
let sketch =
RandomProjectionFrameSketch::from_decoder_blocks(&blocks, sketch_dim, 12345).unwrap();
let index = SaeCandidateIndex::build(&sketch, IndexConfig::auto(sketch_dim, k, 12345)).unwrap();
let atlas = EncodeAtlas::build(
&atoms,
&vec![1.2_f64; k],
3.0,
AtlasConfig {
grid_resolution: 4,
ridge: 1e-10,
newton_steps: 4,
},
)
.expect("atlas builds over the K-atom dictionary");
(atoms, atlas, index, sketch)
}
fn planted_targets(atoms: &[SaeManifoldAtom], n: usize, p: usize) -> (Array2<f64>, Array1<f64>) {
let k = atoms.len();
let mut targets = Array2::<f64>::zeros((n, p));
let mut amps = Array1::<f64>::ones(n);
let evaluator = atoms[0].basis_evaluator.as_ref().unwrap().clone();
for row in 0..n {
let atom_k = (row * 2654435761) % k; let t = ((row as f64 * 0.6180339887) % 1.0).abs();
let z = 0.8 + 0.4 * ((row as f64 * 0.123).sin() * 0.5 + 0.5);
amps[row] = z;
let coord = Array2::from_shape_fn((1, 1), |_| t);
let (phi, _) = evaluator.evaluate(coord.view()).unwrap();
let decoded = phi.dot(&atoms[atom_k].decoder_coefficients); for c in 0..p {
targets[[row, c]] = z * decoded[[0, c]];
}
}
(targets, amps)
}
#[test]
fn massive_k_encode_is_sublinear_in_k() {
let p = 96usize;
let n = 4_096usize;
let ks = [1_024usize, 8_192usize, 32_000usize];
let mut rates: Vec<f64> = Vec::new();
for &k in &ks {
let (atoms, atlas, index, sketch) = build_dictionary(k, p);
let (targets, amps) = planted_targets(&atoms, n, p);
let (warm_coords, _) = atlas
.amortized_encode_with_index_fast(&atoms, &index, &sketch, targets.view(), amps.view(), 1)
.expect("warm fast index-routed encode");
assert_eq!(
warm_coords.nrows(),
n,
"warm encode must return one coordinate row per target (K={k})"
);
let start = Instant::now();
let (coords, valid) = atlas
.amortized_encode_with_index_fast(&atoms, &index, &sketch, targets.view(), amps.view(), 1)
.expect("timed fast index-routed encode");
let elapsed = start.elapsed();
let rows_per_sec = n as f64 / elapsed.as_secs_f64();
rates.push(rows_per_sec);
let n_valid = valid.iter().filter(|&&v| v).count();
assert_eq!(coords.nrows(), n);
eprintln!(
"[k-scaling] K={k:>6} p={p} N={n} rows/sec={rows_per_sec:>12.1} \
routed={n_valid}/{n} ({:.1}%)",
100.0 * n_valid as f64 / n as f64
);
}
let (r_small, r_big) = (rates[0], rates[ks.len() - 1]);
let k_ratio = ks[ks.len() - 1] as f64 / ks[0] as f64; let slowdown = r_small / r_big;
let alpha = slowdown.max(1.0).ln() / k_ratio.ln();
eprintln!(
"[k-scaling] K {}→{} : throughput slowdown {:.2}× ⇒ scaling exponent α≈{:.3} \
(O(N·K) router is α=1.0)",
ks[0], ks[ks.len() - 1], slowdown, alpha
);
assert!(
alpha < 0.97,
"massive-K encode must scale SUBLINEARLY in K: measured cost ~ K^{alpha:.3} \
(throughput fell {slowdown:.2}× over a {k_ratio:.0}× K increase); an O(N·K) router is K^1.0."
);
}