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 achieved_block_l0: f64,
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,
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 !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 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 dictionary_bits = 0.5 * dictionary_params as f64 / n as f64 * (n.max(2) as f64).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,
achieved_block_l0: l0,
per_target,
native_bits_per_token,
})
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
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,
&[0.9],
Some(1.25),
move |_atom, 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);
let target = result.per_target[0];
let dictionary_bits = 0.5 * 4.0 / 6.0 * 6.0_f64.log2();
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 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);
}
}
}