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}