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}