fills.rs

11.3 kB · rust · 339 lines

1use crate::quasi::{degree, fit, fraction, leading, text};2use crate::tables::write_csv;3use mrlymath::bang::baseq::{4    canonical, distinct_designs, group, group_order, orbit, representatives, WALK_LIMIT,5};6use mrlymath::bang::counting;7use mrlymath::bang::factory::{corners_to_code, levels_code, residue_corners};8use mrlymath::bang::universe;9use mrlymath::bang::Code;10use mrlymath::formulas::{fill, void};11use mrlymath::rules::render;12use std::collections::{BTreeMap, BTreeSet};13use std::path::Path;1415const SIDES: usize = 12;16const CASES: [(usize, usize); 4] = [(2, 2), (2, 3), (3, 2), (3, 3)];17const SHOWN: usize = 6;1819pub fn cell_index(cell: &[u8], base: usize) -> usize {20    cell.iter().fold(0, |acc, &r| acc * base + r as usize)21}2223fn named(base: usize, dimension: usize) -> Vec<(&'static str, Code)> {24    let cells = residue_corners(dimension, base);25    let select = |keep: &dyn Fn(&[u8]) -> bool| {26        let filled: Vec<Vec<u8>> = cells.iter().filter(|cell| keep(cell)).cloned().collect();27        corners_to_code(&filled, dimension, base)28    };29    let free = if dimension == 2 { 0 } else { dimension - 1 };30    vec![31        ("carpet", levels_code(dimension, base, &[0, 1])),32        (33            "net",34            select(&|cell| cell.iter().map(|&r| r as usize).sum::<usize>() + 1 >= dimension),35        ),36        ("void", select(&|cell| cell.iter().all(|&r| r == cell[0]))),37        (38            "tree",39            select(&|cell| (0..dimension).filter(|&a| a != free).all(|a| cell[a] == 0)),40        ),41    ]42}4344fn burnside(base: usize, dimension: usize) -> u128 {45    distinct_designs(base, dimension).expect("the Burnside average is an integer")46}4748pub fn orbits_report() {49    println!("base 2 cube group: order 2^D D!, designs up to symmetry three ways");50    for dimension in 1..=4 {51        let walk = representatives(2, dimension)52            .expect("the walk stays under the code limit")53            .len();54        let order = group_order(2, dimension);55        let burnside = burnside(2, dimension);56        let classes = counting::distinct_designs(dimension).expect("the class sums close");57        println!(58            "D {dimension}: order {order} orbit walk {walk} Burnside {burnside} class sum {classes}"59        );60    }61}6263pub struct Row {64    pub base: usize,65    pub dimension: usize,66    pub code: Code,67    pub label: String,68    pub orbit: usize,69    pub popcount: u32,70    pub gf2: Option<i32>,71    pub gfq: i64,72    pub mobius: i64,73    pub poly: String,74    pub degree: String,75    pub lead: String,76    pub fills: Vec<u128>,77    pub voids: Vec<u128>,78}7980fn rendered(code: Code, number: usize, dimension: usize, base: usize) -> u128 {81    let tile = render(82        |r| code >> cell_index(r, base) & 1 == 1,83        number,84        dimension,85        base,86    )87    .expect("the tile renders");88    u128::from(tile.sum())89}9091fn live_degree(cells: &[Vec<u8>], live: impl Fn(usize) -> bool) -> i64 {92    cells93        .iter()94        .enumerate()95        .filter(|(index, _)| live(*index))96        .map(|(_, cell)| cell.iter().map(|&r| i64::from(r)).sum::<i64>())97        .max()98        .unwrap_or(-1)99}100101fn mobius_degree(code: Code, cells: &[Vec<u8>], dimension: usize, base: usize) -> i64 {102    let mut coeff: Vec<i64> = (0..cells.len()).map(|i| (code >> i & 1) as i64).collect();103    for axis in 0..dimension {104        for value in (1..base as u8).rev() {105            for (index, cell) in cells.iter().enumerate() {106                if cell[axis] == value {107                    let mut lower = cell.clone();108                    lower[axis] -= 1;109                    coeff[index] -= coeff[cell_index(&lower, base)];110                }111            }112        }113    }114    live_degree(cells, |index| coeff[index] != 0)115}116117fn inverse_modular(value: i64, modulus: i64) -> i64 {118    (1..modulus)119        .find(|candidate| candidate * value.rem_euclid(modulus) % modulus == 1)120        .expect("the pivot is invertible in a prime field")121}122123fn inverse_vandermonde(q: usize) -> Vec<Vec<i64>> {124    let modulus = q as i64;125    let mut rows: Vec<Vec<i64>> = (0..q)126        .map(|i| {127            let mut row: Vec<i64> = (0..q)128                .map(|j| (i as i64).pow(j as u32).rem_euclid(modulus))129                .collect();130            row.extend((0..q).map(|c| i64::from(c == i)));131            row132        })133        .collect();134    for column in 0..q {135        let pivot = (column..q)136            .find(|&r| rows[r][column] != 0)137            .expect("the Vandermonde matrix is nonsingular over a prime field");138        rows.swap(column, pivot);139        let scale = inverse_modular(rows[column][column], modulus);140        for value in rows[column].iter_mut() {141            *value = *value * scale % modulus;142        }143        for r in 0..q {144            if r != column && rows[r][column] != 0 {145                let factor = rows[r][column];146                let pivot_row = rows[column].clone();147                for (value, above) in rows[r].iter_mut().zip(&pivot_row) {148                    *value = (*value - factor * above).rem_euclid(modulus);149                }150            }151        }152    }153    rows.into_iter().map(|row| row[q..].to_vec()).collect()154}155156fn gfq_degree(code: Code, cells: &[Vec<u8>], dimension: usize, q: usize) -> i64 {157    let inverse = inverse_vandermonde(q);158    let mut coeff: Vec<i64> = (0..cells.len()).map(|i| (code >> i & 1) as i64).collect();159    for axis in 0..dimension {160        let mut next = vec![0i64; cells.len()];161        for cell in cells.iter().filter(|cell| cell[axis] == 0) {162            let line: Vec<i64> = (0..q)163                .map(|t| {164                    let mut key = cell.clone();165                    key[axis] = t as u8;166                    coeff[cell_index(&key, q)]167                })168                .collect();169            for (exponent, row) in inverse.iter().enumerate() {170                let acc: i64 = row.iter().zip(&line).map(|(a, b)| a * b).sum();171                let mut key = cell.clone();172                key[axis] = exponent as u8;173                next[cell_index(&key, q)] = acc.rem_euclid(q as i64);174            }175        }176        coeff = next;177    }178    live_degree(cells, |index| coeff[index] != 0)179}180181fn row(base: usize, dimension: usize, code: Code, orbit: usize, label: String) -> Row {182    let cells = residue_corners(dimension, base);183    let count =184        |n: usize, level: u32| fill(code, n, dimension, level, base).expect("the fill counts");185    let fills: Vec<u128> = (1..=SIDES).map(|n| count(n, 1)).collect();186    let voids: Vec<u128> = (1..=SIDES)187        .map(|n| void(code, n, dimension, 1, base).expect("the void counts"))188        .collect();189    for n in 1..=SIDES {190        assert!(191            rendered(code, n, dimension, base) == fills[n - 1],192            "the closed form matches the rendered tile"193        );194    }195    for n in [base, base + 1, 2 * base] {196        for level in [2u32, 3] {197            assert!(198                count(n, level) == fills[n - 1].pow(level),199                "the level law holds"200            );201        }202    }203    let (polys, collapses) = fit(code, dimension, base);204    let top = polys.iter().filter_map(|poly| degree(poly)).max();205    let poly = if collapses {206        text(&polys[0])207    } else {208        (0..base)209            .map(|class| format!("n\u{2261}{class}(mod {base}): {}", text(&polys[class])))210            .collect::<Vec<String>>()211            .join(" ; ")212    };213    Row {214        base,215        dimension,216        code,217        label,218        orbit,219        popcount: code.count_ones(),220        gf2: (base == 2).then(|| universe::degree(code, dimension)),221        gfq: gfq_degree(code, &cells, dimension, base),222        mobius: mobius_degree(code, &cells, dimension, base),223        poly,224        degree: top.map_or(String::from("-oo"), |d| d.to_string()),225        lead: fraction(&leading(&polys[0])),226        fills,227        voids,228    }229}230231fn case(base: usize, dimension: usize) -> (Vec<Row>, bool) {232    let group = group(base, dimension);233    let full = base.pow(dimension as u32) <= WALK_LIMIT;234    if full {235        let labels: BTreeMap<Code, &str> = named(base, dimension)236            .into_iter()237            .map(|(name, code)| (canonical(&group, code), name))238            .collect();239        let rows = representatives(base, dimension)240            .expect("the walk stays under the code limit")241            .into_iter()242            .map(|(code, size)| {243                let label = labels.get(&code).copied().unwrap_or_default().to_string();244                row(base, dimension, code, size, label)245            })246            .collect();247        (rows, true)248    } else {249        let rows = named(base, dimension)250            .into_iter()251            .map(|(name, code)| {252                let representative = canonical(&group, code);253                let size = orbit(&group, representative).len();254                row(base, dimension, representative, size, name.to_string())255            })256            .collect();257        (rows, false)258    }259}260261fn header() -> Vec<String> {262    let mut out: Vec<String> = [263        "base",264        "dim",265        "rep_code",266        "label",267        "orbit_size",268        "popcount",269        "gf2_degree",270        "gfq_degree",271        "intmobius_degree",272        "fill_poly",273        "fill_deg",274        "fill_lead",275    ]276    .iter()277    .map(|name| (*name).to_string())278    .collect();279    out.extend((1..=SIDES).map(|n| format!("fill_n{n}")));280    out.extend((1..=SIDES).map(|n| format!("void_n{n}")));281    out282}283284fn record(row: &Row) -> Vec<String> {285    let mut out = vec![286        row.base.to_string(),287        row.dimension.to_string(),288        row.code.to_string(),289        row.label.clone(),290        row.orbit.to_string(),291        row.popcount.to_string(),292        row.gf2.map(|d| d.to_string()).unwrap_or_default(),293        row.gfq.to_string(),294        row.mobius.to_string(),295        row.poly.clone(),296        row.degree.clone(),297        row.lead.clone(),298    ];299    out.extend(row.fills.iter().map(u128::to_string));300    out.extend(row.voids.iter().map(u128::to_string));301    out302}303304pub fn report(path: &Path) {305    println!("fill census: one row per cube-group orbit, sides 1..{SIDES}, every row rendered cell by cell");306    let mut rows: Vec<Row> = Vec::new();307    for (base, dimension) in CASES {308        let (case_rows, full) = case(base, dimension);309        let expected = burnside(base, dimension);310        let scope = if full { "full walk" } else { "named only" };311        let polys: BTreeSet<&str> = case_rows.iter().map(|row| row.poly.as_str()).collect();312        println!(313            "base {base} D {dimension}: {} designs, Burnside {expected}, {} distinct fill polynomials, {scope}",314            case_rows.len(),315            polys.len()316        );317        for row in case_rows.iter().filter(|row| !row.label.is_empty()) {318            println!(319                "  {:<6} code {:>9} orbit {:>3} popcount {:>2} degree {} lead {:<5} fill n1..n{SHOWN} {:?}",320                row.label,321                row.code,322                row.orbit,323                row.popcount,324                row.degree,325                row.lead,326                &row.fills[..SHOWN]327            );328        }329        rows.extend(case_rows);330    }331    let labels = rows.iter().filter(|row| !row.label.is_empty()).count();332    println!(333        "{} designs, {} columns, {labels} labelled rows, written sequences.csv",334        rows.len(),335        header().len()336    );337    let records: Vec<Vec<String>> = rows.iter().map(record).collect();338    write_csv(path, &header(), &records);339}