star.rs

15.1 kB · rust · 402 lines

1use crate::bang::code_to_corners;2use mrlycore::errors::{value_error, Result};3use mrlynum::series::{CATALAN, EULER};45/// The exact reading of a cut layer: how many cells were inked out of how many were read.6#[derive(Clone, Copy, Debug, PartialEq, Eq)]7pub struct Share {8    /// The count of inked cells.9    pub inked: i64,10    /// The count of cells read.11    pub cells: i64,12}1314impl Share {15    /// The share as a real number.16    pub fn value(self) -> f64 {17        self.inked as f64 / self.cells as f6418    }1920    /// The share in lowest terms, numerator then denominator.21    pub fn reduced(self) -> (i64, i64) {22        let (mut a, mut b) = (self.inked.abs(), self.cells.abs());23        while b != 0 {24            let next = a % b;25            a = b;26            b = next;27        }28        let divisor = a.max(1);29        (self.inked / divisor, self.cells / divisor)30    }31}3233/// The real character mod 8 of `Q(sqrt 2)`: `+1` at `n = 1, 7`, `-1` at `n = 3, 5`, zero at even `n`.34///35/// ```36/// use mrlymath::six::star::chi8;37/// assert_eq!([chi8(1), chi8(3), chi8(5), chi8(7), chi8(4)], [1, -1, -1, 1, 0]);38/// ```39pub fn chi8(number: usize) -> i64 {40    [0, 1, 0, -1, 0, -1, 0, 1][number % 8]41}4243/// The closed form of the star arm's ink at odd `n`, `1/2 + chi_8(n)/(2n)`, as `n + chi_8(n)` cells of `2n`.44///45/// The arm `x = y` of the cut is a diameter of `2n` cells with no width to choose, and on it the two46/// block parities agree, so the space rule collapses to `floor(x/4)` even and the inked count is47/// `f(3n) - f(n) = n + chi_8(n)` with `f(m) = 4 floor(m/8) + min(m mod 8, 4)`.48///49/// ```50/// use mrlymath::six::star::arm_law;51/// assert_eq!(arm_law(7).unwrap().reduced(), (4, 7));52/// ```53pub fn arm_law(number: usize) -> Result<Share> {54    if number == 0 || number.is_multiple_of(2) {55        return value_error("the layer number must be odd.");56    }57    Ok(Share {58        inked: number as i64 + chi8(number),59        cells: 2 * number as i64,60    })61}6263/// The constant beside the decay, `ln(1 + sqrt 2)/(2 sqrt 2) - G/8 - gamma/4 - (ln 2)/2`.64///65/// Every limb is a closed form the subtraction of the two ink laws hands over: the character part is66/// `L(1, chi_8)/2` by the class number formula for `Q(sqrt 2)`, the square tail is `-G/8` with `G`67/// Catalan's constant, and the harmonic tail carries `-gamma/4 - (ln 2)/2`.68pub fn constant() -> f64 {69    (1.0 + 2f64.sqrt()).ln() / (2.0 * 2f64.sqrt()) - CATALAN / 8.0 - EULER / 4.0 - 2f64.ln() / 2.070}7172/// The cell-frame decay coefficient of a band of half-width `W` cells, `-(K + b)/(4(2K + 1))` for `K = floor(W/2)`.73///74/// `b` is the block parity of the band edge, one when `floor(K/2)` is even and zero when it is odd.75/// The band's per-layer excess is `kappa chi + (m + q chi_8(n))/n + chi/(8 n^2)`, and summing the76/// `1/n` term over the first `L` odd layers hands this coefficient as `m/2`, so it is a theorem and77/// not a fit. At even `W` it reads `-(W + 2b)/(8(W + 1))` and at odd `W` it reads78/// `-(W - 1 + 2b)/(8W)`, since `x - y` is even on the cut and an odd width is never a new band:79/// `W = 1` returns the arm's `-1/4` and `W = 3` returns `-1/6`. As `W` grows it tends to `-1/8`.80///81/// ```82/// use mrlymath::six::star::width_law;83/// assert_eq!([width_law(0), width_law(2), width_law(4)], [-0.25, -1.0 / 6.0, -0.1]);84/// assert_eq!([width_law(1), width_law(3)], [width_law(0), width_law(2)]);85/// ```86pub fn width_law(half: usize) -> f64 {87    let reach = (half / 2) as f64;88    let parity = f64::from(u8::from((half / 4).is_multiple_of(2)));89    -(reach + parity) / (4.0 * (2.0 * reach + 1.0))90}9192/// The three classes of layer count the `1/L^2` term of the decay reads.93#[derive(Clone, Copy, Debug, PartialEq, Eq)]94pub enum Branch {95    /// `L` divisible by four, where the residual is `-23/192`.96    Zero,97    /// `L` twice an odd number, where the residual is `+25/192`.98    Two,99    /// Odd `L`, where the constant shifts by `1/8` and the error is `O(1/L)`.100    Odd,101}102103impl Branch {104    /// The branch of a layer count.105    pub fn of(layers: usize) -> Branch {106        match layers % 4 {107            0 => Branch::Zero,108            2 => Branch::Two,109            _ => Branch::Odd,110        }111    }112113    /// The exact `1/L^2` coefficient at even `L`, absent at odd `L`.114    pub fn residual(self) -> Option<f64> {115        match self {116            Branch::Zero => Some(-23.0 / 192.0),117            Branch::Two => Some(25.0 / 192.0),118            Branch::Odd => None,119        }120    }121122    /// The constant the ladder converges on, `C` at even `L` and `C + 1/8` at odd `L`.123    pub fn constant(self) -> f64 {124        constant() + if self == Branch::Odd { 0.125 } else { 0.0 }125    }126127    /// The name of the branch.128    pub fn name(self) -> &'static str {129        match self {130            Branch::Zero => "0 mod 4",131            Branch::Two => "2 mod 4",132            Branch::Odd => "odd",133        }134    }135}136137/// The `L`-layer reading of the ghost star's decay in the cell frame.138#[derive(Clone, Copy, Debug, PartialEq)]139pub struct Decay {140    /// The layer count `L`.141    pub layers: usize,142    /// The mean excess of the star over the background across the first `L` odd layers.143    pub excess: f64,144    /// That excess times `L`.145    pub scaled: f64,146    /// The scaled excess plus `(ln L)/4`, which settles on the branch constant.147    pub logged: f64,148    /// The miss of the settled value against the branch constant.149    pub miss: f64,150    /// The miss times `L`, the reading odd `L` leaves behind.151    pub linear: f64,152    /// The miss times `L` squared, the reading even `L` leaves behind.153    pub residual: f64,154    /// The slope of the scaled excess against `ln L`, read from `L/2` to `L`, absent unless `L` is divisible by four.155    pub slope: Option<f64>,156}157158/// The ghost star of a coded cube's hexagonal cut stack, read in the cell frame.159///160/// The cut of the level-one cube of side `n` lives on the cube's own `4n` cell lattice, one layer at161/// a time and nothing resampled, so the half-cell displacement `1/(2n)` between consecutive layers162/// is carried rather than averaged away. A cell `(x, y, z)` of the plane `x + y + z = 6n - 2` is163/// inked when the design's corner mask holds the three block parities `floor(c/4) mod 2`.164pub struct Star {165    corners: [bool; 8],166}167168impl Star {169    /// Reads the star of a base-2 space code, the carpet being `23`.170    pub fn new(code: u128) -> Result<Star> {171        let filled = code_to_corners(code, 3, 2)?;172        let mut corners = [false; 8];173        for corner in filled {174            corners[usize::from(corner[0]) << 2175                | usize::from(corner[1]) << 1176                | usize::from(corner[2])] = true;177        }178        Ok(Star { corners })179    }180181    fn inked(&self, x: i64, y: i64, z: i64) -> bool {182        let bit = |c: i64| (c.div_euclid(4) & 1) as usize;183        self.corners[bit(x) << 2 | bit(y) << 1 | bit(z)]184    }185186    /// The ink of the cut cell at column `x` and even height `z` of the layer at odd `n`.187    ///188    /// The answer is absent where the plane `x + y + z = 6n - 2` leaves the cube, which is what draws189    /// the hexagon's own edge.190    pub fn cell(&self, number: usize, x: i64, z: i64) -> Option<bool> {191        let n = number as i64;192        let y = 6 * n - 2 - x - z;193        let inside = (0..4 * n).contains(&x) && (0..4 * n).contains(&z) && (0..4 * n).contains(&y);194        inside.then(|| self.inked(x, y, z))195    }196197    /// The exact ink share of the whole hexagonal cut at odd `n`, the background the star is read against.198    ///199    /// The rows are the `2n` even heights `z`, and each runs over the `x` the plane leaves inside the200    /// cube. The fill depends on `x` only through `x mod 8`, so a row is counted in eight steps and201    /// the whole layer in `O(n)`.202    ///203    /// ```204    /// use mrlymath::six::star::Star;205    /// assert_eq!(Star::new(23).unwrap().hexagon(3).unwrap().reduced(), (7, 9));206    /// ```207    pub fn hexagon(&self, number: usize) -> Result<Share> {208        if number == 0 || number.is_multiple_of(2) {209            return value_error("the layer number must be odd.");210        }211        let n = number as i64;212        let size = 4 * n;213        let (mut cells, mut inked) = (0i64, 0i64);214        for index in 0..2 * n {215            let z = 2 * index;216            let target = 6 * n - 2 - z;217            let low = 0.max(target - size + 1);218            let high = (size - 1).min(target);219            if high < low {220                continue;221            }222            for rest in 0..8 {223                let count = (high - rest).div_euclid(8) - (low - 1 - rest).div_euclid(8);224                if count <= 0 {225                    continue;226                }227                cells += count;228                if self.inked(rest, (target - rest).rem_euclid(8), z) {229                    inked += count;230                }231            }232        }233        Ok(Share { inked, cells })234    }235236    /// The exact ink share of the band of half-width `W` cells about the arm `x = y` at odd `n`.237    ///238    /// At `W = 0` the band is the arm itself, the `2n` cells of the diameter, whose share is the239    /// closed form of [`arm_law`].240    ///241    /// ```242    /// use mrlymath::six::star::{arm_law, Star};243    /// assert_eq!(Star::new(23).unwrap().arm(9, 0).unwrap(), arm_law(9).unwrap());244    /// ```245    pub fn arm(&self, number: usize, half: usize) -> Result<Share> {246        if number == 0 || number.is_multiple_of(2) {247            return value_error("the layer number must be odd.");248        }249        let (n, half) = (number as i64, half as i64);250        let size = 4 * n;251        let (mut cells, mut inked) = (0i64, 0i64);252        for index in 0..2 * n {253            let z = 2 * index;254            let target = 6 * n - 2 - z;255            let base = target / 2;256            for x in base - half - 1..=base + half + 1 {257                let y = target - x;258                if !(0..size).contains(&x) || !(0..size).contains(&y) || (x - y).abs() > half {259                    continue;260                }261                cells += 1;262                inked += i64::from(self.inked(x, y, z));263            }264        }265        Ok(Share { inked, cells })266    }267268    /// The per-layer excess of the star band over the hexagon across the first `L` odd layers.269    pub fn excesses(&self, layers: usize, half: usize) -> Result<Vec<f64>> {270        if layers == 0 {271            return value_error("the layer count must be at least one.");272        }273        (0..layers)274            .map(|step| {275                let number = 2 * step + 1;276                Ok(self.arm(number, half)?.value() - self.hexagon(number)?.value())277            })278            .collect()279    }280}281282/// The decay read off the per-layer excesses at a layer count, the slope taken from `L/2` to `L`.283///284/// At `W = 0` and even `L` the reading is `L * excess_L = -(ln L)/4 + C + O(1/L^2)`, so the slope285/// settles on `-1/4` and the residual on the branch's exact `1/L^2` coefficient. The sliding window286/// carries a residue trap: the per-layer excess opens on a term in the ink law's character whose sum287/// vanishes at even `L` alone, so a window from `L/2` to `L` cancels it only when `L` is divisible by288/// four. At `L = 2 mod 4` the half is odd and the window converges somewhere else entirely, so the289/// slope is refused there rather than reported wrong.290pub fn decay(excesses: &[f64], layers: usize) -> Result<Decay> {291    if layers < 2 || layers > excesses.len() {292        return value_error("the layer count must be at least two and within the excesses read.");293    }294    let mean = |count: usize| excesses[..count].iter().sum::<f64>() / count as f64;295    let scaled = mean(layers) * layers as f64;296    let near = mean(layers / 2) * (layers / 2) as f64;297    let logged = scaled + (layers as f64).ln() / 4.0;298    let miss = logged - Branch::of(layers).constant();299    Ok(Decay {300        layers,301        excess: mean(layers),302        scaled,303        logged,304        miss,305        linear: miss * layers as f64,306        residual: miss * layers as f64 * layers as f64,307        slope: layers308            .is_multiple_of(4)309            .then(|| (scaled - near) / 2f64.ln()),310    })311}312313#[cfg(test)]314mod tests {315    use super::*;316317    #[test]318    fn the_arm_ink_is_the_closed_form() {319        let star = Star::new(23).unwrap();320        let read: Vec<(i64, i64)> = (0..8)321            .map(|step| star.arm(2 * step + 1, 0).unwrap().reduced())322            .collect();323        assert_eq!(324            read,325            vec![326                (1, 1),327                (1, 3),328                (2, 5),329                (4, 7),330                (5, 9),331                (5, 11),332                (6, 13),333                (8, 15)334            ]335        );336        for step in 0..8 {337            let number = 2 * step + 1;338            assert_eq!(star.arm(number, 0).unwrap(), arm_law(number).unwrap());339        }340        assert!((0..1001).all(|step| {341            let number = 2 * step + 1;342            star.arm(number, 0).unwrap() == arm_law(number).unwrap()343        }));344    }345346    #[test]347    fn the_hexagon_ink_is_the_cut_ink_law() {348        let star = Star::new(23).unwrap();349        for step in 0..28 {350            let number = 2 * step + 1;351            let n = number as i64;352            let chi = if (3 * n - 1) / 2 % 2 == 0 { 1 } else { -1 };353            let want =354                0.5 + chi as f64 / 8.0 + 1.0 / (2 * n) as f64 - chi as f64 / (8 * n * n) as f64;355            assert!((star.hexagon(number).unwrap().value() - want).abs() < 1e-12);356        }357    }358359    #[test]360    fn the_cell_frame_decay_is_minus_a_quarter() {361        let star = Star::new(23).unwrap();362        let excesses = star.excesses(400, 0).unwrap();363        let read = decay(&excesses, 28).unwrap();364        assert_eq!(format!("{:.6}", read.scaled), "-1.126964");365        assert_eq!(format!("{:.10}", read.logged), "-0.2939128437");366        assert_eq!(format!("{:.8}", read.residual), "-0.11937029");367        let deep = decay(&excesses, 400).unwrap();368        assert_eq!(format!("{:.6}", deep.scaled), "-1.791627");369        assert_eq!(format!("{:.10}", deep.logged), "-0.2937613344");370        assert_eq!(format!("{:.8}", deep.residual), "-0.11978958");371        assert!((deep.slope.expect("a window at L = 0 mod 4") + 0.25).abs() < 1e-4);372        assert_eq!(decay(&excesses, 102).unwrap().slope, None);373        assert_eq!(format!("{:.10}", Branch::Zero.constant()), "-0.2937605857");374        assert_eq!(Branch::of(400).residual(), Some(-23.0 / 192.0));375        assert_eq!(Branch::of(402).residual(), Some(25.0 / 192.0));376        assert_eq!(Branch::of(401).name(), "odd");377    }378379    #[test]380    fn a_wider_band_walks_off_the_quarter() {381        let star = Star::new(23).unwrap();382        assert_eq!(width_law(1), width_law(0));383        assert_eq!(width_law(3), width_law(2));384        assert_eq!(format!("{:.6}", width_law(3)), "-0.166667");385        for half in [1usize, 2, 3, 4, 6] {386            let excesses = star.excesses(400, half).unwrap();387            let slope = decay(&excesses, 400)388                .unwrap()389                .slope390                .expect("a window at L = 0 mod 4");391            assert!((slope - width_law(half)).abs() < 2e-4);392        }393    }394395    #[test]396    fn even_layers_are_refused_and_the_code_is_checked() {397        let star = Star::new(23).unwrap();398        assert!(star.hexagon(4).is_err());399        assert!(star.arm(0, 0).is_err());400        assert!(Star::new(1 << 9).is_err());401    }402}