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}