mass.rs

7.6 kB · rust · 256 lines

1use crate::design::BASE;2use mrlycore::Tensor;34pub struct Mass {5    pub radii: Vec<f64>,6    pub cumulative: Vec<u64>,7}89impl Mass {10    pub fn at(&self, radius: f64) -> u64 {11        match self12            .radii13            .binary_search_by(|probe| probe.partial_cmp(&radius).expect("finite radii"))14        {15            Ok(index) => self.cumulative[index],16            Err(0) => 0,17            Err(index) => self.cumulative[index - 1],18        }19    }2021    pub fn total(&self) -> u64 {22        *self.cumulative.last().unwrap_or(&0)23    }24}2526pub fn shells(grid: &Tensor, digit: (usize, usize)) -> Mass {27    let side = grid.shape[0];28    let bytes = grid.bytes();29    let anchor = (side as i64 * digit.0 as i64, side as i64 * digit.1 as i64);30    let mut keys: Vec<u32> = Vec::new();31    for row in 0..side {32        for col in 0..side {33            if bytes[row * side + col] == 0 {34                continue;35            }36            let dr = 2 * row as i64 + 1 - anchor.0;37            let dc = 2 * col as i64 + 1 - anchor.1;38            keys.push((dr * dr + dc * dc) as u32);39        }40    }41    keys.sort_unstable();42    let mut radii = Vec::new();43    let mut cumulative = Vec::new();44    let mut running = 0u64;45    for (index, key) in keys.iter().enumerate() {46        running += 1;47        if index + 1 == keys.len() || keys[index + 1] != *key {48            radii.push((*key as f64).sqrt() / 2.0);49            cumulative.push(running);50        }51    }52    Mass { radii, cumulative }53}5455/// The nearest filled cell to the fixed point of the digit, as four times its squared distance, an exact integer.56pub fn nearest_cell(grid: &Tensor, digit: (usize, usize)) -> u64 {57    let side = grid.shape[0];58    let bytes = grid.bytes();59    let anchor = (side as i64 * digit.0 as i64, side as i64 * digit.1 as i64);60    let reach = |at: i64, low: i64| -> i64 {61        let (a, b) = (2 * low - at, at - 2 * (low + 1));62        a.max(b).max(0)63    };64    let mut best = u64::MAX;65    for row in 0..side {66        for col in 0..side {67            if bytes[row * side + col] == 0 {68                continue;69            }70            let dr = reach(anchor.0, row as i64);71            let dc = reach(anchor.1, col as i64);72            best = best.min((dr * dr + dc * dc) as u64);73        }74    }75    best76}7778pub fn horizon(code: u128, digit: (usize, usize), table: &[(usize, usize)]) -> f64 {79    let point = (digit.0 as f64 / 2.0, digit.1 as f64 / 2.0);80    let mut best = f64::INFINITY;81    for bit in 0..9 {82        if code >> bit & 1 == 0 {83            continue;84        }85        let other = table[bit];86        if other == digit {87            continue;88        }89        let gap = |at: f64, index: usize| {90            let (low, high) = (index as f64 / 3.0, (index as f64 + 1.0) / 3.0);91            (low - at).max(at - high).max(0.0)92        };93        let (dr, dc) = (gap(point.0, other.0), gap(point.1, other.1));94        best = best.min((dr * dr + dc * dc).sqrt());95    }96    (3.0 * best).min(1.0)97}9899pub struct Ripple {100    pub slope: f64,101    pub curve: Vec<f64>,102    pub swing: f64,103    pub drift: f64,104    pub periods: usize,105}106107fn sample(mass: &Mass, low: f64, periods: usize, bins: usize) -> Vec<(f64, f64)> {108    let step = (BASE as f64).ln() / bins as f64;109    (0..periods * bins)110        .filter_map(|index| {111            let radius = low * (index as f64 * step).exp();112            let count = mass.at(radius);113            if count == 0 {114                None115            } else {116                Some((radius.ln(), (count as f64).ln()))117            }118        })119        .collect()120}121122fn fit(points: &[(f64, f64)]) -> (f64, f64) {123    let n = points.len() as f64;124    let sx: f64 = points.iter().map(|p| p.0).sum();125    let sy: f64 = points.iter().map(|p| p.1).sum();126    let sxx: f64 = points.iter().map(|p| p.0 * p.0).sum();127    let sxy: f64 = points.iter().map(|p| p.0 * p.1).sum();128    let slope = (n * sxy - sx * sy) / (n * sxx - sx * sx);129    (slope, (sy - slope * sx) / n)130}131132fn fold(points: &[(f64, f64)], dimension: f64, bins: usize) -> Vec<f64> {133    let mut sums = vec![0.0; bins];134    let mut hits = vec![0.0; bins];135    for (lx, ly) in points {136        let residual = ly - dimension * lx;137        let phase = (lx / (BASE as f64).ln()).rem_euclid(1.0);138        let bin = ((phase * bins as f64) as usize).min(bins - 1);139        sums[bin] += residual;140        hits[bin] += 1.0;141    }142    let curve: Vec<f64> = sums143        .iter()144        .zip(&hits)145        .map(|(sum, hit)| if *hit == 0.0 { f64::NAN } else { sum / hit })146        .collect();147    let live: Vec<f64> = curve.iter().copied().filter(|v| v.is_finite()).collect();148    let mean = live.iter().sum::<f64>() / live.len().max(1) as f64;149    curve.iter().map(|v| v - mean).collect()150}151152/// The whole powers of the base that fit between the window ends, counted by repeated multiplication so no logarithm can round a boundary the wrong way.153pub fn periods(low: f64, high: f64) -> usize {154    let mut count = 0usize;155    let mut edge = low * BASE as f64;156    while edge <= high * (1.0 + 1e-12) {157        count += 1;158        edge *= BASE as f64;159    }160    count161}162163pub fn ripple(mass: &Mass, low: f64, high: f64, dimension: f64, bins: usize) -> Ripple {164    let periods = periods(low, high).max(1);165    let points = sample(mass, low, periods, bins);166    let (slope, _) = fit(&points);167    let curve = fold(&points, dimension, bins);168    let swing = spread(&curve);169    let half = periods / 2;170    let drift = if half == 0 {171        f64::NAN172    } else {173        let early = fold(&sample(mass, low, half, bins), dimension, bins);174        let late = fold(175            &sample(176                mass,177                low * (BASE as f64).powi(half as i32),178                periods - half,179                bins,180            ),181            dimension,182            bins,183        );184        early185            .iter()186            .zip(&late)187            .map(|(a, b)| (a - b).abs())188            .fold(189                0.0f64,190                |best, gap| if gap.is_finite() { best.max(gap) } else { best },191            )192    };193    Ripple {194        slope,195        curve,196        swing,197        drift,198        periods,199    }200}201202pub fn spread(curve: &[f64]) -> f64 {203    let live: Vec<f64> = curve.iter().copied().filter(|v| v.is_finite()).collect();204    let low = live.iter().copied().fold(f64::INFINITY, f64::min);205    let high = live.iter().copied().fold(f64::NEG_INFINITY, f64::max);206    high - low207}208209pub fn distance(left: &[f64], right: &[f64]) -> f64 {210    left.iter()211        .zip(right)212        .map(|(a, b)| (a - b).abs())213        .fold(214            0.0f64,215            |best, gap| if gap.is_finite() { best.max(gap) } else { best },216        )217}218219pub fn scaling_error(mass: &Mass, low: f64, high: f64, fill: f64) -> f64 {220    let mut worst: f64 = 0.0;221    let mut radius = low;222    while radius * (BASE as f64) <= high {223        let near = mass.at(radius) as f64;224        let far = mass.at(radius * BASE as f64) as f64;225        if near > 0.0 {226            worst = worst.max((far / (fill * near) - 1.0).abs());227        }228        radius *= 1.05;229    }230    worst231}232233#[cfg(test)]234mod tests {235    use super::periods;236237    #[test]238    fn the_window_counts_whole_periods_at_every_level() {239        for level in 4..=12u32 {240            let side = 3f64.powi(level as i32);241            assert_eq!(242                periods(27.0, side),243                level as usize - 3,244                "corner level {level}"245            );246            assert_eq!(247                periods(27.0, side / 2.0),248                level as usize - 4,249                "centre level {level}"250            );251        }252        assert_eq!(periods(27.0, 26.0), 0);253        assert_eq!(periods(27.0, 81.0), 1);254        assert_eq!(periods(27.0, 80.9), 0);255    }256}