rhai-sci 0.4.0

Scientific computing in the Rhai scripting language
Documentation
use rhai::{packages::Package, Array, Dynamic, Engine, EvalAltResult};
use rhai_sci::SciPackage;

#[test]
fn constructors_preserve_values_and_convert_vector_orientation() {
    for script in ["row([1, 2.5, 3])", "row(col([1, 2.5, 3]))"] {
        assert_matrix_eq(eval_array(script).unwrap(), &[&[1.0, 2.5, 3.0]]);
    }
    for script in ["col([1, 2.5, 3])", "col(row([1, 2.5, 3]))"] {
        assert_matrix_eq(eval_array(script).unwrap(), &[&[1.0], &[2.5], &[3.0]]);
    }
    let values = eval_array("mat([[1, 2.5]])").unwrap()[0]
        .clone()
        .into_array()
        .unwrap();
    assert!(values[0].is_int());
    assert!(values[1].is_float());
}

#[test]
fn dot_is_a_scalar_inner_product_independent_of_orientation() {
    let mut engine = Engine::new();
    engine.register_global_module(SciPackage::new().as_shared_module());
    for left in ["[1, 2]", "row([1, 2])", "col([1, 2])"] {
        for right in ["[3, 4.0]", "row([3, 4.0])", "col([3, 4.0])"] {
            let value = engine
                .eval::<f64>(&format!("dot({left}, {right})"))
                .unwrap();
            assert_eq!(value, 11.0);
        }
    }
    assert_eq!(
        engine.eval::<f64>("col([1, 2]).dot(row([3, 4]))").unwrap(),
        11.0
    );
    for script in [
        "dot([1], [1, 2])",
        "dot([], [])",
        "dot([[1, 2], [3, 4]], [1, 2])",
        "dot([1, \"x\"], [1, 2])",
    ] {
        assert!(engine.eval::<Dynamic>(script).is_err(), "{script}");
    }
}

#[cfg(feature = "nalgebra")]
#[test]
fn matrix_products_and_concatenation_compose_with_constructors() {
    assert_matrix_eq(
        eval_array("mtimes(mat([[1, 2], [3, 4]]), col([5, 6]))").unwrap(),
        &[&[17.0], &[39.0]],
    );
    assert_matrix_eq(
        eval_array("transpose(row([1, 2]))").unwrap(),
        &[&[1.0], &[2.0]],
    );
    assert_matrix_eq(
        eval_array("transpose(col([1, 2]))").unwrap(),
        &[&[1.0, 2.0]],
    );
    assert_matrix_eq(
        eval_array("transpose(transpose(col([1, 2])))").unwrap(),
        &[&[1.0], &[2.0]],
    );
    assert_matrix_eq(
        eval_array("mtimes(row([1, 2]), col([3, 4]))").unwrap(),
        &[&[11.0]],
    );
    assert_matrix_eq(
        eval_array("horzcat(mat([[1, 2], [3, 4]]), col([5, 6]))").unwrap(),
        &[&[1.0, 2.0, 5.0], &[3.0, 4.0, 6.0]],
    );
    assert_matrix_eq(
        eval_array("vertcat(mat([[1, 2], [3, 4]]), row([5, 6]))").unwrap(),
        &[&[1.0, 2.0], &[3.0, 4.0], &[5.0, 6.0]],
    );
    assert_error_contains("horzcat(row([1, 2]), col([3, 4]))", "same number of rows");
    assert_error_contains(
        "vertcat(row([1, 2]), col([3, 4]))",
        "same number of columns",
    );
    assert_error_contains("mtimes(col([1, 2]), col([3, 4]))", "not compatible");
    assert_error_contains(
        "let A = mat([[1, 2], [3, 4]]); A[1] = [3]; mtimes(A, col([1, 2]))",
        "matrix",
    );
}

#[test]
fn constructors_reject_invalid_shapes_and_values() {
    assert_error_contains("mat([[1, 2], [3]])", "equal length");
    assert_error_contains("mat([[1, \"x\"]])", "INT or FLOAT");
    assert_error_contains("mat([[]])", "nonempty");
    assert_error_contains("mat([])", "nonempty");
    assert_error_contains("row([])", "at least one value");
    assert_error_contains("col([])", "at least one value");
    for constructor in ["row", "col"] {
        assert_error_contains(&format!("{constructor}([[1], [2, 3]])"), "vector");
        assert_error_contains(&format!("{constructor}([[1, 2], [3, 4]])"), "vector");
    }
}

#[test]
fn shape_predicates_check_every_row() {
    let mut engine = Engine::new();
    engine.register_global_module(SciPackage::new().as_shared_module());
    engine
        .run(
            r#"
        assert_eq(is_matrix([[1], [2, 3]]), false);
        assert_eq(is_column_vector([[1], [2, 3]]), false);
        assert_eq(is_matrix([[1, 2], [3], [4, 5, 6]]), false);
        assert_eq(is_row_vector([[[1, 2]]]), false);
        assert_eq(is_numeric_list(row([1, 2.5])), true);
        assert_eq(is_numeric_list(col([1, 2.5])), true);
    "#,
        )
        .unwrap();
}

fn eval_array(script: &str) -> Result<Array, Box<EvalAltResult>> {
    let mut engine = Engine::new();
    engine.register_global_module(SciPackage::new().as_shared_module());
    engine.eval::<Array>(script)
}

fn assert_error_contains(script: &str, expected: &str) {
    let err = eval_array(script).unwrap_err();
    match err.as_ref() {
        EvalAltResult::ErrorArithmetic(message, _) => {
            assert!(
                message.contains(expected),
                "expected error message `{message}` to contain `{expected}`"
            );
        }
        other => panic!("unexpected error: {other:?}"),
    }
}

fn assert_matrix_eq(actual: Array, expected: &[&[f64]]) {
    let actual = numeric_matrix(actual);
    let expected = expected
        .iter()
        .map(|row| row.to_vec())
        .collect::<Vec<Vec<f64>>>();
    assert_eq!(actual, expected);
}

fn numeric_matrix(matrix: Array) -> Vec<Vec<f64>> {
    matrix
        .into_iter()
        .map(|row| {
            row.into_array()
                .expect("matrix rows should be arrays")
                .into_iter()
                .map(|value| {
                    if value.is_float() {
                        value.as_float().expect("value should be FLOAT")
                    } else {
                        value.as_int().expect("value should be INT") as f64
                    }
                })
                .collect()
        })
        .collect()
}