sim-lib-numbers-signal 0.1.0

Deterministic transforms, autoregression, spectral estimation, and guarded signal operations.
Documentation
//! Parsing and rendering helpers shared by spectral runtime operations.

use std::collections::BTreeMap;

use sim_kernel::{Cx, Error, Expr, NumberLiteral, Result, Symbol, Value, force_list_to_vec};

use crate::{
    ArOrderCriterion, BurgTermination, EndpointConvention, Normalization, NyquistConvention,
    Periodicity, SignConvention,
    runtime_convolution_render::f64_value,
    runtime_spectral_callable::{SpectralOperation, argument_error},
};

pub(crate) type Options = BTreeMap<String, Value>;

pub(crate) fn expr_options(
    cx: &mut Cx,
    operation: SpectralOperation,
    exprs: &[Expr],
) -> Result<Options> {
    if !exprs.len().is_multiple_of(2) {
        return Err(argument_error(operation));
    }
    let mut options = Options::new();
    for pair in exprs.chunks(2) {
        let key = keyword_expr(operation, &pair[0])?;
        insert_option(operation, &mut options, key, cx.eval_expr(pair[1].clone())?)?;
    }
    Ok(options)
}

pub(crate) fn value_options(
    cx: &mut Cx,
    operation: SpectralOperation,
    values: &[Value],
) -> Result<Options> {
    if !values.len().is_multiple_of(2) {
        return Err(argument_error(operation));
    }
    let mut options = Options::new();
    for pair in values.chunks(2) {
        let key = keyword_expr(operation, &pair[0].object().as_expr(cx)?)?;
        insert_option(operation, &mut options, key, pair[1].clone())?;
    }
    Ok(options)
}

fn insert_option(
    operation: SpectralOperation,
    options: &mut Options,
    key: String,
    value: Value,
) -> Result<()> {
    if options.insert(key.clone(), value).is_some() {
        return Err(Error::Eval(format!(
            "{}: duplicate option :{key}",
            operation.name()
        )));
    }
    Ok(())
}

fn keyword_expr(operation: SpectralOperation, expression: &Expr) -> Result<String> {
    let Expr::Symbol(symbol) = expression else {
        return Err(argument_error(operation));
    };
    symbol
        .name
        .strip_prefix(':')
        .map(str::to_owned)
        .ok_or_else(|| argument_error(operation))
}

pub(crate) fn reject_unknown(
    operation: SpectralOperation,
    options: &Options,
    allowed: &[&str],
) -> Result<()> {
    for key in options.keys() {
        if !allowed.contains(&key.as_str()) {
            return Err(Error::Eval(format!(
                "{}: unknown option :{key}",
                operation.name()
            )));
        }
    }
    Ok(())
}

pub(crate) fn option_symbol(cx: &mut Cx, options: &Options, key: &str) -> Result<Option<String>> {
    options
        .get(key)
        .map(|value| {
            let Expr::Symbol(symbol) = value.object().as_expr(cx)? else {
                return Err(Error::Eval(format!("option :{key} must be a symbol")));
            };
            Ok(symbol.as_qualified_str().to_owned())
        })
        .transpose()
}

pub(crate) fn option_f64(cx: &mut Cx, options: &Options, key: &str) -> Result<Option<f64>> {
    options
        .get(key)
        .map(|value| {
            parse_number(cx, value, key)?
                .canonical
                .parse::<f64>()
                .map_err(|_| Error::Eval(format!("option :{key} must be f64")))
        })
        .transpose()
}

pub(crate) fn option_usize(cx: &mut Cx, options: &Options, key: &str) -> Result<Option<usize>> {
    options
        .get(key)
        .map(|value| {
            parse_number(cx, value, key)?
                .canonical
                .parse::<usize>()
                .map_err(|_| Error::Eval(format!("option :{key} must be a non-negative integer")))
        })
        .transpose()
}

pub(crate) fn option_u64(cx: &mut Cx, options: &Options, key: &str) -> Result<Option<u64>> {
    options
        .get(key)
        .map(|value| {
            parse_number(cx, value, key)?
                .canonical
                .parse::<u64>()
                .map_err(|_| Error::Eval(format!("option :{key} must be a non-negative integer")))
        })
        .transpose()
}

fn parse_number(cx: &mut Cx, value: &Value, name: &str) -> Result<NumberLiteral> {
    value
        .object()
        .as_number_value()
        .ok_or(Error::TypeMismatch {
            expected: "number",
            found: "non-number",
        })?
        .number_literal(cx)?
        .ok_or_else(|| Error::Eval(format!("signal value {name} has no numeric literal")))
}

fn value_list(
    cx: &mut Cx,
    value: &Value,
    name: &str,
    operation: SpectralOperation,
) -> Result<Vec<Value>> {
    if let Some(list) = value.object().as_list() {
        return force_list_to_vec(cx, list, &format!("{} {name}", operation.name()));
    }
    match value.object().as_expr(cx)? {
        Expr::List(items) | Expr::Block(items) => {
            items.into_iter().map(|item| cx.eval_expr(item)).collect()
        }
        expression => Err(Error::Eval(format!(
            "{} {name} must be a list, got {expression:?}",
            operation.name()
        ))),
    }
}

pub(crate) fn real_list(
    cx: &mut Cx,
    value: &Value,
    name: &str,
    operation: SpectralOperation,
) -> Result<Vec<f64>> {
    value_list(cx, value, name, operation)?
        .iter()
        .enumerate()
        .map(|(index, value)| {
            parse_number(cx, value, &format!("{name}[{index}]"))?
                .canonical
                .parse::<f64>()
                .map_err(|_| {
                    Error::Eval(format!("{} {name}[{index}] must be f64", operation.name()))
                })
        })
        .collect()
}

pub(crate) fn complex_list(
    cx: &mut Cx,
    value: &Value,
    name: &str,
    operation: SpectralOperation,
) -> Result<Vec<(f64, f64)>> {
    value_list(cx, value, name, operation)?
        .iter()
        .enumerate()
        .map(|(index, value)| {
            let pair = real_list(cx, value, &format!("{name}[{index}]"), operation)?;
            let [real, imag] = pair.as_slice() else {
                return Err(Error::Eval(format!(
                    "{} {name}[{index}] must contain real and imaginary components",
                    operation.name()
                )));
            };
            Ok((*real, *imag))
        })
        .collect()
}

pub(crate) fn complex_values(cx: &mut Cx, values: &[(f64, f64)]) -> Result<Value> {
    let values = values
        .iter()
        .map(|(real, imag)| {
            let real = f64_value(cx, *real)?;
            let imag = f64_value(cx, *imag)?;
            cx.factory().list(vec![real, imag])
        })
        .collect::<Result<Vec<_>>>()?;
    cx.factory().list(values)
}

pub(crate) fn u64_value(cx: &mut Cx, value: u64) -> Result<Value> {
    cx.factory()
        .number_literal(Symbol::qualified("numbers", "f64"), value.to_string())
}

pub(crate) fn parse_normalization(name: &str) -> Result<Normalization> {
    match name {
        "none" => Ok(Normalization::None),
        "forward" => Ok(Normalization::Forward),
        "inverse" => Ok(Normalization::Inverse),
        "orthonormal" | "unitary" => Ok(Normalization::Orthonormal),
        _ => Err(Error::Eval(format!("unsupported DFT normalization {name}"))),
    }
}

pub(crate) fn criterion_name(value: ArOrderCriterion) -> &'static str {
    match value {
        ArOrderCriterion::Fixed => "fixed",
        ArOrderCriterion::Akaike => "aic",
        ArOrderCriterion::Bayesian => "bic",
        ArOrderCriterion::FinalPredictionError => "fpe",
    }
}

pub(crate) fn termination_name(value: BurgTermination) -> &'static str {
    match value {
        BurgTermination::RequestedOrder => "requested-order",
        BurgTermination::SingularAt(_) => "singular-reduction",
        BurgTermination::UnstableAt(_) => "unstable-reduction",
    }
}

pub(crate) fn periodicity_name(value: Periodicity) -> &'static str {
    match value {
        Periodicity::Wrap => "wrap",
        Periodicity::PrincipalPeriod => "principal-period",
    }
}

pub(crate) fn endpoint_name(value: EndpointConvention) -> &'static str {
    match value {
        EndpointConvention::Excluded => "excluded",
        EndpointConvention::Included => "included",
    }
}

pub(crate) fn normalization_name(value: Normalization) -> &'static str {
    match value {
        Normalization::None => "none",
        Normalization::Forward => "forward",
        Normalization::Inverse => "inverse",
        Normalization::Orthonormal => "unitary",
    }
}

pub(crate) fn sign_name(value: SignConvention) -> &'static str {
    match value {
        SignConvention::NegativeForward => "negative-forward",
        SignConvention::PositiveForward => "positive-forward",
    }
}

pub(crate) fn nyquist_name(value: NyquistConvention) -> &'static str {
    match value {
        NyquistConvention::Positive => "positive",
        NyquistConvention::Negative => "negative",
    }
}