memory.rs

21.0 kB · rust · 636 lines

1use mrlycore::errors::{value_error, Result};23/// The largest digit span a rule may read, so its code fits a `u64`.4pub const SPAN: usize = 6;56/// A rule on `k` consecutive digits of a design word.7///8/// The alphabet is the `2^D` digit vectors `d` of `{0, 1}^D`, each read as the corner integer `c = sum_i d[i] 2^i`, with `x` bit `0`, `y` bit `1` and `z` bit `2`.9/// A window is `k` digits `(c_1, ..., c_k)` read as `w = sum_j c_j 2^(D (k - j))`, the first digit most significant, and the code has bit `w` set exactly when that window is allowed.10/// The code therefore lives in `[0, 2^(2^(k D)))`, which forces `k D <= SPAN`.11/// A word `d_1 d_2 ... d_L` is read coarsest digit first and is accepted when every window of `k` consecutive digits is allowed; for `L < k` there is no window, so every word is accepted.12/// Width 1 is the memoryless design of the same code: the cells of `bang dim <dim>, code <code>` at that level.13#[derive(Clone, Copy, Debug, PartialEq, Eq)]14pub struct Rule {15    /// The dimension `D`, one to three.16    pub dimension: usize,17    /// The window width `k`, at least one.18    pub width: usize,19    /// The window code, bit `w` set when window `w` is allowed.20    pub code: u64,21}2223impl Rule {24    /// Builds a rule, or an error when the dimension, the width or the code is out of range.25    ///26    /// ```27    /// assert!(mrlynum::memory::Rule::new(1, 2, 7).is_ok());28    /// assert!(mrlynum::memory::Rule::new(2, 4, 0).is_err());29    /// ```30    pub fn new(dimension: usize, width: usize, code: u64) -> Result<Rule> {31        if !(1..=3).contains(&dimension) {32            return value_error(format!("dimension {dimension} is not one, two or three."));33        }34        if width < 1 {35            return value_error("width must be at least one.");36        }37        if width * dimension > SPAN {38            return value_error(format!(39                "width {width} at dimension {dimension} reads {} digit bits, over the span of {SPAN}.",40                width * dimension41            ));42        }43        let rule = Rule {44            dimension,45            width,46            code: 0,47        };48        let bound = rule.codes();49        if u128::from(code) >= bound {50            return value_error(format!(51                "code {code} is out of range for width {width} at dimension {dimension} (0..{}).",52                bound - 153            ));54        }55        Ok(Rule {56            dimension,57            width,58            code,59        })60    }6162    /// Returns the rule that allows every window.63    ///64    /// ```65    /// assert_eq!(mrlynum::memory::Rule::full(1, 2).code, 15);66    /// ```67    pub fn full(dimension: usize, width: usize) -> Rule {68        let rule = Rule::new(dimension, width, 0).expect("a zero code is always in range");69        Rule {70            code: (rule.codes() - 1) as u64,71            ..rule72        }73    }7475    /// Returns the letter count `2^D`, the digit vectors of the cube's corners.76    pub fn letters(&self) -> usize {77        1 << self.dimension78    }7980    /// Returns the window count `2^(k D)`.81    pub fn windows(&self) -> usize {82        1 << (self.width * self.dimension)83    }8485    /// Returns the count of rules of this shape, `2^(2^(k D))`.86    pub fn codes(&self) -> u128 {87        1u128 << self.windows()88    }8990    /// Returns the state count `2^((k - 1) D)`, the windows of one digit less that the transfer matrix runs on.91    pub fn states(&self) -> usize {92        1 << ((self.width - 1) * self.dimension)93    }9495    /// Returns whether the window is allowed, and false for any window out of range.96    ///97    /// ```98    /// let golden = mrlynum::memory::Rule::new(1, 2, 7).unwrap();99    /// assert!(golden.allowed(2));100    /// assert!(!golden.allowed(3));101    /// ```102    pub fn allowed(&self, window: usize) -> bool {103        window < self.windows() && (self.code >> window) & 1 == 1104    }105106    /// Returns whether a word, coarsest digit first, is accepted.107    ///108    /// ```109    /// let golden = mrlynum::memory::Rule::new(1, 2, 7).unwrap();110    /// assert!(golden.accepts(&[1, 0, 1]));111    /// assert!(!golden.accepts(&[1, 1, 0]));112    /// ```113    pub fn accepts(&self, word: &[usize]) -> bool {114        if word.len() < self.width {115            return word.iter().all(|&c| c < self.letters());116        }117        word.windows(self.width).all(|slice| {118            let mut w = 0;119            for &c in slice {120                w = (w << self.dimension) | c;121            }122            self.allowed(w)123        })124    }125126    /// Returns the letters that stand in at least one allowed window.127    ///128    /// ```129    /// assert_eq!(mrlynum::memory::Rule::new(1, 2, 7).unwrap().alphabet(), vec![0, 1]);130    /// assert!(mrlynum::memory::Rule::new(1, 2, 0).unwrap().alphabet().is_empty());131    /// ```132    pub fn alphabet(&self) -> Vec<usize> {133        let mut seen = vec![false; self.letters()];134        for w in 0..self.windows() {135            if !self.allowed(w) {136                continue;137            }138            for j in 0..self.width {139                seen[(w >> (self.dimension * j)) & (self.letters() - 1)] = true;140            }141        }142        (0..self.letters()).filter(|&c| seen[c]).collect()143    }144}145146// THE TRANSFER MATRIX147148/// Returns the transfer matrix on the `(k - 1)`-windows: entry `(s, t)` is one when the window that overlaps state `s` onto state `t` is allowed.149///150/// At `k = 1` the one state is the empty window and the single entry counts the allowed letters.151///152/// ```153/// let golden = mrlynum::memory::Rule::new(1, 2, 7).unwrap();154/// assert_eq!(mrlynum::memory::transfer(&golden), vec![vec![1, 1], vec![1, 0]]);155/// ```156pub fn transfer(rule: &Rule) -> Vec<Vec<u64>> {157    let states = rule.states();158    let letters = rule.letters();159    let mut matrix = vec![vec![0u64; states]; states];160    if rule.width == 1 {161        matrix[0][0] = (0..letters).filter(|&c| rule.allowed(c)).count() as u64;162        return matrix;163    }164    for (s, row) in matrix.iter_mut().enumerate() {165        for c in 0..letters {166            let window = (s << rule.dimension) | c;167            if !rule.allowed(window) {168                continue;169            }170            row[window & (states - 1)] = 1;171        }172    }173    matrix174}175176fn step(matrix: &[Vec<u64>], vector: &[u64]) -> Option<Vec<u64>> {177    let mut out = vec![0u64; vector.len()];178    for (s, row) in matrix.iter().enumerate() {179        for (t, &entry) in row.iter().enumerate() {180            if entry == 0 || vector[t] == 0 {181                continue;182            }183            out[s] = out[s].checked_add(entry.checked_mul(vector[t])?)?;184        }185    }186    Some(out)187}188189// THE COUNTS190191/// Returns `N_W(L)`, the count of accepted words, for `L = 1 ..= levels`, and stops early on the level whose count overruns a `u64`.192///193/// For `L < k` every word of that length is accepted, so the count is `2^(D L)`.194///195/// ```196/// let golden = mrlynum::memory::Rule::new(1, 2, 7).unwrap();197/// assert_eq!(mrlynum::memory::counts(&golden, 6), vec![2, 3, 5, 8, 13, 21]);198/// ```199pub fn counts(rule: &Rule, levels: usize) -> Vec<u64> {200    let matrix = transfer(rule);201    let mut vector = vec![1u64; rule.states()];202    let mut out = Vec::with_capacity(levels);203    for level in 1..=levels {204        if level + 1 < rule.width {205            match 1u64.checked_shl((rule.dimension * level) as u32) {206                Some(count) => out.push(count),207                None => break,208            }209            continue;210        }211        if level + 1 > rule.width {212            match step(&matrix, &vector) {213                Some(next) => vector = next,214                None => break,215            }216        }217        out.push(vector.iter().sum());218    }219    out220}221222/// Returns the accepted words of the level as cell indices of the `2^L` grid, `x` from bit `0` of every digit, `y` from bit `1`, `z` from bit `2`, coarsest digit first.223///224/// The index is row major, `z side^2 + y side + x` with `side = 2^L`, so at `k = 1` the set is the level-`L` fill of the design of the same code.225///226/// ```227/// let golden = mrlynum::memory::Rule::new(1, 2, 7).unwrap();228/// assert_eq!(mrlynum::memory::cells(&golden, 2), vec![0, 1, 2]);229/// ```230pub fn cells(rule: &Rule, level: usize) -> Vec<u64> {231    let side = 1u64 << level;232    let mut out = Vec::new();233    let mut word = vec![0usize; level];234    walk(rule, level, 0, &mut word, &mut |word| {235        let mut place = vec![0u64; rule.dimension];236        for (j, &c) in word.iter().enumerate() {237            let weight = 1u64 << (level - 1 - j);238            for (axis, seat) in place.iter_mut().enumerate() {239                *seat += ((c >> axis) & 1) as u64 * weight;240            }241        }242        out.push(place.iter().rev().fold(0, |run, &seat| run * side + seat));243    });244    out245}246247fn walk(248    rule: &Rule,249    level: usize,250    at: usize,251    word: &mut Vec<usize>,252    emit: &mut impl FnMut(&[usize]),253) {254    if at == level {255        emit(word);256        return;257    }258    for c in 0..rule.letters() {259        word[at] = c;260        if at + 1 >= rule.width {261            let start = at + 1 - rule.width;262            let mut w = 0;263            for &digit in &word[start..=at] {264                w = (w << rule.dimension) | digit;265            }266            if !rule.allowed(w) {267                continue;268            }269        }270        walk(rule, level, at + 1, word, emit);271    }272}273274// THE GROWTH275276/// The absolute `l^1` move of the normalised iterate that stops the power iteration, counted only when it holds over three consecutive sweeps.277pub const TOLERANCE: f64 = 1e-14;278279/// The sweep cap of the power iteration.280pub const SWEEPS: usize = 100_000;281282/// Returns the Perron root of the transfer matrix, the count's growth per level.283///284/// The digraph is split into its strongly connected components first; a component carrying no cycle contributes nothing, and a rule whose components all die past the window returns exactly `0`.285/// Each cyclic component is irreducible, so `I + B` is primitive and the power iteration on it converges geometrically; the iteration stops on a sustained Cauchy move of the normalised vector, never on a plateau of one scalar, and the Collatz-Wielandt ratios bracket the root.286/// The estimate is then made exact against the component's characteristic polynomial, taken by integer Faddeev-LeVerrier: an integer root is returned exactly, and otherwise the simple Perron root is bisected to the resolution of an `f64`.287/// The answer is the largest component root.288///289/// ```290/// let golden = mrlynum::memory::Rule::new(1, 2, 7).unwrap();291/// assert!((mrlynum::memory::perron(&golden) - 1.618_033_988_749_895).abs() < 1e-12);292/// assert_eq!(mrlynum::memory::perron(&mrlynum::memory::Rule::new(1, 2, 0).unwrap()), 0.0);293/// ```294pub fn perron(rule: &Rule) -> f64 {295    let matrix = transfer(rule);296    let reach = reachability(&matrix);297    let states = matrix.len();298    let mut seen = vec![false; states];299    let mut best = 0.0f64;300    for s in 0..states {301        if seen[s] || (reach[s] >> s) & 1 == 0 {302            continue;303        }304        let part: Vec<usize> = (0..states)305            .filter(|&t| t == s || ((reach[s] >> t) & 1 == 1 && (reach[t] >> s) & 1 == 1))306            .collect();307        for &t in &part {308            seen[t] = true;309        }310        let block: Vec<Vec<i128>> = part311            .iter()312            .map(|&i| part.iter().map(|&j| matrix[i][j] as i128).collect())313            .collect();314        let root = component_root(&block);315        if root > best {316            best = root;317        }318    }319    best320}321322fn reachability(matrix: &[Vec<u64>]) -> Vec<u64> {323    let states = matrix.len();324    let mut reach: Vec<u64> = matrix325        .iter()326        .map(|row| {327            row.iter()328                .enumerate()329                .filter(|(_, &entry)| entry > 0)330                .map(|(t, _)| 1u64 << t)331                .sum()332        })333        .collect();334    for _ in 0..states {335        let old = reach.clone();336        for s in 0..states {337            for t in 0..states {338                if (old[s] >> t) & 1 == 1 {339                    reach[s] |= old[t];340                }341            }342        }343        if reach == old {344            break;345        }346    }347    reach348}349350fn multiply(left: &[Vec<i128>], right: &[Vec<i128>]) -> Vec<Vec<i128>> {351    let n = left.len();352    let mut out = vec![vec![0i128; n]; n];353    for i in 0..n {354        for t in 0..n {355            if left[i][t] == 0 {356                continue;357            }358            for j in 0..n {359                out[i][j] += left[i][t] * right[t][j];360            }361        }362    }363    out364}365366fn characteristic(block: &[Vec<i128>]) -> Vec<i128> {367    let n = block.len();368    let mut poly = vec![0i128; n + 1];369    poly[n] = 1;370    let mut carry = vec![vec![0i128; n]; n];371    for step in 1..=n {372        let mut product = multiply(block, &carry);373        for (i, row) in product.iter_mut().enumerate() {374            row[i] += poly[n - step + 1];375        }376        carry = product;377        let next = multiply(block, &carry);378        let trace: i128 = (0..n).map(|i| next[i][i]).sum();379        poly[n - step] = -trace / step as i128;380    }381    poly382}383384fn value(poly: &[i128], x: f64) -> f64 {385    poly.iter().rev().fold(0.0f64, |run, &c| run * x + c as f64)386}387388fn vanishes(poly: &[i128], x: i128) -> bool {389    let mut run = 0i128;390    for &c in poly.iter().rev() {391        run = match run.checked_mul(x).and_then(|v| v.checked_add(c)) {392            Some(v) => v,393            None => return false,394        };395    }396    run == 0397}398399/// Returns the seed itself, unpolished, when no sign change of the characteristic polynomial is found within `0.25` of it, so a component whose bracket search fails reads the Collatz-Wielandt midpoint and never a wrong root.400fn component_root(block: &[Vec<i128>]) -> f64 {401    let n = block.len();402    let mut vector = vec![1.0f64 / n as f64; n];403    let mut settled = 0usize;404    for _ in 0..SWEEPS {405        let mut next = vec![0.0f64; n];406        for (s, row) in block.iter().enumerate() {407            let mut sum = vector[s];408            for (t, &entry) in row.iter().enumerate() {409                sum += entry as f64 * vector[t];410            }411            next[s] = sum;412        }413        let mass: f64 = next.iter().sum();414        for seat in next.iter_mut() {415            *seat /= mass;416        }417        let move_size: f64 = next418            .iter()419            .zip(vector.iter())420            .map(|(a, b)| (a - b).abs())421            .sum();422        vector = next;423        if move_size <= TOLERANCE {424            settled += 1;425            if settled >= 3 {426                break;427            }428        } else {429            settled = 0;430        }431    }432    let mut low = f64::INFINITY;433    let mut high = 0.0f64;434    for (s, row) in block.iter().enumerate() {435        let image: f64 = row436            .iter()437            .enumerate()438            .map(|(t, &entry)| entry as f64 * vector[t])439            .sum();440        let ratio = image / vector[s];441        low = low.min(ratio);442        high = high.max(ratio);443    }444    let seed = 0.5 * (low + high);445    let poly = characteristic(block);446    let rounded = seed.round();447    if (0.0..1e18).contains(&rounded) && vanishes(&poly, rounded as i128) {448        return rounded;449    }450    let mut under;451    let mut over;452    let mut delta = 1e-13f64;453    loop {454        under = seed - delta;455        over = seed + delta;456        if value(&poly, under) < 0.0 && value(&poly, over) > 0.0 {457            break;458        }459        delta *= 4.0;460        if delta > 0.25 {461            return seed;462        }463    }464    for _ in 0..200 {465        let middle = 0.5 * (under + over);466        if middle <= under || middle >= over {467            break;468        }469        if value(&poly, middle) < 0.0 {470            under = middle;471        } else {472            over = middle;473        }474    }475    0.5 * (under + over)476}477478/// Returns the growth exponent `log_2 rho`, the growth per digit of the accepted word count.479///480/// ```481/// let full = mrlynum::memory::Rule::full(2, 1);482/// assert!((mrlynum::memory::exponent(&full) - 2.0).abs() < 1e-12);483/// ```484pub fn exponent(rule: &Rule) -> f64 {485    let rho = perron(rule);486    if rho <= 0.0 {487        return f64::NEG_INFINITY;488    }489    rho.log2()490}491492/// Returns the count of allowed windows `card W`, the bits the code sets inside its window range.493///494/// ```495/// assert_eq!(mrlynum::memory::allowed_windows(&mrlynum::memory::Rule::new(1, 2, 7).unwrap()), 3);496/// ```497pub fn allowed_windows(rule: &Rule) -> usize {498    let windows = rule.windows();499    let mask = if windows >= 64 {500        u64::MAX501    } else {502        (1u64 << windows) - 1503    };504    (rule.code & mask).count_ones() as usize505}506507/// Returns the memory number `kappa(W) = log_2(card W) / k - log_2 rho`, the bits a digit spends on memory.508///509/// The window budget `log_2(card W) / k` is what a word of length `mk` could carry if its `m` disjoint windows were free of each other, and `log_2 rho` is what it really carries, so `kappa >= 0` and the gap is the interference the rule pays for.510/// It is zero on every product rule `W = F^k`, where `card W = card F^k` and `rho = card F`, so every `k = 1` rule reads zero; it is `f64::INFINITY` when a window is allowed and nothing survives past the window, and zero when no window is allowed.511///512/// ```513/// let golden = mrlynum::memory::Rule::new(1, 2, 7).unwrap();514/// assert!((mrlynum::memory::kappa(&golden) - 0.098_239_336_730).abs() < 1e-9);515/// assert_eq!(mrlynum::memory::kappa(&mrlynum::memory::Rule::full(2, 2)), 0.0);516/// ```517pub fn kappa(rule: &Rule) -> f64 {518    let windows = allowed_windows(rule);519    if windows == 0 {520        return 0.0;521    }522    let rho = perron(rule);523    if rho <= 0.0 {524        return f64::INFINITY;525    }526    (windows as f64).log2() / rule.width as f64 - rho.log2()527}528529#[cfg(test)]530mod tests {531    use super::*;532533    #[test]534    fn width_one_is_the_memoryless_design() {535        for code in [1u64, 7, 11, 13, 14] {536            let rule = Rule::new(2, 1, code).unwrap();537            let fills = code.count_ones() as u64;538            assert_eq!(539                counts(&rule, 4),540                vec![fills, fills.pow(2), fills.pow(3), fills.pow(4)]541            );542            assert_eq!(kappa(&rule), 0.0);543            let level = cells(&rule, 1);544            let want: Vec<u64> = (0..4).filter(|c| (code >> c) & 1 == 1).collect();545            assert_eq!(level, want);546        }547    }548549    #[test]550    fn the_golden_rule_counts_the_fibonacci_numbers() {551        let golden = Rule::new(1, 2, 7).unwrap();552        assert_eq!(counts(&golden, 8), vec![2, 3, 5, 8, 13, 21, 34, 55]);553        assert!((perron(&golden) - 1.618_033_988_749_895).abs() < 1e-12);554        assert_eq!(format!("{:.6}", kappa(&golden)), "0.098239");555    }556557    #[test]558    fn the_perron_root_is_the_spectral_radius_on_every_reducible_rule() {559        let plastic = 1.324_717_957_244_746;560        let golden = 1.618_033_988_749_895;561        let quartic = 1.380_277_569_097_614;562        for (code, want) in [563            (5u64, 1.0),564            (62, plastic),565            (91, quartic),566            (95, golden),567            (125, plastic),568            (190, plastic),569        ] {570            let rule = Rule::new(1, 3, code).unwrap();571            let root = perron(&rule);572            assert!(573                (root - want).abs() < 1e-12,574                "code {code} reads {root} against {want}"575            );576        }577    }578579    #[test]580    fn the_supergolden_rule_counts_the_narayana_cows() {581        let rule = Rule::new(1, 3, 23).unwrap();582        assert_eq!(counts(&rule, 8), vec![2, 4, 4, 6, 9, 13, 19, 28]);583        let terms = counts(&rule, 12);584        for l in 5..terms.len() {585            assert_eq!(terms[l], terms[l - 1] + terms[l - 3]);586        }587    }588589    #[test]590    fn the_full_rule_counts_every_word() {591        for dimension in 1..=3 {592            for width in 1..=SPAN / dimension {593                let rule = Rule::full(dimension, width);594                let want: Vec<u64> = (1..=5).map(|l| 1u64 << (dimension * l)).collect();595                assert_eq!(counts(&rule, 5), want, "d{dimension} k{width}");596                assert!((exponent(&rule) - dimension as f64).abs() < 1e-12);597            }598        }599    }600601    #[test]602    fn the_empty_rule_dies_past_its_window() {603        for width in 1..=3 {604            let rule = Rule::new(1, width, 0).unwrap();605            let want: Vec<u64> = (1..=5)606                .map(|l| if l < width { 1 << l } else { 0 })607                .collect();608            assert_eq!(counts(&rule, 5), want, "k{width}");609            assert_eq!(perron(&rule), 0.0);610            assert!(cells(&rule, 5).is_empty());611        }612    }613614    #[test]615    fn the_cells_are_the_accepted_words() {616        let rule = Rule::new(2, 2, 0xf1e2).unwrap();617        let level = 4;618        let side = 1u64 << level;619        let drawn = cells(&rule, level);620        assert_eq!(drawn.len() as u64, counts(&rule, level)[level - 1]);621        let mut seen = std::collections::BTreeSet::new();622        for index in &drawn {623            assert!(seen.insert(*index));624            assert!(*index < side * side);625        }626    }627628    #[test]629    fn a_rule_out_of_range_is_refused() {630        assert!(Rule::new(0, 1, 0).is_err());631        assert!(Rule::new(4, 1, 0).is_err());632        assert!(Rule::new(1, 0, 0).is_err());633        assert!(Rule::new(3, 3, 0).is_err());634        assert!(Rule::new(1, 2, 16).is_err());635    }636}