blend.rs
12.2 kB · rust · 392 lines
1use crate::{code_of, Fault};2use mrlycore::json::parse;3use mrlycore::{json, Json, Map};4use mrlylab::ledger::{self, Axis, Key, Measure};5use mrlynum::blend;6use wasm_bindgen::prelude::*;78// TERMS910fn whole(text: &str) -> Result<i128, Fault> {11 text.trim()12 .parse()13 .map_err(|_| Fault::new(format!("term {text:?} is not a whole number.")))14}1516fn read(terms: &[String]) -> Result<Vec<i128>, Fault> {17 terms.iter().map(|term| whole(term)).collect()18}1920fn numbers(value: &Json) -> Result<Vec<i128>, Fault> {21 value22 .as_array()23 .ok_or_else(|| Fault::new("the terms are a JSON array of decimal strings."))?24 .iter()25 .map(|term| whole(term.as_str().unwrap_or_default()))26 .collect()27}2829fn pairs(text: &str) -> Result<Vec<(i128, i128)>, Fault> {30 let value = parse(text)?;31 let rows = value32 .as_array()33 .ok_or_else(|| Fault::new("the coefficients are a JSON array of pairs."))?;34 rows.iter()35 .map(|row| match (row[0].as_i64(), row[1].as_i64()) {36 (Some(num), Some(den)) if den > 0 => Ok((num as i128, den as i128)),37 _ => Err(Fault::new(38 "a coefficient is a numerator and a denominator above zero.",39 )),40 })41 .collect()42}4344fn spread(rule: &[(i128, i128)]) -> Json {45 json!(rule46 .iter()47 .map(|&(num, den)| json!([num as i64, den as i64]))48 .collect::<Vec<Json>>())49}5051// SPELLING5253fn size(num: i128, den: i128) -> String {54 if den == 1 {55 num.abs().to_string()56 } else {57 format!("{}/{den}", num.abs())58 }59}6061fn spell(value: f64) -> String {62 if !value.is_finite() {63 return "none".to_string();64 }65 let text = format!("{value:.4}");66 let text = text.trim_end_matches('0').trim_end_matches('.');67 if text == "-0" {68 "0".to_string()69 } else {70 text.to_string()71 }72}7374fn recurrence_text(rule: &[(i128, i128)]) -> String {75 let mut text = String::from("a(n) =");76 for (index, &(num, den)) in rule.iter().enumerate() {77 if num == 0 {78 continue;79 }80 text.push_str(if num < 0 {81 " - "82 } else if text.ends_with('=') {83 " "84 } else {85 " + "86 });87 let weight = size(num, den);88 if weight != "1" {89 text.push_str(&weight);90 text.push(' ');91 }92 text.push_str(&format!("a(n-{})", index + 1));93 }94 if text.ends_with('=') {95 text.push_str(" 0");96 }97 text98}99100fn polynomial_text(poly: &[(i128, i128)]) -> String {101 let top = poly.len().saturating_sub(1);102 let mut text = String::new();103 for (index, &(num, den)) in poly.iter().enumerate() {104 if num == 0 {105 continue;106 }107 let power = top - index;108 if text.is_empty() {109 if num < 0 {110 text.push('-');111 }112 } else {113 text.push_str(if num < 0 { " - " } else { " + " });114 }115 let weight = size(num, den);116 let shown = weight != "1" || power == 0;117 if shown {118 text.push_str(&weight);119 }120 if power > 0 {121 if shown {122 text.push(' ');123 }124 text.push('x');125 if power > 1 {126 text.push_str(&format!("^{power}"));127 }128 }129 }130 if text.is_empty() {131 text.push('0');132 }133 text134}135136// VIEWS137138fn tenlog(term: i128) -> f64 {139 if term == 0 {140 f64::NAN141 } else {142 (term.unsigned_abs() as f64).log10()143 }144}145146fn ratios(terms: &[i128]) -> Vec<String> {147 terms148 .windows(2)149 .map(|pair| spell(pair[1] as f64 / pair[0] as f64))150 .collect()151}152153fn triangle(terms: &[i128], depth: usize) -> Vec<Vec<String>> {154 let mut rows = vec![terms.to_vec()];155 while rows.len() < depth.max(1) {156 match rows.last() {157 Some(row) if row.len() > 1 => rows.push(blend::delta(row)),158 _ => break,159 }160 }161 rows.iter()162 .map(|row| row.iter().map(|term| term.to_string()).collect())163 .collect()164}165166fn slope(terms: &[i128]) -> f64 {167 let points: Vec<(f64, f64)> = terms168 .iter()169 .enumerate()170 .filter(|(_, &term)| term != 0)171 .map(|(index, &term)| (index as f64, tenlog(term)))172 .collect();173 let keep = points.len().div_ceil(2).max(4).min(points.len());174 let tail = &points[points.len() - keep..];175 let count = tail.len() as f64;176 let mean = |pick: fn(&(f64, f64)) -> f64| tail.iter().map(pick).sum::<f64>() / count;177 let (mx, my) = (mean(|point| point.0), mean(|point| point.1));178 let top: f64 = tail179 .iter()180 .map(|point| (point.0 - mx) * (point.1 - my))181 .sum();182 let bottom: f64 = tail183 .iter()184 .map(|point| (point.0 - mx) * (point.0 - mx))185 .sum();186 top / bottom187}188189fn views(terms: &[i128], depth: usize) -> Map {190 let rule = blend::recurrence(terms);191 let root = rule192 .as_ref()193 .map(|rule| blend::growth(rule))194 .filter(|root| root.is_finite() && *root > 0.0);195 let growth = root.unwrap_or_else(|| 10f64.powf(slope(terms)));196 let body = json!({197 "terms": terms.iter().map(|term| term.to_string()).collect::<Vec<String>>(),198 "log10": terms.iter().map(|&term| json!(tenlog(term))).collect::<Vec<Json>>(),199 "ratios": ratios(terms),200 "differences": triangle(terms, depth),201 "order": rule.as_ref().map_or(Json::Null, |rule| json!(rule.len())),202 "coefficients": rule.as_ref().map_or(Json::Null, |rule| spread(rule)),203 "characteristic": rule204 .as_ref()205 .map_or(Json::Null, |rule| spread(&blend::characteristic(rule))),206 "recurrence": rule.as_ref().map_or(String::new(), |rule| recurrence_text(rule)),207 "polynomial": rule.as_ref().map_or(String::new(), |rule| polynomial_text(208 &blend::characteristic(rule)209 )),210 "root": json!(root),211 "growth": json!(growth),212 "growth_from": if root.is_some() {213 "the recurrence root"214 } else {215 "the least-squares slope of the log terms over the tail, a fit"216 },217 "exponent": json!(growth.log10()),218 });219 match body {220 Json::Object(map) => map,221 _ => Map::new(),222 }223}224225// MIX226227fn held(value: Option<i128>) -> Result<i128, Fault> {228 value.ok_or_else(|| Fault::new("the mix passes a hundred and twenty-eight bits."))229}230231fn mixed(left: &[i128], right: &[i128], op: &str, argument: i32) -> Result<Vec<i128>, Fault> {232 let width = left.len().min(right.len());233 match op {234 "add" => {235 for index in 0..width {236 held(left[index].checked_add(right[index]))?;237 }238 Ok(blend::add(left, right))239 }240 "sub" => {241 for index in 0..width {242 held(left[index].checked_sub(right[index]))?;243 }244 Ok(blend::sub(left, right))245 }246 "hadamard" => {247 for index in 0..width {248 held(left[index].checked_mul(right[index]))?;249 }250 Ok(blend::hadamard(left, right))251 }252 "cauchy" => {253 for n in 0..width {254 let mut sum = 0i128;255 for i in 0..=n {256 sum = held(257 left[i]258 .checked_mul(right[n - i])259 .and_then(|part| sum.checked_add(part)),260 )?;261 }262 }263 Ok(blend::cauchy(left, right))264 }265 "shift" => Ok(blend::shift(left, argument.max(0) as usize)),266 "decimate" => Ok(blend::decimate(left, argument.max(1) as usize, 0)),267 "delta" => Ok(blend::delta(left)),268 "sigma" => {269 let mut sum = 0i128;270 for &term in left {271 sum = held(sum.checked_add(term))?;272 }273 Ok(blend::sigma(left))274 }275 "scale" => {276 for &term in left {277 held(term.checked_mul(argument as i128))?;278 }279 Ok(blend::scale(left, argument as i128))280 }281 other => Err(Fault::new(format!("unknown blend op {other:?}."))),282 }283}284285// EXPORTS286287/// Names the term operations a mix takes, in page order.288#[wasm_bindgen]289pub fn blend_ops() -> Vec<String> {290 [291 "add", "sub", "hadamard", "cauchy", "shift", "decimate", "delta", "sigma", "scale",292 ]293 .iter()294 .map(|op| op.to_string())295 .collect()296}297298/// Finds the smallest linear constant-coefficient recurrence every term satisfies, as JSON: null where none fits, else the order, the coefficients as numerator and denominator pairs newest term first, and the rule spelled out.299#[wasm_bindgen]300pub fn blend_recurrence(terms: Vec<String>) -> Result<String, Fault> {301 let terms = read(&terms)?;302 Ok(match blend::recurrence(&terms) {303 None => "null".to_string(),304 Some(rule) => json!({305 "order": rule.len(),306 "coefficients": spread(&rule),307 "recurrence": recurrence_text(&rule),308 })309 .to_string(),310 })311}312313/// Returns the monic characteristic polynomial of a recurrence highest power first, as JSON pairs, from the coefficients as JSON pairs.314#[wasm_bindgen]315pub fn blend_characteristic(coefficients: &str) -> Result<String, Fault> {316 Ok(spread(&blend::characteristic(&pairs(coefficients)?)).to_string())317}318319/// Returns the largest positive real root of a recurrence's characteristic polynomial, the growth rate, from the coefficients as JSON pairs.320#[wasm_bindgen]321pub fn blend_growth(coefficients: &str) -> Result<f64, Fault> {322 Ok(blend::growth(&pairs(coefficients)?))323}324325/// Reads one registry sequence with every view the plot draws, as JSON: the catalog row, the terms as decimal strings, the base-ten logarithm of each, the ratio of each term to the one before, the difference triangle to the depth, the recurrence with its characteristic polynomial and largest real root, and the growth with the exponent and which reading gave it.326#[wasm_bindgen]327#[allow(clippy::too_many_arguments)]328pub fn blend_series(329 code: &str,330 dimension: usize,331 base: usize,332 measure: &str,333 axis: &str,334 count: usize,335 cells: &str,336 depth: usize,337) -> Result<String, Fault> {338 let text = crate::ledger::ledger_row(code, dimension, base, measure, axis, count, cells)?;339 let row = parse(&text)?;340 let terms = numbers(&row["terms"])?;341 let mut out = match row {342 Json::Object(map) => map,343 _ => Map::new(),344 };345 out.extend(views(&terms, depth));346 Ok(Json::Object(out).to_string())347}348349/// Reads the first terms of one measure and axis for every design of a dimension and base, as JSON: the code, the sequence name, the terms as decimal strings and whether the budget cut them short.350#[wasm_bindgen]351pub fn blend_family(352 dimension: usize,353 base: usize,354 measure: &str,355 axis: &str,356 count: usize,357 cells: &str,358) -> Result<String, Fault> {359 let measure = Measure::parse(measure)?;360 let axis = Axis::parse(axis)?;361 let cells = code_of(cells)?;362 let rows: Vec<Json> = ledger::designs(dimension, base)?363 .iter()364 .filter_map(|&code| {365 let key = Key::new(code, dimension, base, measure, axis);366 let (terms, capped) = ledger::terms(&key, count, cells).ok()?;367 Some(json!({368 "code": code.to_string(),369 "name": key.name(),370 "capped": capped,371 "terms": terms.iter().map(|term| term.to_string()).collect::<Vec<String>>(),372 }))373 })374 .collect();375 Ok(json!(rows).to_string())376}377378/// Mixes two term lists by a blend operation and returns the mixed terms with the same views, as JSON: add, sub, hadamard and cauchy take both lists, shift, decimate, delta, sigma and scale take the first, and the argument is the shift count, the decimate step or the scale factor.379#[wasm_bindgen]380pub fn blend_mix(381 left: Vec<String>,382 right: Vec<String>,383 op: &str,384 argument: i32,385 depth: usize,386) -> Result<String, Fault> {387 let terms = mixed(&read(&left)?, &read(&right)?, op, argument)?;388 let mut out = views(&terms, depth);389 out.insert("op".to_string(), json!(op));390 out.insert("argument".to_string(), json!(argument));391 Ok(Json::Object(out).to_string())392}