use ndarray::{Array1, Array2, ArrayView2};
use gam_linalg::faer_ndarray::{FaerEigh, FaerSvd};
use crate::description_length::{selection_bits, weighted_reverse_water_filling};
pub const DEFAULT_EQ4_R2_TARGETS: &[f64] = &[0.99, 0.95, 0.90, 0.80];
const GATE_ACTIVE_THRESHOLD: f64 = 1e-10;
const SPECTRUM_ROW_CAP: usize = 4096;
#[derive(Clone, Copy, Debug)]
pub struct Eq4TargetBits {
pub target: f64,
pub bits: f64,
pub code_bits: f64,
pub resid_bits: f64,
}
#[derive(Clone, Debug)]
pub struct Eq4DescriptionLength {
pub support_bits: f64,
pub independent_support_bits: f64,
pub achieved_block_l0: f64,
pub dictionary_bits: f64,
pub estimation_rows: i64,
pub amortization_horizon: i64,
pub per_target: Vec<Eq4TargetBits>,
pub native_bits_per_token: Option<f64>,
}
fn covariance_eigenvalues(values: ArrayView2<f64>) -> Result<Array1<f64>, String> {
let centered = column_centered(values);
let n = values.nrows();
let denom = (n.saturating_sub(1)).max(1) as f64;
let mut covariance = centered.t().dot(¢ered);
covariance.mapv_inplace(|v| v / denom);
let (eigenvalues, _vectors) = covariance
.eigh(faer::Side::Lower)
.map_err(|e| format!("residual covariance eigensolve failed: {e:?}"))?;
Ok(eigenvalues)
}
fn column_centered(values: ArrayView2<f64>) -> Array2<f64> {
let mean = values
.mean_axis(ndarray::Axis(0))
.expect("nonempty matrix has a column mean");
let mut centered = values.to_owned();
for mut row in centered.rows_mut() {
row -= &mean;
}
centered
}
fn atom_code_spectrum(contribution: ArrayView2<f64>, code_dim: usize) -> Result<Vec<f64>, String> {
let rows = contribution.nrows();
let centered = column_centered(contribution);
let denom = (rows.saturating_sub(1)).max(1) as f64;
if code_dim == 1 {
let frobenius_sq: f64 = centered.iter().map(|&value| value * value).sum();
return Ok(vec![frobenius_sq / denom]);
}
let (_u, singular_values, _vt) = centered
.svd(false, false)
.map_err(|e| format!("atom contribution SVD failed: {e:?}"))?;
let keep = code_dim.min(singular_values.len());
Ok(singular_values
.iter()
.take(keep)
.map(|&s| s * s / denom)
.collect())
}
pub fn eq4_fixed_distortion_description_length<F>(
test_x: ArrayView2<f64>,
recon: ArrayView2<f64>,
gate: ArrayView2<f64>,
code_dims: &[i64],
dictionary_params: i64,
amortization_horizon: i64,
r2_targets: &[f64],
native_bits_per_token: Option<f64>,
mut fetch_contribution: F,
) -> Result<Eq4DescriptionLength, String>
where
F: FnMut(usize, &[usize]) -> Result<Array2<f64>, String>,
{
let (n, d) = (test_x.nrows(), test_x.ncols());
if test_x.dim() != recon.dim() {
return Err(format!(
"test_x and recon must have the same shape, got {:?} and {:?}",
test_x.dim(),
recon.dim()
));
}
if n == 0 || d == 0 {
return Err("test_x must contain at least one row and one column".to_string());
}
let n_atoms = gate.ncols();
if gate.nrows() != n {
return Err(format!(
"gate and recon must contain the same number of rows, got {} and {}",
gate.nrows(),
n
));
}
if code_dims.len() != n_atoms {
return Err(format!(
"code_dims must have one entry per atom, got {} for {} atoms",
code_dims.len(),
n_atoms
));
}
if code_dims.iter().any(|&dimension| dimension < 0) {
return Err("code_dims must contain only nonnegative dimensions".to_string());
}
if dictionary_params < 0 {
return Err("dictionary_params must be nonnegative".to_string());
}
if amortization_horizon < 2 {
return Err(format!(
"amortization_horizon must be at least 2 (the declared message/deployment \
or training-observation N); it is passed separately from the {n}-row \
estimation subsample and is never defaulted to it, got {amortization_horizon}"
));
}
if !test_x.iter().all(|v| v.is_finite()) || !recon.iter().all(|v| v.is_finite()) {
return Err("test_x and recon must contain only finite values".to_string());
}
if !gate.iter().all(|v| v.is_finite()) {
return Err("gate must contain only finite values".to_string());
}
if r2_targets.is_empty() {
return Err("r2_targets must not be empty".to_string());
}
if !r2_targets
.iter()
.all(|&t| t.is_finite() && (0.0..1.0).contains(&t))
{
return Err("every R-squared target must be finite and in [0, 1)".to_string());
}
if native_bits_per_token.is_some_and(|bits| !bits.is_finite() || bits < 0.0) {
return Err("native_bits_per_token must be finite and nonnegative".to_string());
}
let mut active_per_atom = vec![0.0_f64; n_atoms];
let mut total_active = 0.0_f64;
for row in 0..n {
for atom in 0..n_atoms {
if gate[[row, atom]] > GATE_ACTIVE_THRESHOLD {
active_per_atom[atom] += 1.0;
total_active += 1.0;
}
}
}
let p_g: Vec<f64> = active_per_atom.iter().map(|&c| c / n as f64).collect();
let l0 = total_active / n as f64;
let support_cardinality = (l0.round_ties_even() as i64).clamp(0, n_atoms as i64);
let support_bits = selection_bits(n_atoms as i64, support_cardinality);
let independent_support_bits: f64 = p_g
.iter()
.filter(|&&probability| probability > 0.0 && probability < 1.0)
.map(|&probability| {
-(probability * probability.log2() + (1.0 - probability) * (1.0 - probability).log2())
})
.sum();
let mut residual = test_x.to_owned();
residual -= &recon;
let residual_covariance_eigenvalues = covariance_eigenvalues(residual.view())?;
let centered_x = column_centered(test_x);
let reference_variance = centered_x.iter().map(|&v| v * v).sum::<f64>() / n as f64;
if reference_variance <= 0.0 {
return Err("test_x must have positive variance".to_string());
}
let mut code_spectra: Vec<Vec<f64>> = Vec::with_capacity(n_atoms);
for atom in 0..n_atoms {
let code_dim = code_dims[atom] as usize;
let rows: Vec<usize> = (0..n)
.filter(|&row| gate[[row, atom]] > GATE_ACTIVE_THRESHOLD)
.collect();
if rows.len() < (code_dim + 1).max(4) {
code_spectra.push(vec![0.0; code_dim]);
continue;
}
let take: Vec<usize> = if rows.len() <= SPECTRUM_ROW_CAP {
rows
} else {
let step = rows.len().div_ceil(SPECTRUM_ROW_CAP);
rows.iter().step_by(step).copied().collect()
};
let contribution = fetch_contribution(atom, &take)?;
if contribution.dim() != (take.len(), d) {
return Err(format!(
"atom {atom} contribution has shape {:?}; expected {:?}",
contribution.dim(),
(take.len(), d)
));
}
if !contribution.iter().all(|v| v.is_finite()) {
return Err(format!(
"atom {atom} contribution contains non-finite values"
));
}
code_spectra.push(atom_code_spectrum(contribution.view(), code_dim)?);
}
let horizon = amortization_horizon as f64;
let dictionary_bits = 0.5 * dictionary_params as f64 / horizon * horizon.log2();
let mut per_target = Vec::with_capacity(r2_targets.len());
for &target in r2_targets {
let total_distortion = (1.0 - target) * reference_variance;
let mut components: Vec<(f64, Vec<f64>)> = p_g
.iter()
.zip(code_spectra.iter())
.map(|(&probability, spectrum)| (probability, spectrum.clone()))
.collect();
components.push((1.0, residual_covariance_eigenvalues.to_vec()));
let component_bits = weighted_reverse_water_filling(&components, total_distortion)?;
let code_bits: f64 = component_bits[..n_atoms].iter().sum();
let resid_bits = component_bits[n_atoms];
per_target.push(Eq4TargetBits {
target,
bits: support_bits + code_bits + resid_bits + dictionary_bits,
code_bits,
resid_bits,
});
}
Ok(Eq4DescriptionLength {
support_bits,
independent_support_bits,
achieved_block_l0: l0,
dictionary_bits,
estimation_rows: n as i64,
amortization_horizon,
per_target,
native_bits_per_token,
})
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
const FIXTURE_HORIZON: i64 = 4096;
fn fixture(code_dims: &[i64], dictionary_params: i64) -> Result<Eq4DescriptionLength, String> {
let test_x = array![
[0.0, 0.0],
[1.0, 0.5],
[2.0, 1.5],
[3.0, 1.0],
[4.0, 2.0],
[5.0, 3.0],
];
let recon = test_x.mapv(|value| 0.8 * value);
let gate = Array2::ones((test_x.nrows(), 1));
let contribution = recon.clone();
eq4_fixed_distortion_description_length(
test_x.view(),
recon.view(),
gate.view(),
code_dims,
dictionary_params,
FIXTURE_HORIZON,
&[0.9],
Some(1.25),
move |_, take| {
let mut selected = Array2::zeros((take.len(), contribution.ncols()));
for (out_row, &source_row) in take.iter().enumerate() {
selected
.row_mut(out_row)
.assign(&contribution.row(source_row));
}
Ok(selected)
},
)
}
#[test]
fn production_eq4_fixture_reconciles_report_terms() {
let result = fixture(&[1], 4).unwrap();
assert_eq!(result.support_bits, selection_bits(1, 1));
assert_eq!(result.achieved_block_l0, 1.0);
assert_eq!(result.native_bits_per_token, Some(1.25));
assert_eq!(result.per_target.len(), 1);
assert_eq!(result.estimation_rows, 6);
assert_eq!(result.amortization_horizon, FIXTURE_HORIZON);
let target = result.per_target[0];
let horizon = FIXTURE_HORIZON as f64;
let dictionary_bits = 0.5 * 4.0 / horizon * horizon.log2();
assert_eq!(result.dictionary_bits, dictionary_bits);
assert!(
(target.bits
- (result.support_bits + target.code_bits + target.resid_bits + dictionary_bits))
.abs()
< 1.0e-12
);
}
#[test]
fn production_eq4_rejects_negative_dimensions_and_dictionary_cost() {
assert!(fixture(&[-1], 0).unwrap_err().contains("code_dims"));
assert!(fixture(&[1], -1).unwrap_err().contains("dictionary_params"));
}
#[test]
fn production_eq4_rejects_a_sub_two_amortization_horizon() {
let test_x = array![[0.0, 0.0], [1.0, 0.5], [2.0, 1.5], [3.0, 1.0]];
let recon = test_x.mapv(|value| 0.8 * value);
let gate = Array2::ones((test_x.nrows(), 1));
let contribution = recon.clone();
for horizon in [1_i64, 0, -8] {
let err = eq4_fixed_distortion_description_length(
test_x.view(),
recon.view(),
gate.view(),
&[1],
4,
horizon,
&[0.9],
None,
|_, take| {
let mut selected = Array2::zeros((take.len(), contribution.ncols()));
for (out_row, &source_row) in take.iter().enumerate() {
selected
.row_mut(out_row)
.assign(&contribution.row(source_row));
}
Ok(selected)
},
)
.unwrap_err();
assert!(
err.contains("amortization_horizon"),
"horizon {horizon} error should name amortization_horizon: {err}"
);
}
}
#[test]
fn eq4_dictionary_term_is_invariant_to_the_estimation_subsample() {
const FULL_ROWS: usize = 8192;
const BLOCK_ROWS: usize = 8;
let mut test_x = Array2::<f64>::zeros((FULL_ROWS, 2));
let mut recon = Array2::<f64>::zeros((FULL_ROWS, 2));
let mut gate = Array2::<f64>::zeros((FULL_ROWS, 1));
for row in 0..FULL_ROWS {
let within = row % BLOCK_ROWS;
let code = match within {
0 | 2 => 1.0,
4 | 6 => -1.0,
_ => 0.0,
};
let active = within % 2 == 0;
let residual = if within < 4 { 1.0 } else { -1.0 };
recon[[row, 0]] = code;
test_x[[row, 0]] = code;
test_x[[row, 1]] = residual;
gate[[row, 0]] = if active { 1.0 } else { 0.0 };
}
let horizon = 120_000_i64;
let dictionary_params = 4096_i64;
let targets = [0.99, 0.95, 0.90, 0.80];
let score_at = |rows: usize| -> Eq4DescriptionLength {
let window = ndarray::s![..rows, ..];
let contribution = recon.slice(window);
eq4_fixed_distortion_description_length(
test_x.slice(window),
recon.slice(window),
gate.slice(window),
&[1],
dictionary_params,
horizon,
&targets,
None,
move |_, take| {
let mut selected = Array2::zeros((take.len(), contribution.ncols()));
for (out_row, &source_row) in take.iter().enumerate() {
selected
.row_mut(out_row)
.assign(&contribution.row(source_row));
}
Ok(selected)
},
)
.expect("balanced Eq. 4 fixture must score")
};
let small = score_at(256);
let medium = score_at(1024);
let large = score_at(FULL_ROWS);
for run in [&small, &medium, &large] {
assert_eq!(run.amortization_horizon, horizon);
assert_eq!(run.achieved_block_l0, 0.5);
assert_eq!(run.support_bits, 0.0);
assert_eq!(run.independent_support_bits, 1.0);
}
assert_eq!(small.estimation_rows, 256);
assert_eq!(medium.estimation_rows, 1024);
assert_eq!(large.estimation_rows, FULL_ROWS as i64);
let expected_dictionary =
0.5 * dictionary_params as f64 / horizon as f64 * (horizon as f64).log2();
assert_eq!(small.dictionary_bits, expected_dictionary);
assert_eq!(medium.dictionary_bits, expected_dictionary);
assert_eq!(large.dictionary_bits, expected_dictionary);
let code_variance = |rows: usize| (rows as f64 / 2.0) / (rows as f64 / 2.0 - 1.0);
let residual_variance = |rows: usize| rows as f64 / (rows as f64 - 1.0);
let expected_drift = |rows: usize| {
0.25 * (code_variance(rows) / code_variance(FULL_ROWS)).log2()
+ 0.5 * (residual_variance(rows) / residual_variance(FULL_ROWS)).log2()
};
let small_drift = expected_drift(256);
let medium_drift = expected_drift(1024);
assert!(small_drift > medium_drift && medium_drift > 0.0);
for (target_index, &target) in targets.iter().enumerate() {
let observed_small =
small.per_target[target_index].bits - large.per_target[target_index].bits;
let observed_medium =
medium.per_target[target_index].bits - large.per_target[target_index].bits;
let tolerance =
1.0e-11 * (1.0 + large.per_target[target_index].bits.abs() + small_drift.abs());
assert!(
(observed_small - small_drift).abs() <= tolerance,
"target {target}: 256-row drift {observed_small:.17e} != derived \
{small_drift:.17e} within {tolerance:.3e}"
);
assert!(
(observed_medium - medium_drift).abs() <= tolerance,
"target {target}: 1024-row drift {observed_medium:.17e} != derived \
{medium_drift:.17e} within {tolerance:.3e}"
);
}
let legacy_dictionary =
|rows: usize| 0.5 * dictionary_params as f64 * (rows as f64).log2() / rows as f64;
let legacy_swing = (legacy_dictionary(256) - legacy_dictionary(FULL_ROWS)).abs();
assert!((legacy_swing - 60.75).abs() < 1.0e-12);
assert!(legacy_swing > 1000.0 * small_drift);
}
#[test]
fn flat_atom_fast_path_matches_svd_to_tolerance() {
let codes = array![0.3_f64, -1.2, 2.5, 0.0, 4.1, -0.7];
let decoder = array![1.5_f64, -0.5, 2.0, 0.25];
let mut contribution = Array2::<f64>::zeros((codes.len(), decoder.len()));
for (i, &code) in codes.iter().enumerate() {
for (j, &weight) in decoder.iter().enumerate() {
contribution[[i, j]] = code * weight;
}
}
let fast = atom_code_spectrum(contribution.view(), 1).unwrap();
let centered = column_centered(contribution.view());
let (_u, singular_values, _vt) = centered.svd(false, false).unwrap();
let denom = (codes.len() - 1) as f64;
let svd_spectrum = singular_values[0] * singular_values[0] / denom;
assert_eq!(fast.len(), 1);
assert!(
(fast[0] - svd_spectrum).abs() <= 1.0e-10 * (1.0 + svd_spectrum.abs()),
"fast {} vs svd {}",
fast[0],
svd_spectrum
);
if singular_values.len() > 1 {
assert!(
singular_values[1] <= 1.0e-9 * singular_values[0].max(1.0),
"flat contribution was not rank-one: {singular_values:?}"
);
}
}
#[test]
fn curved_atom_still_uses_full_svd_spectrum() {
let contribution = array![
[1.0_f64, 0.0, 0.5],
[0.0, 2.0, 0.5],
[1.0, 2.0, 1.0],
[2.0, 1.0, 1.5],
[3.0, 0.0, 1.5],
];
let spectrum = atom_code_spectrum(contribution.view(), 2).unwrap();
assert_eq!(spectrum.len(), 2);
let centered = column_centered(contribution.view());
let (_u, singular_values, _vt) = centered.svd(false, false).unwrap();
let denom = (contribution.nrows() - 1) as f64;
for (k, value) in spectrum.iter().enumerate() {
assert!((value - singular_values[k] * singular_values[k] / denom).abs() < 1.0e-12);
}
}
}