sim-lib-numbers-numeric 0.1.0-rc.1

SIM workspace package for sim lib numbers numeric.
Documentation
//! Argument and option parsing for the numeric operations, turning expression
//! and table inputs into typed `DiffOpts`, `QuadOpts`, and `OdeOpts`.

use std::{collections::HashMap, sync::Arc};

use sim_kernel::{Cx, DefaultFactory, EagerPolicy, Error, Expr, QuoteMode, Result, Symbol, Value};

use super::traits::{DiffOpts, OdeOpts, QuadOpts};

pub fn parse_diff_exprs(cx: &mut Cx, args: Vec<Expr>) -> Result<(Value, Symbol, Value, DiffOpts)> {
    let [func_expr, var_expr, point_expr, rest @ ..] = args.as_slice() else {
        return Err(Error::Eval(
            "numeric-diff expects func, var, point, and optional key/value options".to_owned(),
        ));
    };
    let options = parse_expr_options(cx, "numeric-diff", rest)?;
    let func = cx.eval_expr(func_expr.clone())?;
    let var = parse_symbolish_expr(var_expr)
        .ok_or_else(|| Error::Eval("numeric-diff expects a symbol variable".to_owned()))?;
    let point = cx.eval_expr(point_expr.clone())?;
    let method = option_symbol(&options, "method")?.unwrap_or(Symbol::new("auto"));
    let h = option_f64(&options, "h")?.unwrap_or(1.0e-6);
    reject_unknown("numeric-diff", &options, &["method", "h"])?;
    Ok((func, var, point, DiffOpts { method, h }))
}

pub fn parse_integrate_exprs(
    cx: &mut Cx,
    name: &str,
    args: Vec<Expr>,
    adaptive: bool,
) -> Result<(Value, Symbol, Value, Value, QuadOpts)> {
    let [func_expr, var_expr, lo_expr, hi_expr, rest @ ..] = args.as_slice() else {
        return Err(Error::Eval(format!(
            "{name} expects func, var, lo, hi, and optional key/value options"
        )));
    };
    let options = parse_expr_options(cx, name, rest)?;
    let func = cx.eval_expr(func_expr.clone())?;
    let var = parse_symbolish_expr(var_expr)
        .ok_or_else(|| Error::Eval(format!("{name} expects a symbol variable")))?;
    let lo = cx.eval_expr(lo_expr.clone())?;
    let hi = cx.eval_expr(hi_expr.clone())?;
    let defaults = if adaptive {
        QuadOpts::adaptive_default()
    } else {
        QuadOpts::fixed_default()
    };
    let method = option_symbol(&options, "method")?.unwrap_or(defaults.method);
    let n = option_usize(&options, "n")?;
    let tol = option_f64(&options, "tol")?.or(defaults.tol);
    reject_unknown(name, &options, &["method", "n", "tol"])?;
    Ok((func, var, lo, hi, QuadOpts { method, n, tol }))
}

pub fn parse_ode_exprs(
    cx: &mut Cx,
    args: Vec<Expr>,
) -> Result<(Value, Symbol, Symbol, Value, Value, Value, OdeOpts)> {
    let [
        dy_expr,
        var_expr,
        y_var_expr,
        x0_expr,
        y0_expr,
        x_end_expr,
        rest @ ..,
    ] = args.as_slice()
    else {
        return Err(Error::Eval(
            "ode-solve expects dy/dx, x var, y var, x0, y0, x-end, and optional key/value options"
                .to_owned(),
        ));
    };
    let options = parse_expr_options(cx, "ode-solve", rest)?;
    let dy = cx.eval_expr(dy_expr.clone())?;
    let var = parse_symbolish_expr(var_expr)
        .ok_or_else(|| Error::Eval("ode-solve expects a symbol x variable".to_owned()))?;
    let y_var = parse_symbolish_expr(y_var_expr)
        .ok_or_else(|| Error::Eval("ode-solve expects a symbol y variable".to_owned()))?;
    let x0 = cx.eval_expr(x0_expr.clone())?;
    let y0 = cx.eval_expr(y0_expr.clone())?;
    let x_end = cx.eval_expr(x_end_expr.clone())?;
    let method = option_symbol(&options, "method")?.unwrap_or(Symbol::new("auto"));
    let h = option_f64(&options, "h")?;
    let tol = option_f64(&options, "tol")?;
    let max_steps = option_usize(&options, "max-steps")?;
    reject_unknown("ode-solve", &options, &["method", "h", "tol", "max-steps"])?;
    Ok((
        dy,
        var,
        y_var,
        x0,
        y0,
        x_end,
        OdeOpts {
            method,
            h,
            tol,
            max_steps,
        },
    ))
}

pub fn parse_table_options(
    cx: &mut Cx,
    name: &str,
    value: &Value,
) -> Result<HashMap<String, Value>> {
    let expr = value.object().as_expr(cx)?;
    let Expr::Map(entries) = expr else {
        return Err(Error::Eval(format!("{name} options must be a table")));
    };
    let mut options = HashMap::new();
    for (key_expr, value_expr) in entries {
        options.insert(keyword(&key_expr)?, cx.eval_expr(value_expr)?);
    }
    Ok(options)
}

pub fn parse_symbolish_value(cx: &mut Cx, value: &Value) -> Result<Option<Symbol>> {
    Ok(parse_symbolish_expr(&value.object().as_expr(cx)?))
}

pub fn option_symbol(options: &HashMap<String, Value>, key: &str) -> Result<Option<Symbol>> {
    match options.get(key) {
        Some(value) => {
            let mut cx = dummy_cx();
            parse_symbolish_value(&mut cx, value)?
                .map(Some)
                .ok_or_else(|| Error::Eval(format!("expected symbol option :{key}")))
        }
        None => Ok(None),
    }
}

pub fn option_f64(options: &HashMap<String, Value>, key: &str) -> Result<Option<f64>> {
    match options.get(key) {
        Some(value) => value
            .object()
            .display(&mut dummy_cx())?
            .parse::<f64>()
            .map(Some)
            .map_err(|_| Error::Eval(format!("expected numeric option :{key}"))),
        None => Ok(None),
    }
}

pub fn option_usize(options: &HashMap<String, Value>, key: &str) -> Result<Option<usize>> {
    match options.get(key) {
        Some(value) => value
            .object()
            .display(&mut dummy_cx())?
            .parse::<usize>()
            .map(Some)
            .map_err(|_| Error::Eval(format!("expected integer option :{key}"))),
        None => Ok(None),
    }
}

pub fn reject_unknown(
    name: &str,
    options: &HashMap<String, Value>,
    allowed: &[&str],
) -> Result<()> {
    for key in options.keys() {
        if !allowed.iter().any(|allowed_key| key == allowed_key) {
            return Err(Error::Eval(format!("{name}: unknown option :{key}")));
        }
    }
    Ok(())
}

fn parse_expr_options(cx: &mut Cx, name: &str, exprs: &[Expr]) -> Result<HashMap<String, Value>> {
    if !exprs.len().is_multiple_of(2) {
        return Err(Error::Eval(format!(
            "{name} options must be key/value pairs"
        )));
    }
    let mut options = HashMap::new();
    for pair in exprs.chunks(2) {
        options.insert(keyword(&pair[0])?, cx.eval_expr(pair[1].clone())?);
    }
    Ok(options)
}

fn parse_symbolish_expr(expr: &Expr) -> Option<Symbol> {
    match expr {
        Expr::Symbol(symbol) => Some(symbol.clone()),
        Expr::Quote { mode, expr } if *mode == QuoteMode::Quote => match expr.as_ref() {
            Expr::Symbol(symbol) => Some(symbol.clone()),
            _ => None,
        },
        _ => None,
    }
}

fn keyword(expr: &Expr) -> Result<String> {
    let Expr::Symbol(symbol) = expr else {
        return Err(Error::Eval("expected keyword option".to_owned()));
    };
    let Some(keyword) = symbol.name.strip_prefix(':') else {
        return Err(Error::Eval(format!(
            "expected keyword option, found {symbol}"
        )));
    };
    Ok(keyword.to_owned())
}

fn dummy_cx() -> Cx {
    Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory))
}