runmat-runtime 0.6.0

Core runtime for RunMat with builtins, BLAS/LAPACK integration, and execution APIs
Documentation
//! Least-squares line plotting compatibility helper.

use runmat_builtins::{
    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor, Type,
    Value,
};
use runmat_macros::runtime_builtin;

use crate::builtins::plotting::properties::{resolve_plot_handle, PlotHandle};
use crate::{build_runtime_error, BuiltinResult, RuntimeError};

const NAME: &str = "lsline";

const PARAM_AX: BuiltinParamDescriptor = BuiltinParamDescriptor {
    name: "ax",
    ty: BuiltinParamType::AxesHandle,
    arity: BuiltinParamArity::Required,
    default: None,
    description: "Target axes handle.",
};

const OUTPUT_HANDLE: BuiltinParamDescriptor = BuiltinParamDescriptor {
    name: "h",
    ty: BuiltinParamType::NumericArray,
    arity: BuiltinParamArity::Required,
    default: None,
    description: "Line graphics handle or vector of line handles.",
};

const INPUTS_EMPTY: [BuiltinParamDescriptor; 0] = [];
const INPUTS_AX: [BuiltinParamDescriptor; 1] = [PARAM_AX];
const OUTPUTS_HANDLE: [BuiltinParamDescriptor; 1] = [OUTPUT_HANDLE];

const SIGNATURES: [BuiltinSignatureDescriptor; 4] = [
    BuiltinSignatureDescriptor {
        label: "lsline()",
        inputs: &INPUTS_EMPTY,
        outputs: &[],
    },
    BuiltinSignatureDescriptor {
        label: "lsline(ax)",
        inputs: &INPUTS_AX,
        outputs: &[],
    },
    BuiltinSignatureDescriptor {
        label: "h = lsline()",
        inputs: &INPUTS_EMPTY,
        outputs: &OUTPUTS_HANDLE,
    },
    BuiltinSignatureDescriptor {
        label: "h = lsline(ax)",
        inputs: &INPUTS_AX,
        outputs: &OUTPUTS_HANDLE,
    },
];

pub const LSLINE_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
    code: "RM.LSLINE.INVALID_ARGUMENT",
    identifier: Some("RunMat:lsline:InvalidArgument"),
    when: "The optional axes handle is malformed or extra arguments are supplied.",
    message: "lsline: invalid argument",
};

pub const LSLINE_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
    code: "RM.LSLINE.INTERNAL",
    identifier: Some("RunMat:lsline:Internal"),
    when: "RunMat cannot construct or register the least-squares line.",
    message: "lsline: internal error",
};

const ERRORS: [BuiltinErrorDescriptor; 2] = [LSLINE_ERROR_INVALID_ARGUMENT, LSLINE_ERROR_INTERNAL];

pub const LSLINE_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
    signatures: &SIGNATURES,
    output_mode: BuiltinOutputMode::Fixed,
    completion_policy: BuiltinCompletionPolicy::Public,
    errors: &ERRORS,
};

fn lsline_type(_args: &[Type], _ctx: &runmat_builtins::ResolveContext) -> Type {
    Type::Unknown
}

fn error(descriptor: &'static BuiltinErrorDescriptor, message: impl Into<String>) -> RuntimeError {
    let mut builder = build_runtime_error(message).with_builtin(NAME);
    if let Some(identifier) = descriptor.identifier {
        builder = builder.with_identifier(identifier);
    }
    builder.build()
}

fn invalid_argument(message: impl Into<String>) -> RuntimeError {
    error(&LSLINE_ERROR_INVALID_ARGUMENT, message)
}

fn internal_error(message: impl Into<String>) -> RuntimeError {
    error(&LSLINE_ERROR_INTERNAL, message)
}

fn validate_lsline_args(args: &[Value]) -> BuiltinResult<()> {
    match args {
        [] => Ok(()),
        [arg] => match resolve_plot_handle(arg, NAME) {
            Ok(PlotHandle::Axes(_, _)) => Ok(()),
            Ok(_) => Err(invalid_argument("lsline: expected an axes handle")),
            Err(err) => {
                if err.identifier().is_some() {
                    Err(invalid_argument("lsline: expected an axes handle"))
                } else {
                    Err(invalid_argument(err.message))
                }
            }
        },
        _ => Err(invalid_argument(
            "lsline: expected zero inputs or one axes handle",
        )),
    }
}

fn map_refline_error(err: RuntimeError) -> RuntimeError {
    let message = err.message.replace("refline", "lsline");
    if err
        .identifier()
        .is_some_and(|id| id.contains("InvalidArgument"))
    {
        invalid_argument(message)
    } else {
        internal_error(message)
    }
}

#[runtime_builtin(
    name = "lsline",
    category = "stats/summary",
    summary = "Add least-squares fit lines to scatter-style plots.",
    keywords = "lsline,least squares,statistics,plotting",
    sink = true,
    suppress_auto_output = true,
    type_resolver(lsline_type),
    descriptor(crate::builtins::stats::summary::lsline::LSLINE_DESCRIPTOR),
    builtin_path = "crate::builtins::stats::summary::lsline"
)]
pub(crate) async fn lsline_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
    validate_lsline_args(&args)?;
    super::refline::refline_builtin(args)
        .await
        .map_err(map_refline_error)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::builtins::plotting::get::get_builtin;
    use crate::builtins::plotting::scatter::scatter_builtin;
    use crate::builtins::plotting::state::{encode_axes_handle, PlotTestLockGuard};
    use crate::builtins::plotting::tests::{ensure_plot_test_env, lock_plot_registry};
    use crate::builtins::plotting::{
        clear_figure, clone_figure, configure_subplot, current_figure_handle,
        reset_hold_state_for_run,
    };
    use futures::executor::block_on;
    use runmat_builtins::Tensor;

    fn setup() -> PlotTestLockGuard {
        let guard = lock_plot_registry();
        ensure_plot_test_env();
        reset_hold_state_for_run();
        let _ = clear_figure(None);
        guard
    }

    fn tensor(data: Vec<f64>, rows: usize, cols: usize) -> Value {
        Value::Tensor(Tensor::new(data, vec![rows, cols]).unwrap())
    }

    fn x_data(handle: f64) -> Vec<f64> {
        let value = get_builtin(vec![Value::Num(handle), Value::String("XData".into())]).unwrap();
        Tensor::try_from(&value).unwrap().data
    }

    fn y_data(handle: f64) -> Vec<f64> {
        let value = get_builtin(vec![Value::Num(handle), Value::String("YData".into())]).unwrap();
        Tensor::try_from(&value).unwrap().data
    }

    #[test]
    fn lsline_adds_least_squares_line_to_current_axes() {
        let _guard = setup();
        block_on(scatter_builtin(
            tensor(vec![1.0, 2.0, 3.0], 1, 3),
            tensor(vec![2.0, 4.0, 6.0], 1, 3),
            Vec::new(),
        ))
        .unwrap();

        let handle = block_on(lsline_builtin(Vec::new())).unwrap();
        let Value::Num(handle) = handle else {
            panic!("expected line handle");
        };
        assert_eq!(x_data(handle), vec![1.0, 3.0]);
        assert_eq!(y_data(handle), vec![2.0, 6.0]);
    }

    #[test]
    fn lsline_targets_axes_and_returns_one_line_per_eligible_plot() {
        let _guard = setup();
        configure_subplot(1, 2, 0).unwrap();
        block_on(scatter_builtin(
            tensor(vec![100.0, 200.0], 1, 2),
            tensor(vec![10.0, 20.0], 1, 2),
            Vec::new(),
        ))
        .unwrap();
        configure_subplot(1, 2, 1).unwrap();
        block_on(scatter_builtin(
            tensor(vec![2.0, 4.0], 1, 2),
            tensor(vec![1.0, 5.0], 1, 2),
            Vec::new(),
        ))
        .unwrap();

        let fig = current_figure_handle();
        let ax = encode_axes_handle(fig, 1);
        let handle = block_on(lsline_builtin(vec![Value::Num(ax)])).unwrap();
        let Value::Num(handle) = handle else {
            panic!("expected line handle");
        };
        assert_eq!(x_data(handle), vec![2.0, 4.0]);
        assert_eq!(y_data(handle), vec![1.0, 5.0]);

        let figure = clone_figure(fig).unwrap();
        assert_eq!(figure.len(), 3);
    }

    #[test]
    fn lsline_rejects_extra_arguments() {
        let _guard = setup();
        let err = block_on(lsline_builtin(vec![Value::Num(1.0), Value::Num(2.0)]))
            .expect_err("extra arguments should fail");
        assert_eq!(err.identifier(), LSLINE_ERROR_INVALID_ARGUMENT.identifier);
    }

    #[test]
    fn lsline_descriptor_signatures_cover_matlab_surface() {
        let labels: Vec<&str> = LSLINE_DESCRIPTOR
            .signatures
            .iter()
            .map(|sig| sig.label)
            .collect();
        assert!(labels.contains(&"lsline()"));
        assert!(labels.contains(&"lsline(ax)"));
        assert!(labels.contains(&"h = lsline()"));
        assert!(labels.contains(&"h = lsline(ax)"));
    }
}