#![cfg(test)]
use ndarray::{Array2, s};
use std::sync::Arc;
use crate::manifold::{
AssignmentMode, CrosscoderLayer, CrosscoderLayout, LatentManifold, PeriodicHarmonicEvaluator,
SaeAssignment, SaeAtomBasisKind, SaeBasisEvaluator, SaeManifoldAtom, SaeManifoldTerm,
measure_crosscoder_drift,
};
const P: usize = 4; const H: usize = 2;
fn layer_block(m: usize, alpha: f64) -> Array2<f64> {
let mut b = Array2::<f64>::zeros((m, P));
b[[0, 0]] = 1.0;
b[[1, 1]] = alpha.cos();
b[[1, 2]] = alpha.sin();
b
}
fn augmented_decoder(m: usize, angles: &[f64], block_log_lambda: &[f64]) -> Array2<f64> {
let l = angles.len();
let mut d = Array2::<f64>::zeros((m, P * l));
for (layer, &alpha) in angles.iter().enumerate() {
let mut block = layer_block(m, alpha);
if layer >= 1 {
let sqrt_lambda = (0.5 * block_log_lambda[layer - 1]).exp();
block.mapv_inplace(|v| v * sqrt_lambda);
}
d.slice_mut(s![.., layer * P..(layer + 1) * P])
.assign(&block);
}
d
}
fn build_term(
angles_per_atom: &[Vec<f64>],
block_dims: Vec<usize>,
block_log_lambda: Vec<f64>,
) -> (SaeManifoldTerm, CrosscoderLayout) {
let n = 8usize;
let evaluator = Arc::new(
PeriodicHarmonicEvaluator::new(2 * H + 1)
.expect("2H+1 is odd and positive, the basis width this evaluator requires"),
);
let coords = Array2::<f64>::from_shape_fn((n, 1), |(i, _)| i as f64 / n as f64);
let (phi, jet) = evaluator
.evaluate(coords.view())
.expect("fixture coords are finite and inside the unit-period circle chart");
let m = phi.ncols();
let p_tot = P * angles_per_atom[0].len();
let atoms: Vec<SaeManifoldAtom> = angles_per_atom
.iter()
.map(|angles| {
let decoder = augmented_decoder(m, angles, &block_log_lambda);
assert_eq!(decoder.ncols(), p_tot);
SaeManifoldAtom::new_with_provided_function_gram(
"cc",
SaeAtomBasisKind::Periodic,
1,
phi.clone(),
jet.clone(),
decoder,
Array2::<f64>::eye(m),
)
.expect("decoder, phi and jet widths agree by construction above")
})
.collect();
let k = atoms.len();
let logits = Array2::<f64>::from_elem((n, k), 6.0);
let assignment = SaeAssignment::from_blocks_with_mode_and_manifolds(
logits,
vec![coords.clone(); k],
vec![LatentManifold::Circle { period: 1.0 }; k],
AssignmentMode::softmax(1.0),
)
.expect("one coord block and one manifold per atom, matching the logits' k columns");
let term = SaeManifoldTerm::new(atoms, assignment)
.expect("the assignment was built with exactly one block per atom");
let labels: Vec<String> = (0..block_dims.len())
.map(|l| format!("layer{}", l + 1))
.collect();
let layout = CrosscoderLayout::new(P, block_dims, labels, block_log_lambda)
.expect("block dims, labels and per-block log-lambdas are all the same length");
(term, layout)
}
fn assert_close(got: f64, want: f64, tol: f64, what: &str) {
assert!(
(got - want).abs() < tol,
"{what}: got {got}, want {want} (|Δ| = {})",
(got - want).abs()
);
}
#[test]
fn drift_recovers_planted_layer_rotations() {
let angles = vec![0.0, 0.3, 0.7];
let (term, layout) = build_term(&[angles], vec![P, P], vec![0.0, 0.0]);
let report = measure_crosscoder_drift(&term, &layout).unwrap();
assert_eq!(report.num_atoms, 1);
assert_eq!(report.num_steps(), 2);
assert_eq!(report.layer_chain.len(), 3);
assert_eq!(report.layer_chain[0], CrosscoderLayer::Anchor);
assert_eq!(report.layer_chain[1], CrosscoderLayer::Block(0));
assert_eq!(report.layer_chain[2], CrosscoderLayer::Block(1));
let step0 = &report.steps[0];
assert_close(
step0.max_principal_angle(),
0.3,
1e-9,
"step0 max principal angle",
);
assert_close(
step0.drift,
(1.0 - 0.3_f64.cos()).sqrt(),
1e-9,
"step0 drift",
);
let step1 = &report.steps[1];
assert_close(
step1.max_principal_angle(),
0.4,
1e-9,
"step1 max principal angle",
);
assert_close(
step1.drift,
(1.0 - 0.4_f64.cos()).sqrt(),
1e-9,
"step1 drift",
);
let profile = report.atom_drift_profile(0);
assert_eq!(profile.len(), 2);
assert_close(profile[0], step0.drift, 1e-12, "profile[0] == step0.drift");
assert_close(profile[1], step1.drift, 1e-12, "profile[1] == step1.drift");
assert_close(
report.atom_total_drift(0),
step0.drift + step1.drift,
1e-12,
"atom total drift == sum of steps",
);
assert_eq!(report.most_drifting_atom(), Some(0));
assert_eq!(report.most_stable_atom(), Some(0));
}
#[test]
fn honest_drift_is_invariant_to_block_lambda() {
let angles = vec![0.0, 0.3, 0.7];
let (term_unit, layout_unit) = build_term(&[angles.clone()], vec![P, P], vec![0.0, 0.0]);
let (term_scaled, layout_scaled) = build_term(&[angles], vec![P, P], vec![1.3, -0.8]);
let a = measure_crosscoder_drift(&term_unit, &layout_unit).unwrap();
let b = measure_crosscoder_drift(&term_scaled, &layout_scaled).unwrap();
for (sa, sb) in a.steps.iter().zip(b.steps.iter()) {
assert_close(
sb.drift,
sa.drift,
1e-12,
"λ-scaled drift matches unit-λ drift",
);
assert_close(
sb.max_principal_angle(),
sa.max_principal_angle(),
1e-12,
"λ-scaled principal angle matches unit-λ",
);
}
}
#[test]
fn ranking_picks_most_and_least_drifting_atoms() {
let moving = vec![0.0, 0.4, 0.9];
let stable = vec![0.2, 0.2, 0.2];
let (term, layout) = build_term(&[moving, stable], vec![P, P], vec![0.0, 0.0]);
let report = measure_crosscoder_drift(&term, &layout).unwrap();
assert_eq!(report.num_atoms, 2);
assert_close(
report.atom_total_drift(1),
0.0,
1e-12,
"stable atom total drift",
);
for s in report.steps.iter().filter(|s| s.atom == 1) {
assert_close(
s.max_principal_angle(),
0.0,
1e-9,
"stable atom principal angle",
);
}
assert!(report.atom_total_drift(0) > 0.0, "moving atom must drift");
assert_eq!(report.most_drifting_atom(), Some(0));
assert_eq!(report.most_stable_atom(), Some(1));
assert!(report.mean_drift().is_finite());
}
#[test]
fn rejects_layout_that_does_not_describe_the_term() {
let angles = vec![0.0, 0.3];
let (term, _good) = build_term(&[angles], vec![P], vec![0.0]);
let bad = CrosscoderLayout::new(P, vec![P + 1], vec!["x".into()], vec![0.0]).unwrap();
let err = measure_crosscoder_drift(&term, &bad).unwrap_err();
assert!(
err.contains("!= term output_dim"),
"unexpected error: {err}"
);
}
#[test]
fn rejects_layers_of_differing_ambient_width() {
let n = 8usize;
let evaluator = Arc::new(PeriodicHarmonicEvaluator::new(2 * H + 1).unwrap());
let coords = Array2::<f64>::from_shape_fn((n, 1), |(i, _)| i as f64 / n as f64);
let (phi, jet) = evaluator.evaluate(coords.view()).unwrap();
let m = phi.ncols();
let p_tot = P + P + (P - 1);
let decoder =
Array2::<f64>::from_shape_fn(
(m, p_tot),
|(r, c)| {
if r == 0 && c == 0 { 1.0 } else { 0.0 }
},
);
let atom = SaeManifoldAtom::new_with_provided_function_gram(
"cc",
SaeAtomBasisKind::Periodic,
1,
phi,
jet,
decoder,
Array2::<f64>::eye(m),
)
.unwrap();
let logits = Array2::<f64>::from_elem((n, 1), 6.0);
let assignment = SaeAssignment::from_blocks_with_mode_and_manifolds(
logits,
vec![coords.clone()],
vec![LatentManifold::Circle { period: 1.0 }],
AssignmentMode::softmax(1.0),
)
.unwrap();
let term = SaeManifoldTerm::new(vec![atom], assignment).unwrap();
let ragged = CrosscoderLayout::new(
P,
vec![P, P - 1],
vec!["layer1".into(), "layer2".into()],
vec![0.0, 0.0],
)
.unwrap();
let err = measure_crosscoder_drift(&term, &ragged).unwrap_err();
assert!(
err.contains("layer widths differ"),
"unexpected error: {err}"
);
}