morse.rs

9.3 kB · rust · 280 lines

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