star.rs

23.2 kB · rust · 620 lines

1use crate::lattice::{Family, Rule, FAMILIES};2use crate::sums::{mean, odds};3use mrlymath::six::{geometry::cut, FILL, GRID};4use mrlynum::series::{beta, dirichlet, CATALAN, EULER};5use num_rational::Ratio;67pub struct Slice {8    pub rows: usize,9    pub cols: usize,10    pub types: Vec<u8>,11}1213pub fn slice(family: Family, number: usize) -> Slice {14    let hex = cut(&family.cube(number)).expect("a cut cube");15    let (rows, cols) = (hex.height(), hex.width());16    let types = (0..rows * cols)17        .map(|at| hex.cell.types().at(at) as u8)18        .collect();19    Slice { rows, cols, types }20}2122impl Slice {23    pub fn ink(&self) -> Ratio<i64> {24        let filled = self.types.iter().filter(|cell| **cell == FILL).count();25        let inside = self.types.iter().filter(|cell| **cell != GRID).count();26        Ratio::new(filled as i64, inside as i64)27    }2829    fn coordinates(&self, number: usize) -> Vec<[f64; 3]> {30        let (n, cols) = (number as i64, self.cols as i64);31        let mut out = vec![[0.0; 3]; self.rows * self.cols];32        for row in 0..self.rows as i64 {33            let z = 2 * row;34            let target = 6 * n - 2 - z;35            let low = 0.max(target - (4 * n - 1));36            let reach = (4 * n - 1).min(target) - low + 1;37            let offset = (cols - reach) / 2;38            for step in 0..reach {39                let x = low + step;40                let scale = 4.0 * n as f64;41                out[(row * cols + offset + step) as usize] = [42                    x as f64 / scale,43                    (target - x) as f64 / scale,44                    z as f64 / scale,45                ];46            }47        }48        out49    }50}5152pub fn law(family: Family, number: usize) -> Ratio<i64> {53    let n = number as i64;54    let chi = if (3 * n - 1) / 2 % 2 == 0 { 1 } else { -1 };55    let r = |p: i64, q: i64| Ratio::new(p, q);56    let carpet = r(1, 2) + r(chi, 8) + r(1, 2 * n) - r(chi, 8 * n * n);57    match family {58        Family::Carpet => carpet,59        Family::Net => r(1, 1) - carpet,60        Family::Tree => r(1, 4) + (r(1, 3) - r(chi, 12)) / r(n, 1) + r(1 - chi, 6 * n * n),61        Family::Void => r(1, 4) - r(chi, 4 * n) + r(1, 2 * n * n),62    }63}6465pub fn ink_laws(limit: usize) {66    println!("cut ink of every odd n <= {limit} against the closed forms, exact rationals");67    for family in FAMILIES {68        let mut matched = 0;69        let mut centre = 0;70        let mut layers = 0;71        for number in odds(limit) {72            let hex = slice(family, number);73            matched += usize::from(hex.ink() == law(family, number));74            centre += usize::from(hex.types[number * hex.cols + 2 * number - 1] == FILL);75            layers += 1;76        }77        println!(78            "  {}: {matched}/{layers} layers match, centre cell ink in {centre}/{layers}",79            family.name()80        );81    }82}8384const HEIGHT: usize = 1200;85const WIDTH: usize = 2399;86const DEEPEST: usize = 111;8788fn sample(count: usize, extent: usize) -> Vec<usize> {89    (0..count)90        .map(|slot| ((slot as f64 + 0.5) / count as f64 * extent as f64).floor() as usize)91        .collect()92}9394fn raster<T: Copy>(hex: &Slice, cells: &[T]) -> Vec<T> {95    let lines = sample(HEIGHT, hex.rows);96    let slots = sample(WIDTH, hex.cols);97    let mut out = Vec::with_capacity(HEIGHT * WIDTH);98    for line in &lines {99        for slot in &slots {100            out.push(cells[line * hex.cols + slot]);101        }102    }103    out104}105106fn masked(values: &[f64], keep: &[bool]) -> f64 {107    let kept: Vec<f64> = values108        .iter()109        .zip(keep)110        .filter(|(_, inside)| **inside)111        .map(|(value, _)| *value)112        .collect();113    mean(&kept)114}115116pub fn ghost() {117    println!("carpet cut layers rendered on a {HEIGHT} by {WIDTH} raster, odd n <= {DEEPEST}");118    let layers: Vec<(Vec<bool>, Vec<bool>)> = odds(DEEPEST)119        .map(|number| {120            let hex = slice(Family::Carpet, number);121            let fill = raster(122                &hex,123                &hex.types.iter().map(|c| *c == FILL).collect::<Vec<bool>>(),124            );125            let inside = raster(126                &hex,127                &hex.types.iter().map(|c| *c != GRID).collect::<Vec<bool>>(),128            );129            (fill, inside)130        })131        .collect();132    let mut hexagon = vec![true; HEIGHT * WIDTH];133    for (_, inside) in &layers {134        for (keep, ok) in hexagon.iter_mut().zip(inside) {135            *keep &= ok;136        }137    }138    let deepest = slice(Family::Carpet, DEEPEST);139    let gaps = raster(&deepest, &deepest.coordinates(DEEPEST));140    let near = |cell: usize, width: f64| {141        let [x, y, z] = gaps[cell];142        (x - y).abs() < width || (y - z).abs() < width || (z - x).abs() < width143    };144    let star: Vec<bool> = (0..HEIGHT * WIDTH)145        .map(|cell| hexagon[cell] && near(cell, 0.004))146        .collect();147    let background: Vec<bool> = (0..HEIGHT * WIDTH)148        .map(|cell| hexagon[cell] && !near(cell, 0.02))149        .collect();150    println!(151        "  pixels: hexagon {}  star {}  background {}",152        hexagon.iter().filter(|k| **k).count(),153        star.iter().filter(|k| **k).count(),154        background.iter().filter(|k| **k).count()155    );156    let mut total = vec![0.0f64; HEIGHT * WIDTH];157    for (count, (fill, _)) in layers.iter().enumerate() {158        for (slot, value) in total.iter_mut().zip(fill) {159            *slot += f64::from(*value);160        }161        let stacked = count + 1;162        if [5, 28, 56].contains(&stacked) {163            let field: Vec<f64> = total.iter().map(|value| value / stacked as f64).collect();164            let (star_ink, background_ink) = (masked(&field, &star), masked(&field, &background));165            println!(166                "  layers {stacked:2}: star {star_ink:.5}  background {background_ink:.5}  star minus background {:+.5}",167                star_ink - background_ink168            );169        }170    }171}172173pub fn fading_lattice(rule: &Rule) {174    println!("carpet lattice frame, band |x-y| <= 0.01 of the cube side, per-layer excess over the exact ink law");175    let counts = [14usize, 28, 56, 100, 200, 400];176    let mut excesses = Vec::new();177    for number in odds(2 * counts[counts.len() - 1] - 1) {178        let n = number as i64;179        let size = 4 * n;180        let step = 1.0 / size as f64;181        let reach = 2.max((0.01 * size as f64).ceil() as i64 + 2);182        let (mut total, mut inked) = (0.0f64, 0.0f64);183        for index in 0..2 * n {184            let z = 2 * index;185            let target = 6 * n - 2 - z;186            for offset in -reach..=reach {187                let x = target.div_euclid(2) + offset;188                let y = target - x;189                if !(0..size).contains(&x) || !(0..size).contains(&y) {190                    continue;191                }192                let d = (x - y) as f64 * step;193                let weight = ((d + step).min(0.01) - (d - step).max(-0.01)).max(0.0);194                total += weight;195                inked += weight * f64::from(rule.filled(x, y, z));196            }197        }198        let background = law(Family::Carpet, number);199        let background = *background.numer() as f64 / *background.denom() as f64;200        excesses.push(if total > 0.0 {201            inked / total - background202        } else {203            0.0204        });205    }206    let mut scaled = Vec::new();207    for count in counts {208        let excess = mean(&excesses[..count]);209        println!(210            "  L = {count:3}: excess {excess:+.6}  excess * L {:+.4}",211            excess * count as f64212        );213        scaled.push(excess * count as f64);214    }215    println!(216        "  slope of excess * L against ln L: {:+.4} (100 to 200)  {:+.4} (200 to 400)",217        (scaled[4] - scaled[3]) / 2f64.ln(),218        (scaled[5] - scaled[4]) / 2f64.ln()219    );220}221222fn law_value(number: usize) -> f64 {223    let value = law(Family::Carpet, number);224    *value.numer() as f64 / *value.denom() as f64225}226227fn arm_ink(rule: &Rule, n: i64, half: i64) -> f64 {228    let size = 4 * n;229    let (mut total, mut inked) = (0i64, 0i64);230    for index in 0..2 * n {231        let z = 2 * index;232        let target = 6 * n - 2 - z;233        let base = target / 2;234        for x in base - half - 1..=base + half + 1 {235            let y = target - x;236            if !(0..size).contains(&x) || !(0..size).contains(&y) || (x - y).abs() > half {237                continue;238            }239            total += 1;240            inked += i64::from(rule.filled(x, y, z));241        }242    }243    inked as f64 / total as f64244}245246fn arm_count(rule: &Rule, n: i64, axis: usize) -> Ratio<i64> {247    let size = 4 * n;248    let (mut total, mut inked) = (0i64, 0i64);249    for step in 0..size {250        let (x, y, z) = match axis {251            0 => (step, step, 6 * n - 2 - 2 * step),252            1 => (6 * n - 2 - 2 * step, step, step),253            _ => (step, 6 * n - 2 - 2 * step, step),254        };255        if !(0..size).contains(&x)256            || !(0..size).contains(&y)257            || !(0..size).contains(&z)258            || z % 2 != 0259        {260            continue;261        }262        total += 1;263        inked += i64::from(rule.filled(x, y, z));264    }265    Ratio::new(inked, total)266}267268fn arm_law(n: i64) -> Ratio<i64> {269    let rhythm = [0, 1, 0, -1, 0, -1, 0, 1][(n % 8) as usize];270    Ratio::new(1, 2) + Ratio::new(rhythm, 2 * n)271}272273fn nearest(value: f64) -> (i64, i64) {274    let mut best = (0i64, 1i64, f64::INFINITY);275    for denominator in 1..=400i64 {276        let numerator = (value * denominator as f64).round() as i64;277        let gap = (value - numerator as f64 / denominator as f64).abs();278        if gap < best.2 - 1e-12 {279            best = (numerator, denominator, gap);280        }281    }282    (best.0, best.1)283}284285const ARM_LIMIT: usize = 6400;286const LAW_LIMIT: usize = 2001;287const ARMS_LIMIT: usize = 221;288const WIDTH_LIMIT: usize = 800;289const HELD_LIMIT: usize = 1600;290const CHECK_LIMIT: usize = 3200;291292pub fn cell_frame(rule: &Rule) {293    println!(294        "carpet cell frame: star the exact arm x = y of the cut, background the exact ink law"295    );296    let (mut matched, mut layers) = (0, 0);297    for number in odds(LAW_LIMIT) {298        matched += usize::from(arm_count(rule, number as i64, 0) == arm_law(number as i64));299        layers += 1;300    }301    let (mut agreed, mut triples) = (0, 0);302    for number in odds(ARMS_LIMIT) {303        let n = number as i64;304        let inks = [305            arm_count(rule, n, 0),306            arm_count(rule, n, 1),307            arm_count(rule, n, 2),308        ];309        agreed += usize::from(inks[0] == inks[1] && inks[1] == inks[2]);310        triples += 1;311    }312    println!(313        "  arm ink 1/2 + chi_8(n)/(2n), chi_8 the mod-8 character of Q(sqrt 2), exact rationals: {matched}/{layers} layers match at odd n <= {LAW_LIMIT}, three arms agree in {agreed}/{triples} at odd n <= {ARMS_LIMIT}"314    );315    let excesses: Vec<f64> = odds(2 * ARM_LIMIT - 1)316        .map(|number| arm_ink(rule, number as i64, 0) - law_value(number))317        .collect();318    let constant = (1.0 + 2f64.sqrt()).ln() / (2.0 * 2f64.sqrt())319        - CATALAN / 8.0320        - EULER / 4.0321        - 2f64.ln() / 2.0;322    let mut scaled = Vec::new();323    let mut residuals = Vec::new();324    println!(325        "  L = 0 mod 4: the -chi/8 term sums to 0, residual is excess * L + (ln L)/4 - C, against -23/192 = {:+.8}",326        -23.0 / 192.0327    );328    for count in [28usize, 56, 100, 200, 400, 800, 1600, 3200, 6400] {329        let excess = mean(&excesses[..count]);330        let scale = excess * count as f64;331        let logged = scale + (count as f64).ln() / 4.0;332        println!(333            "  L = {count:4}: excess {excess:+.8}  excess * L {scale:+.6}  excess * L + (ln L)/4 {logged:+.10}  residual * L^2 {:+.8}",334            (logged - constant) * (count as f64) * (count as f64)335        );336        scaled.push(scale);337        residuals.push(logged - constant);338    }339    print!("  residual ratio per doubling from L = 100:");340    for step in 2..residuals.len() - 1 {341        print!(" {:.2}", residuals[step] / residuals[step + 1]);342    }343    println!();344    println!(345        "  L = 2 mod 4: the -chi/8 term still sums to 0 and the character tail changes sign, against +25/192 = {:+.8}",346        25.0 / 192.0347    );348    for count in [102usize, 202, 402, 802, 1602, 3202] {349        let excess = mean(&excesses[..count]);350        let logged = excess * count as f64 + (count as f64).ln() / 4.0;351        println!(352            "  L = {count:4}: excess * L + (ln L)/4 {logged:+.10}  residual * L^2 {:+.8}",353            (logged - constant) * (count as f64) * (count as f64)354        );355    }356    println!(357        "  L odd: the -chi/8 term sums to +1/8, so the constant is C + 1/8 and the error is O(1/L)"358    );359    for count in [27usize, 99, 401, 1601, 6399] {360        let excess = mean(&excesses[..count]);361        let logged = excess * count as f64 + (count as f64).ln() / 4.0;362        println!(363            "  L = {count:4}: excess * L + (ln L)/4 - C {:+.6}  less 1/8 {:+.8}  times L {:+.6}",364            logged - constant,365            logged - constant - 0.125,366            (logged - constant - 0.125) * count as f64367        );368    }369    println!(370        "  slope of excess * L against ln L: {:+.6} (1600 to 3200)  {:+.6} (3200 to 6400)",371        (scaled[7] - scaled[6]) / 2f64.ln(),372        (scaled[8] - scaled[7]) / 2f64.ln()373    );374    println!(375        "  constant ln(1+sqrt2)/(2 sqrt2) - G/8 - gamma/4 - (ln2)/2 = {constant:.10}  G from its own series {:.10}  L(1, chi_8) series {:.8} against ln(1+sqrt2)/sqrt2 {:.8}",376        beta(2.0, 2_000_000),377        dirichlet(1.0, &[0, 1, 0, -1, 0, -1, 0, 1], 20_000_000),378        (1.0 + 2f64.sqrt()).ln() / 2f64.sqrt()379    );380}381382// WIDTH FAMILY383384struct Width {385    k: i64,386    b: i64,387    kappa: Ratio<i64>,388    m: Ratio<i64>,389    q: Ratio<i64>,390}391392fn edge_run(k: i64) -> i64 {393    let hits = (-k..=k)394        .filter(|j| matches!(j.rem_euclid(8), 3 | 4 | 5))395        .count() as i64;396    hits + (k + 2).div_euclid(4) - k397}398399fn width_terms(half: i64) -> Width {400    let k = half.div_euclid(2);401    let b = i64::from(k.div_euclid(2) % 2 == 0);402    let run = edge_run(k);403    Width {404        k,405        b,406        kappa: Ratio::new(if k % 2 == 0 { -1 } else { 1 }, 8 * (2 * k + 1)),407        m: Ratio::new(-(k + b), 2 * (2 * k + 1)),408        q: Ratio::new(1 - 2 * run, 2 * (2 * k + 1)),409    }410}411412fn band_count(rule: &Rule, n: i64, half: i64) -> Ratio<i64> {413    let size = 4 * n;414    let (mut total, mut inked) = (0i64, 0i64);415    for index in 0..2 * n {416        let z = 2 * index;417        let target = 6 * n - 2 - z;418        let base = target / 2;419        for x in base - half - 1..=base + half + 1 {420            let y = target - x;421            if !(0..size).contains(&x) || !(0..size).contains(&y) || (x - y).abs() > half {422                continue;423            }424            total += 1;425            inked += i64::from(rule.filled(x, y, z));426        }427    }428    Ratio::new(inked, total)429}430431fn width_excess_law(half: i64, n: i64) -> Ratio<i64> {432    let w = width_terms(half);433    let chi = if (3 * n - 1) / 2 % 2 == 0 { 1 } else { -1 };434    let eight = [0i64, 1, 0, -1, 0, -1, 0, 1][(n % 8) as usize];435    w.kappa * chi + (w.m + w.q * eight) / n + Ratio::new(chi, 8 * n * n)436}437438fn flat(value: Ratio<i64>) -> f64 {439    *value.numer() as f64 / *value.denom() as f64440}441442const IDENTITY_LIMIT: usize = 201;443const LADDER_LIMIT: usize = 1602;444445pub fn cell_width_law(rule: &Rule) {446    println!("carpet cell frame, one closed form for every band half-width: excess_W(n) = kappa chi + (m + q chi_8(n))/n + chi/(8 n^2), exact rationals against the counted band");447    println!("  K = W/2 since x - y is even on the cut, b = 1 when floor(K/2) is even, and E(K) = #(|j| <= K, j = 3,4,5 mod 8) + floor((K+2)/4) - K is the tail's chi_8 weight, derived and not fitted");448    let periodic = (0..=200i64)449        .filter(|k| edge_run(*k) == [0i64, -1, -1, 0, 1, 2, 2, 1][(k % 8) as usize])450        .count();451    println!("  E(K) against the 8-periodic run 0 -1 -1 0 1 2 2 1, which a block of eight leaves alone since 6 + 2 - 8 = 0: {periodic}/201 at K = 0..200");452    println!("  the match is broken out by n mod 8 so no residue class hides; odd W repeats its even neighbour, so only the first W at each K is counted in the total");453    let (mut seen, mut kept, mut total) = (Vec::new(), 0usize, 0usize);454    for half in [455        0i64, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 20, 24, 32, 36, 64,456    ] {457        let w = width_terms(half);458        let fresh = !seen.contains(&w.k);459        if fresh {460            seen.push(w.k);461        }462        let (mut hit, mut all) = ([0usize; 4], [0usize; 4]);463        for number in odds(IDENTITY_LIMIT) {464            let n = number as i64;465            if n < w.k {466                continue;467            }468            let slot = ((n % 8) / 2) as usize;469            all[slot] += 1;470            hit[slot] += usize::from(471                band_count(rule, n, half) - law(Family::Carpet, number)472                    == width_excess_law(half, n),473            );474        }475        if fresh {476            kept += hit.iter().sum::<usize>();477            total += all.iter().sum::<usize>();478        }479        println!(480            "  W = {half:2}: K {:2} b {} kappa {} m {} q {} slope m/2 {} = {:+.8}  n = 1,3,5,7 mod 8 match {}/{} {}/{} {}/{} {}/{}",481            w.k,482            w.b,483            w.kappa,484            w.m,485            w.q,486            w.m / 2,487            flat(w.m) / 2.0,488            hit[0], all[0], hit[1], all[1], hit[2], all[2], hit[3], all[3]489        );490    }491    println!("  {} distinct half-widths, {kept}/{total} distinct checks; the nine odd rows above repeat their even neighbours and are not counted twice", seen.len());492}493494pub fn cell_width_ladders(rule: &Rule) {495    println!("carpet cell frame, the width family summed: L * excess_L = (m/2) ln L + C_W + O(1/L^2) at even L, C_W = m (ln 2 + gamma/2) + q L(1, chi_8) - G/8 + Delta_W");496    println!("  Delta_W is the exact contribution of the clipped layers n < K, the 1/L^2 coefficient is -q/4 + 1/64 + m/48 at L = 0 mod 4 and +q/4 + 1/64 + m/48 at L = 2 mod 4, and the odd-L constant is C_W - kappa");497    let lvalue = (1.0 + 2f64.sqrt()).ln() / 2f64.sqrt();498    for half in [0i64, 2, 4, 6, 8, 12, 16] {499        let w = width_terms(half);500        let excesses = width_excesses(rule, half, LADDER_LIMIT);501        let mut drift = 0.0f64;502        for number in odds(w.k as usize) {503            if (number as i64) < w.k {504                drift += excesses[(number - 1) / 2] - flat(width_excess_law(half, number as i64));505            }506        }507        let (m, q, kappa) = (flat(w.m), flat(w.q), flat(w.kappa));508        let slope = m / 2.0;509        let constant = m * (2f64.ln() + EULER / 2.0) + q * lvalue - CATALAN / 8.0 + drift;510        let miss = |count: usize| {511            mean(&excesses[..count]) * count as f64 - slope * (count as f64).ln() - constant512        };513        let window = |count: usize| {514            (mean(&excesses[..count]) * count as f64515                - mean(&excesses[..count / 2]) * (count / 2) as f64)516                / 2f64.ln()517        };518        println!(519            "  W = {half:2}: slope {} = {slope:+.8}  C_W {constant:+.10}  Delta_W {drift:+.8}",520            w.m / 2521        );522        println!(523            "    L = 0 mod 4: residual * L^2 {:+.7} {:+.7} {:+.7} at L = 400, 800, 1600 against -q/4 + 1/64 + m/48 = {:+.7}",524            miss(400) * 160000.0,525            miss(800) * 640000.0,526            miss(1600) * 2560000.0,527            -q / 4.0 + 1.0 / 64.0 + m / 48.0528        );529        println!(530            "    L = 2 mod 4: residual * L^2 {:+.7} {:+.7} {:+.7} at L = 402, 802, 1602 against +q/4 + 1/64 + m/48 = {:+.7}",531            miss(402) * 402.0 * 402.0,532            miss(802) * 802.0 * 802.0,533            miss(1602) * 1602.0 * 1602.0,534            q / 4.0 + 1.0 / 64.0 + m / 48.0535        );536        println!(537            "    L odd: (residual + kappa) * L {:+.6} {:+.6} at L = 401, 1601, so the odd-L constant is C_W - kappa with an O(1/L) error",538            (miss(401) + kappa) * 401.0,539            (miss(1601) + kappa) * 1601.0540        );541        println!(542            "    sliding window L/2 to L: {:+.8} at L = 1600 against m/2 {:+.8}, {:+.8} at L = 1602 against m/2 + kappa/ln 2 {:+.8}",543            window(1600),544            slope,545            window(1602),546            slope + kappa / 2f64.ln()547        );548    }549}550551fn width_excesses(rule: &Rule, half: i64, limit: usize) -> Vec<f64> {552    odds(2 * limit - 1)553        .map(|number| arm_ink(rule, number as i64, half) - law_value(number))554        .collect()555}556557fn width_slope(excesses: &[f64], limit: usize) -> f64 {558    let near = mean(&excesses[..limit / 2]) * (limit / 2) as f64;559    let far = mean(&excesses[..limit]) * limit as f64;560    (far - near) / 2f64.ln()561}562563fn edge_law(half: i64) -> f64 {564    let w = width_terms(half);565    -(w.k as f64 + w.b as f64) / (4.0 * (2 * w.k + 1) as f64)566}567568pub fn cell_widths(rule: &Rule) {569    println!("carpet cell frame, band half-width W cells about the arm, slope of excess * L against ln L, K = W/2 and b = 1 when floor(K/2) is even");570    println!("  the twelve swept widths against the derived rational, with a best-rational search over denominators to 400 beside them as a blind reading");571    for half in [0i64, 2, 4, 6, 8, 10, 12, 16, 20, 24, 32, 64] {572        let slope = width_slope(&width_excesses(rule, half, WIDTH_LIMIT), WIDTH_LIMIT);573        let (numerator, denominator) = nearest(slope);574        println!(575            "  W = {half:2} cells at L = {WIDTH_LIMIT}: slope {slope:+.6}  best rational {numerator}/{denominator} = {:+.6}  search residual {:.1e}  against -(K + b)/(4(2K + 1)) = {:+.6}",576            numerator as f64 / denominator as f64,577            (slope - numerator as f64 / denominator as f64).abs(),578            edge_law(half)579        );580    }581    println!("  odd W is not a new band: x - y is even on the arm, so W and W - 1 read one point set and one rational");582    for half in [0i64, 1, 2, 3] {583        let slope = width_slope(&width_excesses(rule, half, WIDTH_LIMIT), WIDTH_LIMIT);584        println!(585            "  W = {half:2} cells at L = {WIDTH_LIMIT}: slope {slope:+.6}  against -(K + b)/(4(2K + 1)) = {:+.6}",586            edge_law(half)587        );588    }589    println!("  the seven smallest widths recomputed to L = {CHECK_LIMIT}, four times the sweep, against the exact rational");590    for half in [0i64, 2, 4, 6, 8, 10, 12] {591        let excesses = width_excesses(rule, half, CHECK_LIMIT);592        let target = edge_law(half);593        let gaps: Vec<f64> = [WIDTH_LIMIT, HELD_LIMIT, CHECK_LIMIT]594            .iter()595            .map(|limit| (width_slope(&excesses, *limit) - target).abs())596            .collect();597        println!(598            "  W = {half:2} cells: slope {:+.8} at L = {CHECK_LIMIT} against {target:+.8}  gap {:.1e} (L = {WIDTH_LIMIT})  {:.1e} (L = {HELD_LIMIT})  {:.1e} (L = {CHECK_LIMIT})",599            width_slope(&excesses, CHECK_LIMIT),600            gaps[0],601            gaps[1],602            gaps[2]603        );604    }605    println!(606        "  four widths held out of the sweep, predicted before measuring, at L = {HELD_LIMIT}"607    );608    for half in [14i64, 18, 28, 36] {609        let slope = width_slope(&width_excesses(rule, half, HELD_LIMIT), HELD_LIMIT);610        let target = edge_law(half);611        println!(612            "  W = {half:2} cells: predicted {target:+.8}  measured {slope:+.8}  gap {:.1e}",613            (slope - target).abs()614        );615    }616    println!(617        "  the width law's own limit as W grows is -1/8 = {:+.6}, the value the fixed-fraction lattice band measures",618        -0.125619    );620}