use crate::atom_codes::SparseAtomCodes;
pub fn scalar_rate_bits(signal_var: f64, delta2: f64) -> f64 {
if signal_var <= 0.0 {
return 0.0;
}
if delta2 <= 0.0 {
return f64::INFINITY;
}
(0.5 * (signal_var / delta2).log2()).max(0.0)
}
pub fn selection_bits(g_dict: i64, k_active: i64) -> f64 {
if g_dict <= 0 || k_active <= 0 {
return 0.0;
}
let k = k_active.min(g_dict);
let mut bits = 0.0;
for i in 1..=k {
bits += ((g_dict - k + i) as f64 / i as f64).log2();
}
bits
}
fn exact_weighted_water_level(breakpoints: &mut Vec<(f64, f64)>, total_distortion: f64) -> f64 {
breakpoints.sort_by(|(left, _), (right, _)| left.total_cmp(right));
let mut saturated_distortion = 0.0_f64;
let mut active_weight: f64 = breakpoints.iter().map(|(_, weight)| weight).sum();
let mut index = 0usize;
loop {
let next_breakpoint = breakpoints[index].0;
let candidate = (total_distortion - saturated_distortion) / active_weight;
if candidate <= next_breakpoint {
return candidate;
}
while index < breakpoints.len() && breakpoints[index].0 == next_breakpoint {
let (variance, weight) = breakpoints[index];
saturated_distortion += weight * variance;
active_weight -= weight;
index += 1;
}
if index == breakpoints.len() {
return next_breakpoint;
}
}
}
pub fn weighted_reverse_water_filling(
components: &[(f64, Vec<f64>)],
total_distortion: f64,
) -> Result<Vec<f64>, String> {
if !total_distortion.is_finite() || total_distortion <= 0.0 {
return Err(format!(
"total distortion must be finite and positive, got {total_distortion}"
));
}
let mut breakpoints: Vec<(f64, f64)> = Vec::new();
let mut spectra: Vec<(f64, Vec<f64>)> = Vec::with_capacity(components.len());
let mut total_variance = 0.0_f64;
for (weight, spectrum) in components {
if !weight.is_finite() || *weight < 0.0 {
return Err(format!(
"component weight must be finite and nonnegative, got {weight}"
));
}
let mut variances = Vec::with_capacity(spectrum.len());
for &value in spectrum {
if !value.is_finite() {
return Err("component spectrum must contain only finite values".to_string());
}
let variance = value.max(0.0);
variances.push(variance);
total_variance += *weight * variance;
if *weight > 0.0 {
breakpoints.push((variance, *weight));
}
}
spectra.push((*weight, variances));
}
if total_distortion >= total_variance || breakpoints.is_empty() {
return Ok(vec![0.0; spectra.len()]);
}
let water_level = exact_weighted_water_level(&mut breakpoints, total_distortion);
Ok(spectra
.iter()
.map(|(weight, variances)| {
*weight
* variances
.iter()
.map(|&variance| scalar_rate_bits(variance, water_level))
.sum::<f64>()
})
.collect())
}
pub fn reverse_water_filling(eigs: &[f64], delta2: f64) -> (f64, Vec<f64>) {
if eigs.is_empty() {
return (0.0, Vec::new());
}
let variances: Vec<f64> = eigs.iter().map(|&value| value.max(0.0)).collect();
if delta2 <= 0.0 {
let per: Vec<f64> = variances
.iter()
.map(|&variance| if variance > 0.0 { f64::INFINITY } else { 0.0 })
.collect();
return (per.iter().sum(), per);
}
let total_variance: f64 = variances.iter().sum();
if delta2 >= total_variance {
return (0.0, vec![0.0; variances.len()]);
}
let component_rates = weighted_reverse_water_filling(&[(1.0, variances.clone())], delta2)
.expect("positive finite one-component water-fill inputs are valid");
let mut breakpoints: Vec<(f64, f64)> = variances
.iter()
.copied()
.map(|variance| (variance, 1.0))
.collect();
let theta = exact_weighted_water_level(&mut breakpoints, delta2);
let per: Vec<f64> = variances
.iter()
.map(|&e| scalar_rate_bits(e, theta))
.collect();
(component_rates[0], per)
}
#[derive(Clone, Copy, Debug)]
pub struct BirthMdlPrescreen {
pub rho: f64,
pub span: f64,
pub intrinsic_dim: usize,
pub basis_size: usize,
pub signal_var: f64,
pub noise_floor: f64,
pub n_tokens: f64,
pub p_out: usize,
pub g_dict: usize,
pub l0: f64,
}
#[must_use]
pub fn predicted_birth_dl_bits(p: &BirthMdlPrescreen) -> f64 {
let span = p.span;
let code_bits =
(span - p.intrinsic_dim as f64 - 1.0) * scalar_rate_bits(p.signal_var, p.noise_floor);
let support_bits = if p.g_dict > 0 && p.l0 > 0.0 {
(span - 1.0) * (p.g_dict as f64 / p.l0).log2()
} else {
0.0
};
let n = p.n_tokens.max(0.0);
let saving = p.rho.clamp(0.0, 1.0) * n * (code_bits + support_bits);
let log2_n = if n >= 2.0 { n.log2() } else { 0.0 };
let dictionary_delta = (p.basis_size as f64 - span) * p.p_out as f64 * 0.5 * log2_n;
saving - dictionary_delta
}
#[derive(Clone, Debug)]
pub struct Featurizer {
pub name: String,
pub kind: String,
pub coded_var: Vec<f64>,
pub n_params: i64,
pub ev: f64,
pub total_var: f64,
pub n_tokens: i64,
pub n_firings: i64,
pub g_dict: i64,
pub k_active: i64,
pub support_entropy_bits: Option<f64>,
}
impl Featurizer {
pub fn m(&self) -> usize {
self.coded_var.len()
}
pub fn residual(&self) -> f64 {
(1.0 - self.ev) * self.total_var
}
pub fn selection_bits_combinatorial(&self) -> f64 {
selection_bits(self.g_dict, self.k_active)
}
pub fn selection_bits_charged(&self) -> f64 {
self.support_entropy_bits
.unwrap_or_else(|| self.selection_bits_combinatorial())
}
}
#[derive(Clone, Debug)]
pub struct ScoreRow {
pub name: String,
pub kind: String,
pub coded_dim_m: usize,
pub code_bits_per_firing: f64,
pub code_coeff_bits_per_firing: f64,
pub selection_bits_per_firing: f64,
pub selection_bits_combinatorial_per_firing: f64,
pub n_params: i64,
pub l_param_bits: f64,
pub dict_bits: f64,
pub code_bits_total: f64,
pub total_bits: f64,
pub bits_per_token: f64,
pub residual_achieved: f64,
pub distortion_floor: f64,
pub distortion_infeasible: bool,
}
pub fn score(feat: &Featurizer, delta2: f64, l_param_bits: Option<f64>) -> ScoreRow {
let (code_coeff, _) = reverse_water_filling(&feat.coded_var, delta2);
let sel_comb = feat.selection_bits_combinatorial();
let sel = feat.selection_bits_charged();
let code_per_firing = code_coeff + sel;
let m = feat.m();
let l_param = l_param_bits.unwrap_or_else(|| {
if m > 0 {
code_coeff / m as f64
} else {
scalar_rate_bits(feat.total_var, delta2)
}
});
let dict_bits = feat.n_params as f64 * l_param;
let code_total = code_per_firing * feat.n_firings as f64;
let total = code_total + dict_bits;
let residual = feat.residual();
ScoreRow {
name: feat.name.clone(),
kind: feat.kind.clone(),
coded_dim_m: m,
code_bits_per_firing: code_per_firing,
code_coeff_bits_per_firing: code_coeff,
selection_bits_per_firing: sel,
selection_bits_combinatorial_per_firing: sel_comb,
n_params: feat.n_params,
l_param_bits: l_param,
dict_bits,
code_bits_total: code_total,
total_bits: total,
bits_per_token: if feat.n_tokens > 0 {
total / feat.n_tokens as f64
} else {
f64::INFINITY
},
residual_achieved: residual,
distortion_floor: delta2,
distortion_infeasible: residual > delta2 * 1.02,
}
}
#[derive(Clone, Debug)]
pub struct Crossover {
pub block: String,
pub chart: String,
pub delta_code_bits_per_firing: f64,
pub delta_coeff_bits_per_firing: f64,
pub selection_bits_delta: f64,
pub selection_bits_delta_combinatorial: f64,
pub selection_asymmetric: bool,
pub phi_extra_params: i64,
pub r_per_freed_coord_bits: f64,
pub l_param_bits: f64,
pub f_star: f64,
pub f_star_matched_simple: f64,
pub chart_wins_at_actual_f: bool,
pub actual_firings: i64,
}
#[derive(Clone, Copy, Debug)]
pub struct DescriptionLength {
pub code_bits: f64,
pub selection_bits: f64,
pub dict_bits: f64,
pub total_bits: f64,
pub bits_per_token: f64,
}
impl DescriptionLength {
}
pub fn circle_coding_gain_bits(a: f64, delta: f64) -> f64 {
if !(a > 0.0) || !(delta > 0.0) {
return 0.0;
}
use std::f64::consts::PI;
0.5 * (3.0 * a * a / (PI * PI * delta * delta)).log2()
}
pub fn se_resolution_bits(se: f64) -> f64 {
if !se.is_finite() || se < 0.0 {
return 0.0;
}
if se == 0.0 {
return f64::INFINITY;
}
let bits = -0.5 * (12.0 * se * se).log2();
bits.max(0.0)
}
#[derive(Clone, Copy, Debug)]
pub struct MatchedDl {
pub coded_columns: i64,
pub ambient_p: i64,
pub l_param_bits: f64,
pub param_bits: f64,
pub coords_per_firing: i64,
pub coding_bits: f64,
pub n_firings: i64,
pub total_dl_bits: f64,
pub ev: f64,
pub dl_per_ev: f64,
}
pub fn matched_dl(
coded_columns: i64,
coords_per_firing: i64,
ambient_p: i64,
l_param_bits: f64,
per_firing_se: &[f64],
ev: f64,
) -> MatchedDl {
let coded_columns = coded_columns.max(0);
let coords_per_firing = coords_per_firing.max(0);
let ambient_p = ambient_p.max(0);
let param_bits = coded_columns as f64 * ambient_p as f64 * l_param_bits.max(0.0);
let coding_bits: f64 = coords_per_firing as f64
* per_firing_se
.iter()
.map(|&se| se_resolution_bits(se))
.sum::<f64>();
let total = param_bits + coding_bits;
let dl_per_ev = if ev > 0.0 { total / ev } else { f64::INFINITY };
MatchedDl {
coded_columns,
ambient_p,
l_param_bits,
param_bits,
coords_per_firing,
coding_bits,
n_firings: per_firing_se.len() as i64,
total_dl_bits: total,
ev,
dl_per_ev,
}
}
pub fn matched_dl_delta(flat: &MatchedDl, chart: &MatchedDl) -> f64 {
flat.total_dl_bits - chart.total_dl_bits
}
#[derive(Clone, Copy, Debug)]
pub struct ManifoldFitDl {
pub ev: f64,
pub n_tokens: i64,
pub k_active: f64,
pub coord_dim: f64,
pub g_dict: i64,
pub n_params: i64,
pub coordinate_rate_bits: f64,
pub l_param_bits: f64,
pub selection_bits_per_token: f64,
pub code_bits_per_token: f64,
pub dict_bits_per_token: f64,
pub code_bits: f64,
pub selection_bits: f64,
pub dict_bits: f64,
pub total_bits: f64,
pub bits_per_token: f64,
}
pub fn manifold_fit_description_length(
codes: &SparseAtomCodes,
coord_variances: &[f64],
delta2: f64,
atom_coord_dims: &[f64],
ev: f64,
n_params: i64,
l_param_bits: Option<f64>,
) -> ManifoldFitDl {
let n_tokens = codes.n_obs() as i64;
let g_dict = codes.k_atoms() as i64;
let support = codes.support_entropy();
let selection_bits_per_token = support.tree_bits;
let k_active = support.mean_support;
let (coord_total_bits, _per_coord) = reverse_water_filling(coord_variances, delta2);
let n_coords = coord_variances.len();
let coordinate_rate_bits = if n_coords > 0 {
coord_total_bits / n_coords as f64
} else {
0.0
};
let mut coded_scalars = 0.0_f64;
let mut total_firings = 0.0_f64;
for code in codes.iter() {
for atom in code.active_mask.iter_ones() {
total_firings += 1.0;
coded_scalars += atom_coord_dims.get(atom).copied().unwrap_or(0.0);
}
}
let coord_dim = if total_firings > 0.0 {
coded_scalars / total_firings
} else {
0.0
};
let l_param = l_param_bits.unwrap_or(coordinate_rate_bits).max(0.0);
let dict_bits = n_params.max(0) as f64 * l_param;
let n = n_tokens.max(0) as f64;
let code_bits = coded_scalars * coordinate_rate_bits;
let code_bits_per_token = if n > 0.0 { code_bits / n } else { 0.0 };
let selection_bits_total = n * selection_bits_per_token;
let total_bits = code_bits + selection_bits_total + dict_bits;
let (bits_per_token, dict_bits_per_token) = if n_tokens > 0 {
(total_bits / n, dict_bits / n)
} else {
(f64::INFINITY, f64::INFINITY)
};
ManifoldFitDl {
ev,
n_tokens,
k_active,
coord_dim,
g_dict,
n_params,
coordinate_rate_bits,
l_param_bits: l_param,
selection_bits_per_token,
code_bits_per_token,
dict_bits_per_token,
code_bits,
selection_bits: selection_bits_total,
dict_bits,
total_bits,
bits_per_token,
}
}
#[cfg(test)]
#[path = "description_length_tests.rs"]
mod description_length_tests;