morse.rs

9.4 kB · rust · 281 lines

1use crate::{code_of, Fault, Grid};2use mrlyrs::core::json;3use mrlyrs::math::bang::Code;4use mrlyrs::math::two;5use mrlyrs::num::morse::{self, Lift, LIFTS};6use wasm_bindgen::prelude::*;78const WORD_MAX: usize = 4096;9const ROUNDS_MAX: usize = 12;10const SIDE_MAX: usize = 512;1112fn word_of(length: usize) -> Result<Vec<u8>, Fault> {13    if !(1..=WORD_MAX).contains(&length) {14        return Err(Fault::new(format!(15            "the word runs from one letter to {WORD_MAX}."16        )));17    }18    Ok(morse::digits(length))19}2021fn tile_of(code: &str, number: usize, base: usize) -> Result<Vec<u8>, Fault> {22    if number < 2 {23        return Err(Fault::new(format!("side {number} is below two.")));24    }25    let cell = two::create(Code::from(code_of(code)?), number, 1, 0, base)?;26    Ok(cell.types().bytes()?.to_vec())27}2829fn signs_of(tile: &[u8]) -> Vec<u8> {30    tile.iter().map(|&byte| 1 - (byte != 0) as u8).collect()31}3233fn side_of(number: usize, level: usize) -> Result<usize, Fault> {34    let side = number35        .checked_pow(level as u32)36        .filter(|&side| side <= SIDE_MAX)37        .ok_or_else(|| {38            Fault::new(format!(39                "side {number} to the {level} is more than the {SIDE_MAX} this page draws."40            ))41        })?;42    Ok(side)43}4445fn design_of(tile: &[u8]) -> Option<String> {46    for code in 0..16u128 {47        let cell = two::create(Code::from(code), 2, 1, 0, 2).ok()?;48        if signs_of(cell.types().bytes().ok()?) == tile {49            return Some(code.to_string());50        }51    }52    None53}5455// THE WORD5657/// Reads the Thue-Morse word to the length: the two constructions, the runs, and the boundary word.58///59/// The digit rule is the parity of the binary digit sum of the place; the substitution grows60/// `0 -> 01`, `1 -> 10` from a single 0. They agree letter for letter, which is the page's first61/// claim, checked here rather than asserted.62#[wasm_bindgen]63pub fn morse_word(length: usize) -> Result<String, Fault> {64    let digits = word_of(length)?;65    let substitution = morse::substitution(length);66    let runs = morse::runs(&digits);67    let longest = runs.iter().copied().max().unwrap_or(0);68    let boundary = morse::boundary(&digits);69    let doubling = morse::doubling(boundary.len());70    Ok(json!({71        "length": length,72        "digits": digits,73        "substitution": substitution.clone(),74        "agree": digits == substitution,75        "ones": digits.iter().map(|&bit| bit as usize).sum::<usize>(),76        "runs": runs.clone(),77        "longest": longest,78        "cube_free": longest <= 2,79        "singles": runs.iter().filter(|&&run| run == 1).count(),80        "doubles": runs.iter().filter(|&&run| run == 2).count(),81        "boundary": boundary.clone(),82        "doubling": doubling.clone(),83        "doubling_agree": boundary == doubling,84    })85    .to_string())86}8788/// Returns the substitution stage after the rounds, a word of length two to the rounds.89#[wasm_bindgen]90pub fn morse_stage(rounds: usize) -> Result<Vec<u8>, Fault> {91    if rounds > ROUNDS_MAX {92        return Err(Fault::new(format!(93            "the substitution animates to {ROUNDS_MAX} rounds."94        )));95    }96    Ok(morse::stage(rounds))97}9899// THE LIFTS100101/// Builds one plane lift of the word as a sign grid, zero for plus one and one for minus one.102#[wasm_bindgen]103pub fn morse_lift(kind: &str, level: usize) -> Result<Grid, Fault> {104    let side = side_of(2, level)?;105    let types = morse::lift(kind.parse::<Lift>()?, side);106    Ok(Grid {107        width: side as u32,108        height: side as u32,109        types,110    })111}112113/// Tests every lift at the level against the Kronecker power of its own corner tile, as JSON.114///115/// Each row carries the lift's formula, the verdict, the corner tile, the count of sites where116/// the fold fails, the first such site, the earlier lift it is identical to when there is one,117/// and the plane design whose plus-minus render it is when the fold succeeds.118#[wasm_bindgen]119pub fn morse_gallery(level: usize) -> Result<String, Fault> {120    let side = side_of(2, level)?;121    let mut rows = Vec::new();122    let mut drawn: Vec<(&'static str, Vec<u8>)> = Vec::new();123    for kind in LIFTS {124        let grid = morse::lift(kind, side);125        let read = morse::fold(&grid, side, 2)?;126        let twin = drawn127            .iter()128            .find(|(_, seen)| *seen == grid)129            .map(|(name, _)| *name);130        rows.push(json!({131            "name": kind.name(),132            "formula": kind.formula(),133            "side": side,134            "level": read.level,135            "folds": read.folds,136            "tile": read.tile.clone(),137            "faults": read.faults,138            "first": read.first.map(|(r, c)| vec![r, c]),139            "twin": twin,140            "design": if read.folds { design_of(&read.tile) } else { None },141        }));142        drawn.push((kind.name(), grid));143    }144    Ok(json!(rows).to_string())145}146147// THE DESIGNS148149/// Builds a plane design read plus-minus at the level: plus one where it fills, minus one where150/// it does not, folded by the exclusive or rather than the and.151#[wasm_bindgen]152pub fn morse_signs(code: &str, number: usize, base: usize, level: usize) -> Result<Grid, Fault> {153    let side = side_of(number, level)?;154    let tile = signs_of(&tile_of(code, number, base)?);155    Ok(Grid {156        width: side as u32,157        height: side as u32,158        types: morse::power(&tile, number, level)?,159    })160}161162// THE FILTER163164struct Pair {165    grown: Vec<u8>,166    fine: Vec<u8>,167    tile: Vec<u8>,168    wide: usize,169}170171fn levels(code: &str, number: usize, base: usize, level: usize, fold: &str) -> Result<Pair, Fault> {172    let wide = side_of(number, level + 1)?;173    let side = wide / number;174    let tile = tile_of(code, number, base)?;175    let (coarse, fine) = match fold {176        "design" => (177            two::create(Code::from(code_of(code)?), number, level, 0, base)?178                .types()179                .bytes()?180                .to_vec(),181            two::create(Code::from(code_of(code)?), number, level + 1, 0, base)?182                .types()183                .bytes()?184                .to_vec(),185        ),186        "sign" => {187            let signs = signs_of(&tile);188            (189                morse::power(&signs, number, level)?,190                morse::power(&signs, number, level + 1)?,191            )192        }193        other => return Err(Fault::new(format!("unknown fold {other:?}."))),194    };195    Ok(Pair {196        grown: morse::upsample(&coarse, side, number),197        fine,198        tile,199        wide,200    })201}202203/// Builds the difference filter: a design's level blown up to the next side and exclusive-ored204/// against the next level, one where the two disagree.205#[wasm_bindgen]206pub fn morse_difference(207    code: &str,208    number: usize,209    base: usize,210    level: usize,211    fold: &str,212) -> Result<Grid, Fault> {213    let pair = levels(code, number, base, level, fold)?;214    Ok(Grid {215        width: pair.wide as u32,216        height: pair.wide as u32,217        types: morse::difference(&pair.grown, &pair.fine),218    })219}220221/// Judges the difference filter against its closed form and against the Thue-Morse grid, as JSON.222///223/// The closed form is exact in both folds and needs no search. Under the and fold the next level224/// is the blown-up level masked by the tile, so the difference is the blown-up level masked by225/// the tile's complement. Under the exclusive-or fold the next level is the blown-up level226/// exclusive-ored with the repeated tile, so the difference is the repeated tile alone. Either227/// way the filter keeps only the last digit, so its output repeats with period `number` while the228/// Thue-Morse grid does not, and the two differ at every side past `number`. Under the229/// exclusive-or fold at side two the disagreement is exactly half the sites at every side four and230/// beyond, for every tile: the low digits fix a residue class and the high digits of the two231/// coordinates carry opposite Thue-Morse letters on exactly half of each class.232#[wasm_bindgen]233pub fn morse_filter(234    code: &str,235    number: usize,236    base: usize,237    level: usize,238    fold: &str,239) -> Result<String, Fault> {240    let pair = levels(code, number, base, level, fold)?;241    let wide = pair.wide;242    let difference = morse::difference(&pair.grown, &pair.fine);243    let signs = signs_of(&pair.tile);244    let (form, closed) = if fold == "sign" {245        (246            "the base tile repeated",247            morse::repeat(&signs, number, wide),248        )249    } else {250        let mask = morse::repeat(&pair.tile, number, wide);251        (252            "the level below, punched by the tile's complement",253            pair.grown254                .iter()255                .zip(&mask)256                .map(|(&bit, &keep)| bit & (1 - keep))257                .collect::<Vec<u8>>(),258        )259    };260    let closed_faults = morse::faults(&difference, &closed);261    let grid = (number == 2).then(|| morse::lift(Lift::Parity, wide));262    let morse_faults = grid.as_ref().map(|grid| morse::faults(&difference, grid));263    Ok(json!({264        "fold": fold,265        "number": number,266        "level": level,267        "side": wide,268        "tile": pair.tile.clone(),269        "signs": signs.clone(),270        "morse_tile": signs == vec![0, 1, 1, 0],271        "form": form,272        "closed": closed.clone(),273        "closed_faults": closed_faults,274        "closed_exact": closed_faults == 0,275        "morse_faults": morse_faults,276        "morse_exact": morse_faults == Some(0),277        "lit": difference.iter().map(|&bit| bit as usize).sum::<usize>(),278        "cells": difference.len(),279    })280    .to_string())281}