runmat-runtime 0.6.0

Core runtime for RunMat with builtins, BLAS/LAPACK integration, and execution APIs
Documentation
use runmat_builtins::{Tensor, Value};

use super::op_common::axes_target::AxesTarget;
use super::properties::{resolve_plot_handle, PlotHandle};
use super::state::{
    axis_display_bounds_snapshot, axis_display_bounds_snapshot_for_axes, axis_ticks_snapshot,
    axis_ticks_snapshot_for_axes, set_axis_ticks, set_axis_ticks_for_axes,
};
use super::{plotting_error, plotting_error_with_source};
use crate::BuiltinResult;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TickAxis {
    X,
    Y,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum TickMode {
    Auto,
    Manual,
}

pub fn axis_ticks_builtin(
    builtin: &'static str,
    axis: TickAxis,
    args: Vec<Value>,
) -> BuiltinResult<Value> {
    let (target, args) = split_optional_axes_target(builtin, args)?;
    match args.as_slice() {
        [] => query_ticks(builtin, axis, target),
        [value] => {
            if let Some(text) = value_as_string(value) {
                match text.trim().to_ascii_lowercase().as_str() {
                    "auto" => {
                        apply_mode(builtin, axis, target, TickMode::Auto)?;
                        return Ok(Value::String("auto".into()));
                    }
                    "manual" => {
                        apply_mode(builtin, axis, target, TickMode::Manual)?;
                        return Ok(Value::String("manual".into()));
                    }
                    "mode" => return query_mode(builtin, axis, target),
                    _ => {}
                }
            }
            let ticks = ticks_from_value(value, builtin)?;
            set_ticks(builtin, axis, target, ticks.clone())?;
            Ok(tick_value(ticks))
        }
        _ => Err(plotting_error(
            builtin,
            format!("{builtin}: expected zero or one tick argument"),
        )),
    }
}

fn split_optional_axes_target(
    builtin: &'static str,
    args: Vec<Value>,
) -> BuiltinResult<(AxesTarget, Vec<Value>)> {
    let mut iter = args.into_iter();
    let Some(first) = iter.next() else {
        return Ok((None, Vec::new()));
    };
    if let Ok(PlotHandle::Axes(handle, axes_index)) = resolve_plot_handle(&first, builtin) {
        return Ok((Some((handle, axes_index)), iter.collect()));
    }
    let mut rest = Vec::with_capacity(iter.size_hint().0 + 1);
    rest.push(first);
    rest.extend(iter);
    Ok((None, rest))
}

fn query_ticks(builtin: &'static str, axis: TickAxis, target: AxesTarget) -> BuiltinResult<Value> {
    let ticks = match target {
        Some((handle, axes_index)) => {
            let (x_ticks, y_ticks) =
                axis_ticks_snapshot_for_axes(handle, axes_index).map_err(|err| {
                    plotting_error_with_source(builtin, format!("{builtin}: {err}"), err)
                })?;
            let bounds =
                axis_display_bounds_snapshot_for_axes(handle, axes_index).map_err(|err| {
                    plotting_error_with_source(builtin, format!("{builtin}: {err}"), err)
                })?;
            match axis {
                TickAxis::X => ticks_or_auto(x_ticks.as_deref(), axis_bounds(bounds, axis)),
                TickAxis::Y => ticks_or_auto(y_ticks.as_deref(), axis_bounds(bounds, axis)),
            }
        }
        None => {
            let (x_ticks, y_ticks) = axis_ticks_snapshot();
            let bounds = axis_display_bounds_snapshot();
            match axis {
                TickAxis::X => ticks_or_auto(x_ticks.as_deref(), axis_bounds(bounds, axis)),
                TickAxis::Y => ticks_or_auto(y_ticks.as_deref(), axis_bounds(bounds, axis)),
            }
        }
    };
    Ok(tick_value(ticks))
}

fn query_mode(builtin: &'static str, axis: TickAxis, target: AxesTarget) -> BuiltinResult<Value> {
    let manual = match target {
        Some((handle, axes_index)) => {
            let (x_ticks, y_ticks) =
                axis_ticks_snapshot_for_axes(handle, axes_index).map_err(|err| {
                    plotting_error_with_source(builtin, format!("{builtin}: {err}"), err)
                })?;
            match axis {
                TickAxis::X => x_ticks.is_some(),
                TickAxis::Y => y_ticks.is_some(),
            }
        }
        None => {
            let (x_ticks, y_ticks) = axis_ticks_snapshot();
            match axis {
                TickAxis::X => x_ticks.is_some(),
                TickAxis::Y => y_ticks.is_some(),
            }
        }
    };
    Ok(Value::String(if manual { "manual" } else { "auto" }.into()))
}

fn apply_mode(
    builtin: &'static str,
    axis: TickAxis,
    target: AxesTarget,
    mode: TickMode,
) -> BuiltinResult<()> {
    let current = match mode {
        TickMode::Auto => None,
        TickMode::Manual => match target {
            Some((handle, axes_index)) => {
                let (x_ticks, y_ticks) =
                    axis_ticks_snapshot_for_axes(handle, axes_index).map_err(|err| {
                        plotting_error_with_source(builtin, format!("{builtin}: {err}"), err)
                    })?;
                let bounds =
                    axis_display_bounds_snapshot_for_axes(handle, axes_index).map_err(|err| {
                        plotting_error_with_source(builtin, format!("{builtin}: {err}"), err)
                    })?;
                Some(match axis {
                    TickAxis::X => ticks_or_auto(x_ticks.as_deref(), axis_bounds(bounds, axis)),
                    TickAxis::Y => ticks_or_auto(y_ticks.as_deref(), axis_bounds(bounds, axis)),
                })
            }
            None => Some({
                let bounds = axis_display_bounds_snapshot();
                match axis {
                    TickAxis::X => {
                        let (x_ticks, _) = axis_ticks_snapshot();
                        ticks_or_auto(x_ticks.as_deref(), axis_bounds(bounds, axis))
                    }
                    TickAxis::Y => {
                        let (_, y_ticks) = axis_ticks_snapshot();
                        ticks_or_auto(y_ticks.as_deref(), axis_bounds(bounds, axis))
                    }
                }
            }),
        },
    };
    match current {
        Some(ticks) => set_ticks(builtin, axis, target, ticks),
        None => clear_ticks(builtin, axis, target),
    }
}

fn set_ticks(
    builtin: &'static str,
    axis: TickAxis,
    target: AxesTarget,
    ticks: Vec<f64>,
) -> BuiltinResult<()> {
    match target {
        Some((handle, axes_index)) => {
            let (x_ticks, y_ticks) =
                axis_ticks_snapshot_for_axes(handle, axes_index).map_err(|err| {
                    plotting_error_with_source(builtin, format!("{builtin}: {err}"), err)
                })?;
            let (x, y) = match axis {
                TickAxis::X => (Some(ticks), y_ticks),
                TickAxis::Y => (x_ticks, Some(ticks)),
            };
            set_axis_ticks_for_axes(handle, axes_index, x, y).map_err(|err| {
                plotting_error_with_source(builtin, format!("{builtin}: {err}"), err)
            })
        }
        None => {
            let (x_ticks, y_ticks) = axis_ticks_snapshot();
            let (x, y) = match axis {
                TickAxis::X => (Some(ticks), y_ticks),
                TickAxis::Y => (x_ticks, Some(ticks)),
            };
            set_axis_ticks(x, y);
            Ok(())
        }
    }
}

fn clear_ticks(builtin: &'static str, axis: TickAxis, target: AxesTarget) -> BuiltinResult<()> {
    match target {
        Some((handle, axes_index)) => {
            let (x_ticks, y_ticks) =
                axis_ticks_snapshot_for_axes(handle, axes_index).map_err(|err| {
                    plotting_error_with_source(builtin, format!("{builtin}: {err}"), err)
                })?;
            let (x, y) = match axis {
                TickAxis::X => (None, y_ticks),
                TickAxis::Y => (x_ticks, None),
            };
            set_axis_ticks_for_axes(handle, axes_index, x, y).map_err(|err| {
                plotting_error_with_source(builtin, format!("{builtin}: {err}"), err)
            })
        }
        None => {
            let (x_ticks, y_ticks) = axis_ticks_snapshot();
            let (x, y) = match axis {
                TickAxis::X => (None, y_ticks),
                TickAxis::Y => (x_ticks, None),
            };
            set_axis_ticks(x, y);
            Ok(())
        }
    }
}

fn ticks_from_value(value: &Value, builtin: &'static str) -> BuiltinResult<Vec<f64>> {
    let tensor =
        Tensor::try_from(value).map_err(|e| plotting_error(builtin, format!("{builtin}: {e}")))?;
    let ticks = tensor.data;
    if ticks.iter().any(|value| !value.is_finite()) {
        return Err(plotting_error(
            builtin,
            format!("{builtin}: tick values must be finite"),
        ));
    }
    if ticks.windows(2).any(|pair| pair[1] <= pair[0]) {
        return Err(plotting_error(
            builtin,
            format!("{builtin}: tick values must be strictly increasing"),
        ));
    }
    Ok(ticks)
}

fn axis_bounds(bounds: Option<(f64, f64, f64, f64)>, axis: TickAxis) -> Option<(f64, f64)> {
    bounds.map(|(x_min, x_max, y_min, y_max)| match axis {
        TickAxis::X => (x_min, x_max),
        TickAxis::Y => (y_min, y_max),
    })
}

fn ticks_or_auto(explicit: Option<&[f64]>, bounds: Option<(f64, f64)>) -> Vec<f64> {
    if let Some(ticks) = explicit {
        return ticks.to_vec();
    }
    let (lo, hi) = bounds.unwrap_or((-1.0, 1.0));
    runmat_plot::core::plot_utils::generate_major_ticks(lo, hi)
}

fn tick_value(data: Vec<f64>) -> Value {
    Value::Tensor(Tensor {
        rows: 1,
        cols: data.len(),
        shape: vec![1, data.len()],
        data,
        integer_data: None,
        dtype: runmat_builtins::NumericDType::F64,
    })
}

fn value_as_string(value: &Value) -> Option<String> {
    match value {
        Value::String(s) => Some(s.clone()),
        Value::CharArray(chars) => Some(chars.data.iter().collect()),
        _ => None,
    }
}