carry.rs

21.6 kB · rust · 651 lines

1use mrlycore::errors::{value_error, Result};23const CEILING: usize = 64;4const SCAN: usize = 8192;5const HALVINGS: usize = 200;6const SWEEPS: usize = 600;78fn overflow<T>() -> Result<T> {9    value_error("the carry arithmetic passes a hundred and twenty-eight bits.")10}1112fn product(a: i128, b: i128) -> Result<i128> {13    match a.checked_mul(b) {14        Some(value) => Ok(value),15        None => overflow(),16    }17}1819fn total(a: i128, b: i128) -> Result<i128> {20    match a.checked_add(b) {21        Some(value) => Ok(value),22        None => overflow(),23    }24}2526fn middle(base: usize) -> Result<usize> {27    if base < 3 || base.is_multiple_of(2) {28        return value_error("the base must be odd and at least three.");29    }30    Ok((base - 1) / 2)31}3233fn sized(dimension: usize) -> Result<()> {34    if !(2..=CEILING).contains(&dimension) {35        return value_error(format!("the dimension must be between 2 and {CEILING}."));36    }37    Ok(())38}3940// POLYNOMIAL4142fn convolve(left: &[i128], right: &[i128]) -> Result<Vec<i128>> {43    let mut out = vec![0i128; left.len() + right.len() - 1];44    for (i, &x) in left.iter().enumerate() {45        if x == 0 {46            continue;47        }48        for (j, &y) in right.iter().enumerate() {49            if y == 0 {50                continue;51            }52            out[i + j] = total(out[i + j], product(x, y)?)?;53        }54    }55    Ok(out)56}5758/// The digit polynomial of the base-`q` middle-digit design in dimension `D`, lowest power first.59///60/// The design keeps the cells whose digit vector has at most one coordinate equal to the middle61/// digit, so its weight generating function is `A(t)^(D-1) (A(t) + D t^m)`, with `A` the sum of62/// every digit power but the middle one. At base three that factors as63/// `(1 + t^2)^(D-1) (1 + D t + t^2)`, and the sum of the coefficients is the fill.64///65/// ```66/// assert_eq!(mrlymath::dim::carry::digit_polynomial(3, 3).unwrap(), vec![1, 3, 3, 6, 3, 3, 1]);67/// ```68pub fn digit_polynomial(base: usize, dimension: usize) -> Result<Vec<i128>> {69    let centre = middle(base)?;70    sized(dimension)?;71    let alpha: Vec<i128> = (0..base).map(|digit| i128::from(digit != centre)).collect();72    let mut poly = vec![1i128];73    for _ in 1..dimension {74        poly = convolve(&poly, &alpha)?;75    }76    let mut second = alpha;77    second[centre] += dimension as i128;78    convolve(&poly, &second)79}8081/// The count of level-one cells the design keeps, `f_D = (q - 1)^(D-1) (q - 1 + D)`.82///83/// ```84/// assert_eq!(mrlymath::dim::carry::fill(3, 3).unwrap(), 20);85/// assert_eq!(mrlymath::dim::carry::fill(5, 3).unwrap(), 112);86/// ```87pub fn fill(base: usize, dimension: usize) -> Result<i128> {88    middle(base)?;89    sized(dimension)?;90    let mut out = 1i128;91    for _ in 1..dimension {92        out = product(out, base as i128 - 1)?;93    }94    product(out, (base - 1 + dimension) as i128)95}9697// MATRIX9899fn coefficient(poly: &[i128], index: i128) -> i128 {100    if index < 0 || index as usize >= poly.len() {101        0102    } else {103        poly[index as usize]104    }105}106107/// The carry matrix over the reachable carries `|c| <= (D-1)/2`, rows indexed by the carry out.108///109/// A level of the design adds one base-`q` digit per coordinate, so the height of the central110/// diagonal hyperplane moves by a digit sum `s` and a carry `c -> (c + mD - s)/q` with `m` the111/// middle digit; the map contracts onto this window from every start.112pub fn carry_matrix(base: usize, dimension: usize) -> Result<Vec<Vec<i128>>> {113    let poly = digit_polynomial(base, dimension)?;114    let shift = (middle(base)? * dimension) as i128;115    let q = base as i128;116    let half = ((dimension - 1) / 2) as i128;117    Ok((-half..=half)118        .map(|out| {119            (-half..=half)120                .map(|inside| coefficient(&poly, inside + shift - q * out))121                .collect()122        })123        .collect())124}125126/// The reflection-even block of the carry matrix, of size `ceil(D/2)`.127///128/// The digit polynomial is palindromic, so the reflection `c -> -c` commutes with the carry map129/// and splits it; the even half carries the central count and its characteristic polynomial is130/// the recurrence the counts obey.131///132/// ```133/// assert_eq!(mrlymath::dim::carry::even_block(3, 3).unwrap(), vec![vec![6, 6], vec![1, 3]]);134/// ```135pub fn even_block(base: usize, dimension: usize) -> Result<Vec<Vec<i128>>> {136    let poly = digit_polynomial(base, dimension)?;137    let shift = (middle(base)? * dimension) as i128;138    let q = base as i128;139    let width = (dimension - 1) / 2 + 1;140    Ok((0..width)141        .map(|out| {142            (0..width)143                .map(|inside| {144                    let step = shift - q * out as i128;145                    let folded = if inside == 0 {146                        0147                    } else {148                        coefficient(&poly, step - inside as i128)149                    };150                    coefficient(&poly, step + inside as i128) + folded151                })152                .collect()153        })154        .collect())155}156157/// The trace of a square integer matrix.158///159/// ```160/// let block = mrlymath::dim::carry::even_block(3, 3).unwrap();161/// assert_eq!(mrlymath::dim::carry::trace(&block), 9);162/// ```163pub fn trace(rows: &[Vec<i128>]) -> i128 {164    (0..rows.len()).map(|index| rows[index][index]).sum()165}166167fn multiply(left: &[Vec<i128>], right: &[Vec<i128>]) -> Result<Vec<Vec<i128>>> {168    let n = left.len();169    let mut out = vec![vec![0i128; n]; n];170    for row in 0..n {171        for step in 0..n {172            let weight = left[row][step];173            if weight == 0 {174                continue;175            }176            for col in 0..n {177                out[row][col] = total(out[row][col], product(weight, right[step][col])?)?;178            }179        }180    }181    Ok(out)182}183184/// The monic characteristic polynomial of a square integer matrix, highest power first.185///186/// Faddeev-LeVerrier in exact integers: every division lands whole, so no fraction is ever needed187/// and the answer is the recurrence's coefficients up to sign.188///189/// ```190/// let block = mrlymath::dim::carry::even_block(3, 3).unwrap();191/// assert_eq!(mrlymath::dim::carry::characteristic(&block).unwrap(), vec![1, -9, 12]);192/// ```193pub fn characteristic(rows: &[Vec<i128>]) -> Result<Vec<i128>> {194    let n = rows.len();195    let mut held: Vec<Vec<i128>> = (0..n)196        .map(|row| (0..n).map(|col| i128::from(row == col)).collect())197        .collect();198    let mut out = vec![1i128];199    for step in 1..=n {200        let walked = multiply(rows, &held)?;201        let sum = trace(&walked);202        if sum % step as i128 != 0 {203            return value_error("the characteristic polynomial left a remainder.");204        }205        let mark = -sum / step as i128;206        out.push(mark);207        held = walked;208        for (index, row) in held.iter_mut().enumerate().take(n) {209            row[index] = total(row[index], mark)?;210        }211    }212    Ok(out)213}214215/// The determinant of a square integer matrix, read off its characteristic polynomial.216///217/// ```218/// let block = mrlymath::dim::carry::even_block(3, 3).unwrap();219/// assert_eq!(mrlymath::dim::carry::determinant(&block).unwrap(), 12);220/// ```221pub fn determinant(rows: &[Vec<i128>]) -> Result<i128> {222    let poly = characteristic(rows)?;223    let last = poly[rows.len()];224    Ok(if rows.len().is_multiple_of(2) {225        last226    } else {227        -last228    })229}230231// LADDER232233/// The counts `a_D(L)` of level-`L` cells meeting the central diagonal hyperplane, from `L = 0`.234///235/// The count is the top-left entry of the `L`-th power of the even block. The walk stops early236/// when the next count would pass a hundred and twenty-eight bits, so the answer runs as far as237/// exact integers reach and no further.238///239/// ```240/// let terms = mrlymath::dim::carry::ladder(3, 3, 6).unwrap();241/// assert_eq!(terms, vec![1, 6, 42, 306, 2250, 16578, 122202]);242/// ```243pub fn ladder(base: usize, dimension: usize, levels: usize) -> Result<Vec<i128>> {244    let block = even_block(base, dimension)?;245    let n = block.len();246    let mut carried = vec![0i128; n];247    carried[0] = 1;248    let mut out = vec![1i128];249    for _ in 0..levels {250        let mut next = vec![0i128; n];251        for row in 0..n {252            let mut sum = 0i128;253            for col in 0..n {254                match block[row][col]255                    .checked_mul(carried[col])256                    .and_then(|part| sum.checked_add(part))257                {258                    Some(value) => sum = value,259                    None => return Ok(out),260                }261            }262            next[row] = sum;263        }264        out.push(next[0]);265        carried = next;266    }267    Ok(out)268}269270// ROOT271272/// The Perron root of a nonnegative square integer matrix.273///274/// The characteristic polynomial is rescaled by the largest row sum, which bounds the root above,275/// then the largest sign change on the unit interval is hunted on a grid and closed by bisection,276/// the same walk the recurrence growth reader takes.277///278/// ```279/// let block = mrlymath::dim::carry::even_block(3, 3).unwrap();280/// let root = mrlymath::dim::carry::perron(&block).unwrap();281/// assert!((root - (9.0 + 33f64.sqrt()) / 2.0).abs() < 1e-9);282/// ```283pub fn perron(rows: &[Vec<i128>]) -> Result<f64> {284    let poly = characteristic(rows)?;285    let bound = rows286        .iter()287        .map(|row| row.iter().sum::<i128>())288        .max()289        .unwrap_or(0) as f64;290    if bound <= 0.0 {291        return value_error("the block has no positive row sum.");292    }293    let mut scaled = Vec::with_capacity(poly.len());294    let mut power = 1.0f64;295    for &term in &poly {296        scaled.push(term as f64 / power);297        power *= bound;298    }299    let value = |y: f64| scaled.iter().fold(0.0f64, |acc, &term| acc * y + term);300    for step in (0..SCAN).rev() {301        let mut lo = step as f64 / SCAN as f64;302        let mut hi = (step + 1) as f64 / SCAN as f64;303        if value(lo) > 0.0 || value(hi) < 0.0 {304            continue;305        }306        for _ in 0..HALVINGS {307            let mid = (lo + hi) / 2.0;308            if value(mid) <= 0.0 {309                lo = mid;310            } else {311                hi = mid;312            }313        }314        return Ok(bound * (lo + hi) / 2.0);315    }316    value_error("the block shows no root inside its row-sum bound.")317}318319// SIGN320321/// The sign of `log_q rho_D - (log_q f_D - 1)`, the slice sign law's reading, in exact integers.322///323/// The characteristic polynomial is evaluated at `f_D/q` with the denominators cleared, so the324/// answer is a comparison of whole numbers and never a rounding. The law reads `(-1)^(D+1)`: the325/// slice exponent stands above the solid's dimension less one at odd `D` and below it at even `D`.326///327/// ```328/// assert_eq!(mrlymath::dim::carry::sign(3, 3).unwrap(), 1);329/// assert_eq!(mrlymath::dim::carry::sign(3, 4).unwrap(), -1);330/// ```331pub fn sign(base: usize, dimension: usize) -> Result<i32> {332    let block = even_block(base, dimension)?;333    let poly = characteristic(&block)?;334    let full = fill(base, dimension)?;335    let q = base as i128;336    let mut acc = 0i128;337    let mut weight = 1i128;338    for &term in &poly {339        acc = total(product(acc, full)?, product(term, weight)?)?;340        weight = product(weight, q)?;341    }342    Ok(match acc {343        acc if acc > 0 => -1,344        acc if acc < 0 => 1,345        _ => 0,346    })347}348349/// The widest dimension the exact carry arithmetic reaches at the base.350///351/// The sign is the heaviest reading: it walks the characteristic polynomial at `f_D/q` with the352/// denominators cleared, so its running value climbs like `f_D^(D/2)` and a hundred and twenty-eight353/// bits run out at dimension fifteen in base three and dimension eleven in base five.354///355/// ```356/// assert_eq!(mrlymath::dim::carry::cap(3).unwrap(), 15);357/// assert_eq!(mrlymath::dim::carry::cap(5).unwrap(), 11);358/// ```359pub fn cap(base: usize) -> Result<usize> {360    middle(base)?;361    let mut top = 0;362    for dimension in 2..=CEILING {363        if sign(base, dimension).is_err() {364            break;365        }366        top = dimension;367    }368    if top < 2 {369        return value_error(format!(370            "base {base} reaches no dimension in exact integers."371        ));372    }373    Ok(top)374}375376// SPECTRUM377378fn ahead(walk: &[Vec<f64>], vector: &[f64]) -> Vec<f64> {379    walk.iter()380        .map(|row| row.iter().zip(vector).map(|(&a, &x)| a * x).sum())381        .collect()382}383384fn behind(walk: &[Vec<f64>], vector: &[f64]) -> Vec<f64> {385    (0..vector.len())386        .map(|col| {387            (0..vector.len())388                .map(|row| walk[row][col] * vector[row])389                .sum()390        })391        .collect()392}393394fn top(vector: &[f64]) -> f64 {395    vector.iter().fold(0.0f64, |peak, &x| peak.max(x.abs()))396}397398fn settle(walk: &[Vec<f64>], left: bool) -> Option<Vec<f64>> {399    let mut vector = vec![1.0f64; walk.len()];400    for _ in 0..SWEEPS {401        let next = if left {402            behind(walk, &vector)403        } else {404            ahead(walk, &vector)405        };406        let peak = top(&next);407        if peak == 0.0 {408            return None;409        }410        vector = next.iter().map(|&x| x / peak).collect();411    }412    Some(vector)413}414415/// The Perron root over the modulus of the second eigenvalue, or none where the block is one wide.416///417/// Power iteration finds the leading pair, Hotelling deflation takes it out and a second walk,418/// reprojected every sweep so rounding cannot bring the leader back, reads the runner up. At base419/// three the ratio falls to `(D + 2)/(D - 2)`, so no fixed spectral gap survives the dimensions.420pub fn spectral_ratio(base: usize, dimension: usize) -> Result<Option<f64>> {421    let block = even_block(base, dimension)?;422    let n = block.len();423    if n < 2 {424        return Ok(None);425    }426    let bound = block427        .iter()428        .map(|row| row.iter().sum::<i128>())429        .max()430        .unwrap_or(1) as f64;431    let walk: Vec<Vec<f64>> = block432        .iter()433        .map(|row| row.iter().map(|&x| x as f64 / bound).collect())434        .collect();435    let (right, left) = match (settle(&walk, false), settle(&walk, true)) {436        (Some(right), Some(left)) => (right, left),437        _ => return Ok(None),438    };439    let lead = top(&ahead(&walk, &right));440    let pair: f64 = left.iter().zip(&right).map(|(&u, &v)| u * v).sum();441    if pair == 0.0 || lead == 0.0 {442        return Ok(None);443    }444    let mut trail: Vec<f64> = (0..n).map(|i| 1.0 + 0.37 * ((i * 7) % 5) as f64).collect();445    let mut second = 0.0f64;446    for _ in 0..SWEEPS {447        let share: f64 = left.iter().zip(&trail).map(|(&u, &x)| u * x).sum::<f64>() / pair;448        let cleared: Vec<f64> = trail449            .iter()450            .zip(&right)451            .map(|(&x, &v)| x - share * v)452            .collect();453        let next = ahead(&walk, &cleared);454        second = top(&next);455        if second == 0.0 {456            return Ok(None);457        }458        trail = next.iter().map(|&x| x / second).collect();459    }460    Ok(Some(lead / second))461}462463#[cfg(test)]464mod tests {465    use super::*;466467    fn brute(base: usize, dimension: usize) -> Vec<i128> {468        let centre = (base - 1) / 2;469        let mut out = vec![0i128; (base - 1) * dimension + 1];470        for mut code in 0..base.pow(dimension as u32) {471            let mut sum = 0;472            let mut middles = 0;473            for _ in 0..dimension {474                let digit = code % base;475                code /= base;476                sum += digit;477                middles += usize::from(digit == centre);478            }479            if middles <= 1 {480                out[sum] += 1;481            }482        }483        out484    }485486    fn central(base: usize, dimension: usize, levels: usize) -> Vec<i128> {487        let poly = digit_polynomial(base, dimension).unwrap();488        let mut out = vec![1i128];489        let mut span = vec![1i128];490        let mut step = 1usize;491        for _ in 0..levels {492            let mut next = vec![0i128; span.len() + (base - 1) * dimension * step];493            for (at, &count) in span.iter().enumerate() {494                if count == 0 {495                    continue;496                }497                for (weight, &many) in poly.iter().enumerate() {498                    next[at + weight * step] += count * many;499                }500            }501            span = next;502            step *= base;503            out.push(span[dimension * (step - 1) / 2]);504        }505        out506    }507508    #[test]509    fn the_digit_polynomial_is_the_enumeration_of_the_kept_cells() {510        for base in [3usize, 5] {511            for dimension in 2..=6 {512                let got = digit_polynomial(base, dimension).unwrap();513                assert_eq!(514                    got,515                    brute(base, dimension),516                    "base {base} dimension {dimension}"517                );518                assert_eq!(519                    got.iter().sum::<i128>(),520                    fill(base, dimension).unwrap(),521                    "base {base} dimension {dimension}"522                );523            }524        }525        assert!(digit_polynomial(4, 3).is_err());526        assert!(digit_polynomial(3, 1).is_err());527    }528529    #[test]530    fn the_three_dimensional_block_is_the_published_anchor() {531        let block = even_block(3, 3).unwrap();532        assert_eq!(block, vec![vec![6, 6], vec![1, 3]]);533        assert_eq!(trace(&block), 9);534        assert_eq!(determinant(&block).unwrap(), 12);535        assert_eq!(characteristic(&block).unwrap(), vec![1, -9, 12]);536        assert_eq!(537            ladder(3, 3, 6).unwrap(),538            vec![1, 6, 42, 306, 2250, 16578, 122202]539        );540        let root = perron(&block).unwrap();541        assert!((root - (9.0 + 33f64.sqrt()) / 2.0).abs() < 1e-9);542        assert!((root.log(3.0) - 1.818_410).abs() < 1e-6);543        assert!((20f64.log(3.0) - 1.0 - 1.726_833).abs() < 1e-6);544    }545546    #[test]547    fn the_traces_read_the_two_closed_forms() {548        let want = [2i128, 9, 11, 60, 47, 336];549        for (index, &term) in want.iter().enumerate() {550            let dimension = index + 2;551            assert_eq!(trace(&even_block(3, dimension).unwrap()), term);552        }553        for dimension in 2..=cap(3).unwrap() {554            let got = trace(&even_block(3, dimension).unwrap());555            let want = if dimension % 2 == 0 {556                3 * 2i128.pow(dimension as u32 - 2) - 1557            } else {558                3 * dimension as i128 * 2i128.pow(dimension as u32 - 3)559            };560            assert_eq!(got, want, "dimension {dimension}");561        }562    }563564    #[test]565    fn the_three_generators_of_the_ladder_agree() {566        for base in [3usize, 5] {567            for dimension in 2..=6 {568                let block = ladder(base, dimension, 4).unwrap();569                assert_eq!(570                    block,571                    central(base, dimension, 4),572                    "base {base} dimension {dimension}"573                );574                let full = carry_matrix(base, dimension).unwrap();575                let half = full.len() / 2;576                let mut walked: Vec<Vec<i128>> = (0..full.len())577                    .map(|row| (0..full.len()).map(|col| i128::from(row == col)).collect())578                    .collect();579                let mut got = vec![1i128];580                for _ in 0..4 {581                    walked = multiply(&full, &walked).unwrap();582                    got.push(walked[half][half]);583                }584                assert_eq!(got, block, "base {base} dimension {dimension}");585            }586        }587        assert_eq!(588            ladder(3, 4, 6).unwrap(),589            vec![1, 6, 132, 1848, 29040, 441408, 6772128]590        );591        assert_eq!(ladder(3, 5, 4).unwrap(), vec![1, 30, 1000, 35700, 1321600]);592        assert_eq!(593            ladder(3, 6, 4).unwrap(),594            vec![1, 20, 4030, 242300, 24642700]595        );596        assert_eq!(ladder(5, 3, 4).unwrap(), vec![1, 18, 414, 9702, 227646]);597    }598599    #[test]600    fn the_ladder_stops_where_the_exact_integers_do() {601        let terms = ladder(3, 14, 40).unwrap();602        assert_eq!(&terms[..3], &[1i128, 3432, 922_926_862]);603        assert_eq!(terms.len(), 9);604        assert_eq!(ladder(3, 10, 40).unwrap().len(), 12);605        assert_eq!(606            ladder(3, 2, 8).unwrap(),607            vec![1, 2, 4, 8, 16, 32, 64, 128, 256]608        );609    }610611    #[test]612    fn the_sign_alternates_to_the_cap_at_both_bases() {613        for base in [3usize, 5] {614            let top = cap(base).unwrap();615            for dimension in 2..=top {616                let got = sign(base, dimension).unwrap();617                let want = if dimension % 2 == 0 { -1 } else { 1 };618                assert_eq!(got, want, "base {base} dimension {dimension}");619                let block = even_block(base, dimension).unwrap();620                let root = perron(&block).unwrap();621                let edge = fill(base, dimension).unwrap() as f64 / base as f64;622                assert_eq!(623                    got,624                    if root > edge { 1 } else { -1 },625                    "base {base} dimension {dimension}"626                );627            }628            assert!(sign(base, top + 1).is_err());629        }630        assert_eq!(cap(3).unwrap(), 15);631        assert_eq!(cap(5).unwrap(), 11);632    }633634    #[test]635    fn the_spectral_ratio_falls_to_the_free_bound() {636        assert_eq!(spectral_ratio(3, 2).unwrap(), None);637        let mut last = f64::INFINITY;638        for dimension in [6usize, 10, 20, 30, 50] {639            let got = spectral_ratio(3, dimension).unwrap().unwrap();640            let free = (dimension as f64 + 2.0) / (dimension as f64 - 2.0);641            assert!(got > 1.0 && got < last, "dimension {dimension} ratio {got}");642            assert!(643                (got - free).abs() < 0.05,644                "dimension {dimension} ratio {got}"645            );646            last = got;647        }648        let fifty = spectral_ratio(3, 50).unwrap().unwrap();649        assert!((fifty - 13.0 / 12.0).abs() < 1e-9, "ratio {fifty}");650    }651}