use runmat_builtins::{
BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
Tensor, Value,
};
use runmat_macros::runtime_builtin;
use runmat_plot::gpu::errorbar::ErrorBarGpuInputs;
use runmat_plot::gpu::line::{
self, LineGpuInputs as MarkerGpuInputs, LineGpuParams as MarkerGpuParams,
};
use runmat_plot::gpu::ScalarType;
use runmat_plot::plots::{ErrorBar, LineMarkerAppearance};
use crate::builtins::common::spec::{
BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
ReductionNaN, ResidencyPolicy, ShapeRequirements,
};
use crate::builtins::plotting::type_resolvers::handle_scalar_type;
use crate::{build_runtime_error, RuntimeError};
use super::common::numeric_pair;
use super::gpu_helpers::gpu_errorbar_bounds;
use super::op_common::line_inputs::NumericInput;
use super::plotting_error;
use super::state::{render_active_plot, PlotRenderOptions};
use super::style::{marker_metadata_from_appearance, parse_line_style_args, LineStyleParseOptions};
const BUILTIN_NAME: &str = "errorbar";
type ErrorBarArgs = (
Option<usize>,
Value,
Value,
Option<Value>,
Option<Value>,
Value,
Value,
Vec<Value>,
);
const ERRORBAR_OUTPUT_HANDLE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
name: "h",
ty: BuiltinParamType::NumericScalar,
arity: BuiltinParamArity::Required,
default: None,
description: "Handle to the created error bar series.",
}];
const ERRORBAR_INPUTS_Y_ERR: [BuiltinParamDescriptor; 2] = [
BuiltinParamDescriptor {
name: "Y",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "Data values.",
},
BuiltinParamDescriptor {
name: "E",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "Symmetric Y error magnitudes.",
},
];
const ERRORBAR_INPUTS_X_Y_ERR: [BuiltinParamDescriptor; 3] = [
BuiltinParamDescriptor {
name: "X",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "X coordinates.",
},
BuiltinParamDescriptor {
name: "Y",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "Y coordinates.",
},
BuiltinParamDescriptor {
name: "E",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "Symmetric Y error magnitudes.",
},
];
const ERRORBAR_INPUTS_X_Y_YNEG_YPOS: [BuiltinParamDescriptor; 4] = [
BuiltinParamDescriptor {
name: "X",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "X coordinates.",
},
BuiltinParamDescriptor {
name: "Y",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "Y coordinates.",
},
BuiltinParamDescriptor {
name: "YNeg",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "Negative Y error magnitudes.",
},
BuiltinParamDescriptor {
name: "YPos",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "Positive Y error magnitudes.",
},
];
const ERRORBAR_INPUTS_X_Y_XNEG_XPOS_YNEG_YPOS: [BuiltinParamDescriptor; 6] = [
BuiltinParamDescriptor {
name: "X",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "X coordinates.",
},
BuiltinParamDescriptor {
name: "Y",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "Y coordinates.",
},
BuiltinParamDescriptor {
name: "XNeg",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "Negative X error magnitudes.",
},
BuiltinParamDescriptor {
name: "XPos",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "Positive X error magnitudes.",
},
BuiltinParamDescriptor {
name: "YNeg",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "Negative Y error magnitudes.",
},
BuiltinParamDescriptor {
name: "YPos",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "Positive Y error magnitudes.",
},
];
const ERRORBAR_INPUTS_AX_DATA_PROPS: [BuiltinParamDescriptor; 3] = [
BuiltinParamDescriptor {
name: "ax",
ty: BuiltinParamType::NumericScalar,
arity: BuiltinParamArity::Required,
default: None,
description: "Target axes handle.",
},
BuiltinParamDescriptor {
name: "data",
ty: BuiltinParamType::Any,
arity: BuiltinParamArity::Variadic,
default: None,
description: "Errorbar positional data arguments.",
},
BuiltinParamDescriptor {
name: "props",
ty: BuiltinParamType::Any,
arity: BuiltinParamArity::Variadic,
default: None,
description: "Style and name/value properties.",
},
];
const ERRORBAR_SIGNATURES: [BuiltinSignatureDescriptor; 5] = [
BuiltinSignatureDescriptor {
label: "h = errorbar(Y, E)",
inputs: &ERRORBAR_INPUTS_Y_ERR,
outputs: &ERRORBAR_OUTPUT_HANDLE,
},
BuiltinSignatureDescriptor {
label: "h = errorbar(X, Y, E)",
inputs: &ERRORBAR_INPUTS_X_Y_ERR,
outputs: &ERRORBAR_OUTPUT_HANDLE,
},
BuiltinSignatureDescriptor {
label: "h = errorbar(X, Y, YNeg, YPos)",
inputs: &ERRORBAR_INPUTS_X_Y_YNEG_YPOS,
outputs: &ERRORBAR_OUTPUT_HANDLE,
},
BuiltinSignatureDescriptor {
label: "h = errorbar(X, Y, XNeg, XPos, YNeg, YPos)",
inputs: &ERRORBAR_INPUTS_X_Y_XNEG_XPOS_YNEG_YPOS,
outputs: &ERRORBAR_OUTPUT_HANDLE,
},
BuiltinSignatureDescriptor {
label: "h = errorbar(ax, ...)",
inputs: &ERRORBAR_INPUTS_AX_DATA_PROPS,
outputs: &ERRORBAR_OUTPUT_HANDLE,
},
];
const ERRORBAR_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
code: "RM.ERRORBAR.INVALID_ARGUMENT",
identifier: Some("RunMat:errorbar:InvalidArgument"),
when: "Input vectors, error vectors, style arguments, or axes-target forms are malformed.",
message: "errorbar: invalid argument",
};
const ERRORBAR_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
code: "RM.ERRORBAR.INTERNAL",
identifier: Some("RunMat:errorbar:Internal"),
when: "Internal render preparation or GPU vertex generation fails.",
message: "errorbar: internal operation failed",
};
const ERRORBAR_ERRORS: [BuiltinErrorDescriptor; 2] =
[ERRORBAR_ERROR_INVALID_ARGUMENT, ERRORBAR_ERROR_INTERNAL];
pub const ERRORBAR_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
signatures: &ERRORBAR_SIGNATURES,
output_mode: BuiltinOutputMode::Fixed,
completion_policy: BuiltinCompletionPolicy::Public,
errors: &ERRORBAR_ERRORS,
};
fn errorbar_error_with_detail(
error: &'static BuiltinErrorDescriptor,
detail: impl AsRef<str>,
) -> RuntimeError {
let mut builder = build_runtime_error(format!("{}: {}", error.message, detail.as_ref()))
.with_builtin(BUILTIN_NAME);
if let Some(identifier) = error.identifier {
builder = builder.with_identifier(identifier);
}
builder.build()
}
fn map_errorbar_invalid(err: RuntimeError) -> RuntimeError {
if err.identifier().is_some() {
return err;
}
errorbar_error_with_detail(&ERRORBAR_ERROR_INVALID_ARGUMENT, err.message)
}
fn errorbar_invalid(detail: impl AsRef<str>) -> RuntimeError {
errorbar_error_with_detail(&ERRORBAR_ERROR_INVALID_ARGUMENT, detail)
}
#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::plotting::errorbar")]
pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
name: "errorbar",
op_kind: GpuOpKind::PlotRender,
supported_precisions: &[],
broadcast: BroadcastSemantics::None,
provider_hooks: &[],
constant_strategy: ConstantStrategy::InlineLiteral,
residency: ResidencyPolicy::InheritInputs,
nan_mode: ReductionNaN::Include,
two_pass_threshold: None,
workgroup_size: None,
accepts_nan_mode: false,
notes: "errorbar is a plotting sink; GPU inputs may remain on device when shared plotting context is installed.",
};
#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::plotting::errorbar")]
pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
name: "errorbar",
shape: ShapeRequirements::Any,
constant_strategy: ConstantStrategy::InlineLiteral,
elementwise: None,
reduction: None,
emits_nan: false,
notes: "errorbar performs rendering and terminates fusion graphs.",
};
#[runtime_builtin(
name = "errorbar",
category = "plotting",
summary = "Create plots with symmetric or asymmetric error bars.",
keywords = "errorbar,plotting,uncertainty",
sink = true,
suppress_auto_output = true,
type_resolver(handle_scalar_type),
descriptor(crate::builtins::plotting::errorbar::ERRORBAR_DESCRIPTOR),
builtin_path = "crate::builtins::plotting::errorbar"
)]
pub fn errorbar_builtin(args: Vec<Value>) -> crate::BuiltinResult<f64> {
let (target_axes, x, y, x_neg, x_pos, y_neg, y_pos, rest) =
parse_errorbar_args(args).map_err(map_errorbar_invalid)?;
let parsed = parse_errorbar_style_args(&rest).map_err(map_errorbar_invalid)?;
let mut x_in = Some(NumericInput::from_value(x, BUILTIN_NAME).map_err(map_errorbar_invalid)?);
let mut y_in = Some(NumericInput::from_value(y, BUILTIN_NAME).map_err(map_errorbar_invalid)?);
let mut xn_in = x_neg
.map(|v| NumericInput::from_value(v, BUILTIN_NAME))
.transpose()
.map_err(map_errorbar_invalid)?;
let mut xp_in = x_pos
.map(|v| NumericInput::from_value(v, BUILTIN_NAME))
.transpose()
.map_err(map_errorbar_invalid)?;
let mut n_in =
Some(NumericInput::from_value(y_neg, BUILTIN_NAME).map_err(map_errorbar_invalid)?);
let mut p_in =
Some(NumericInput::from_value(y_pos, BUILTIN_NAME).map_err(map_errorbar_invalid)?);
let opts = PlotRenderOptions {
title: "Error Bars",
x_label: "X",
y_label: "Y",
..Default::default()
};
let plot_index_out = std::rc::Rc::new(std::cell::RefCell::new(None));
let plot_index_slot = std::rc::Rc::clone(&plot_index_out);
let figure_handle = crate::builtins::plotting::current_figure_handle();
let render_result = render_active_plot(BUILTIN_NAME, opts, move |figure, axes| {
let axes = target_axes.unwrap_or(axes);
let x_arg = x_in.take().expect("x consumed");
let y_arg = y_in.take().expect("y consumed");
let yn_arg = n_in.take().expect("yn consumed");
let yp_arg = p_in.take().expect("yp consumed");
let label = parsed.label.clone().unwrap_or_else(|| "Data".into());
if let (Some(x_gpu), Some(y_gpu), Some(yn_gpu), Some(yp_gpu)) = (
x_arg.gpu_handle(),
y_arg.gpu_handle(),
yn_arg.gpu_handle(),
yp_arg.gpu_handle(),
) {
match build_errorbar_gpu_plot(
BUILTIN_NAME,
x_gpu,
y_gpu,
xn_in.as_ref().and_then(|v| v.gpu_handle()),
xp_in.as_ref().and_then(|v| v.gpu_handle()),
yn_gpu,
yp_gpu,
&parsed,
&label,
) {
Ok(plot) => {
let plot_index = figure.add_errorbar_on_axes(plot, axes);
*plot_index_slot.borrow_mut() = Some((axes, plot_index));
return Ok(());
}
Err(err) => log::warn!("errorbar GPU path unavailable: {err}"),
}
}
let x = x_arg
.into_tensor(BUILTIN_NAME)
.map_err(map_errorbar_invalid)?;
let y = y_arg
.into_tensor(BUILTIN_NAME)
.map_err(map_errorbar_invalid)?;
let xn = xn_in
.take()
.map(|v| v.into_tensor(BUILTIN_NAME))
.transpose()
.map_err(map_errorbar_invalid)?;
let xp = xp_in
.take()
.map(|v| v.into_tensor(BUILTIN_NAME))
.transpose()
.map_err(map_errorbar_invalid)?;
let yn = yn_arg
.into_tensor(BUILTIN_NAME)
.map_err(map_errorbar_invalid)?;
let yp = yp_arg
.into_tensor(BUILTIN_NAME)
.map_err(map_errorbar_invalid)?;
let (x, y) = numeric_pair(x, y, BUILTIN_NAME).map_err(map_errorbar_invalid)?;
let (yn, yp) = numeric_pair(yn, yp, BUILTIN_NAME).map_err(map_errorbar_invalid)?;
let mut plot = if let (Some(xn), Some(xp)) = (xn, xp) {
let (xn, xp) = numeric_pair(xn, xp, BUILTIN_NAME).map_err(map_errorbar_invalid)?;
ErrorBar::new_both(x, y, xn, xp, yn, yp)
} else {
ErrorBar::new_vertical(x, y, yn, yp)
}
.map_err(|e| errorbar_invalid(&e))?
.with_style(
parsed.color,
parsed.line_width,
parsed.line_style,
parsed.cap_size,
)
.with_label(label);
if let Some(marker) = parsed.marker.clone() {
plot.set_marker(Some(marker));
}
let plot_index = figure.add_errorbar_on_axes(plot, axes);
*plot_index_slot.borrow_mut() = Some((axes, plot_index));
Ok(())
});
let Some((axes, plot_index)) = *plot_index_out.borrow() else {
return render_result.map(|_| f64::NAN);
};
let handle =
crate::builtins::plotting::state::register_errorbar_handle(figure_handle, axes, plot_index);
if let Err(err) = render_result {
let lower = err.to_string().to_lowercase();
if lower.contains("plotting is unavailable") || lower.contains("non-main thread") {
return Ok(handle);
}
return Err(map_errorbar_invalid(err));
}
Ok(handle)
}
fn build_errorbar_gpu_plot(
name: &'static str,
x: &runmat_accelerate_api::GpuTensorHandle,
y: &runmat_accelerate_api::GpuTensorHandle,
x_neg: Option<&runmat_accelerate_api::GpuTensorHandle>,
x_pos: Option<&runmat_accelerate_api::GpuTensorHandle>,
y_neg: &runmat_accelerate_api::GpuTensorHandle,
y_pos: &runmat_accelerate_api::GpuTensorHandle,
parsed: &ParsedErrorBarStyle,
label: &str,
) -> crate::BuiltinResult<ErrorBar> {
let context = super::gpu_helpers::ensure_shared_wgpu_context(name)?;
let x_ref = runmat_accelerate_api::export_wgpu_buffer(x)
.ok_or_else(|| plotting_error(name, format!("{name}: unable to export GPU X data")))?;
let y_ref = runmat_accelerate_api::export_wgpu_buffer(y)
.ok_or_else(|| plotting_error(name, format!("{name}: unable to export GPU Y data")))?;
let xn_ref = x_neg.and_then(runmat_accelerate_api::export_wgpu_buffer);
let xp_ref = x_pos.and_then(runmat_accelerate_api::export_wgpu_buffer);
let yn_ref = runmat_accelerate_api::export_wgpu_buffer(y_neg).ok_or_else(|| {
plotting_error(
name,
format!("{name}: unable to export GPU negative error data"),
)
})?;
let yp_ref = runmat_accelerate_api::export_wgpu_buffer(y_pos).ok_or_else(|| {
plotting_error(
name,
format!("{name}: unable to export GPU positive error data"),
)
})?;
if x_ref.len != y_ref.len
|| x_ref.len != yn_ref.len
|| x_ref.len != yp_ref.len
|| xn_ref.as_ref().map(|r| r.len).unwrap_or(x_ref.len) != x_ref.len
|| xp_ref.as_ref().map(|r| r.len).unwrap_or(x_ref.len) != x_ref.len
{
return Err(plotting_error(
name,
format!("{name}: X, Y, and error inputs must have identical lengths"),
));
}
if x_ref.precision != y_ref.precision
|| x_ref.precision != yn_ref.precision
|| x_ref.precision != yp_ref.precision
|| xn_ref
.as_ref()
.map(|r| r.precision)
.unwrap_or(x_ref.precision)
!= x_ref.precision
|| xp_ref
.as_ref()
.map(|r| r.precision)
.unwrap_or(x_ref.precision)
!= x_ref.precision
{
return Err(plotting_error(
name,
format!("{name}: gpuArray precision must match across all errorbar inputs"),
));
}
let scalar =
ScalarType::from_is_f64(x_ref.precision == runmat_accelerate_api::ProviderPrecision::F64);
let bounds = if let (Some(xn), Some(xp)) = (x_neg, x_pos) {
let mut b = gpu_errorbar_bounds(x, y, y_neg, y_pos, name)?;
let (min_xn, max_xn) = super::gpu_helpers::axis_bounds(xn, name)?;
let (min_xp, max_xp) = super::gpu_helpers::axis_bounds(xp, name)?;
b.min.x -= max_xn.max(min_xn.abs());
b.max.x += max_xp.max(min_xp.abs());
b
} else {
gpu_errorbar_bounds(x, y, y_neg, y_pos, name)?
};
let inputs = ErrorBarGpuInputs {
x_buffer: x_ref.buffer.clone(),
y_buffer: y_ref.buffer.clone(),
x_neg_buffer: xn_ref.as_ref().map(|r| r.buffer.clone()),
x_pos_buffer: xp_ref.as_ref().map(|r| r.buffer.clone()),
y_neg_buffer: yn_ref.buffer.clone(),
y_pos_buffer: yp_ref.buffer.clone(),
len: x_ref.len as u32,
scalar,
};
let mut plot = ErrorBar::from_gpu_inputs(
parsed.color,
parsed.line_width,
parsed.line_style,
parsed.cap_size,
if x_neg.is_some() && x_pos.is_some() {
runmat_plot::plots::errorbar::ErrorBarOrientation::Both
} else {
runmat_plot::plots::errorbar::ErrorBarOrientation::Vertical
},
inputs,
bounds,
)
.with_label(label);
if let Some(marker) = parsed.marker.clone() {
let marker_gpu = line::pack_marker_vertices_from_xy(
&context.device,
&context.queue,
&MarkerGpuInputs {
x_buffer: x_ref.buffer.clone(),
y_buffer: y_ref.buffer.clone(),
len: x_ref.len as u32,
scalar,
},
&MarkerGpuParams {
color: marker.face_color,
half_width_px: 0.0,
viewport_width_px: 1.0,
viewport_height_px: 1.0,
x_min: 0.0,
x_span: 1.0,
y_min: 0.0,
y_span: 1.0,
line_style: runmat_plot::plots::LineStyle::Solid,
marker_size: marker.size,
},
)
.map_err(|e| {
plotting_error(
name,
format!("{name}: failed to build marker vertices: {e}"),
)
})?;
plot.set_marker(Some(marker));
plot.set_marker_gpu_vertices(Some(marker_gpu));
}
Ok(plot)
}
struct ParsedErrorBarStyle {
color: glam::Vec4,
line_width: f32,
line_style: runmat_plot::plots::LineStyle,
marker: Option<LineMarkerAppearance>,
label: Option<String>,
cap_size: f32,
}
fn parse_errorbar_style_args(args: &[Value]) -> crate::BuiltinResult<ParsedErrorBarStyle> {
let mut filtered = Vec::new();
let mut cap_size = 6.0;
let mut idx = 0usize;
while idx < args.len() {
if let Some(key) = super::style::value_as_string(&args[idx]) {
if key.trim().eq_ignore_ascii_case("CapSize") && idx + 1 < args.len() {
cap_size = super::style::value_as_f64(&args[idx + 1]).ok_or_else(|| {
plotting_error(BUILTIN_NAME, "errorbar: CapSize must be numeric")
})? as f32;
idx += 2;
continue;
}
}
filtered.push(args[idx].clone());
idx += 1;
}
let parsed = parse_line_style_args(&filtered, &LineStyleParseOptions::generic(BUILTIN_NAME))?;
let marker = marker_metadata_from_appearance(&parsed.appearance);
Ok(ParsedErrorBarStyle {
color: parsed.appearance.color,
line_width: parsed.appearance.line_width,
line_style: parsed.appearance.line_style,
marker,
label: parsed.label,
cap_size,
})
}
fn parse_errorbar_args(args: Vec<Value>) -> crate::BuiltinResult<ErrorBarArgs> {
if args.len() < 2 {
return Err(plotting_error(
BUILTIN_NAME,
"errorbar: expected at least y and error inputs",
));
}
let mut it = args.into_iter();
let mut target_axes = None;
let first = it.next().unwrap();
let first = if let Ok(crate::builtins::plotting::properties::PlotHandle::Axes(_, axes)) =
crate::builtins::plotting::properties::resolve_plot_handle(&first, BUILTIN_NAME)
{
target_axes = Some(axes);
it.next().ok_or_else(|| {
plotting_error(BUILTIN_NAME, "errorbar: expected data after axes handle")
})?
} else {
first
};
let second = it.next().unwrap();
let third = it.next();
let fourth = it.next();
match (third, fourth) {
(None, _) => {
let y = first;
let err = second;
let x = infer_errorbar_x_from_y(&y)?;
Ok((target_axes, x, y, None, None, err.clone(), err, Vec::new()))
}
(Some(third), None) => {
if is_styleish(&third) {
let y = first;
let err = second;
let x = infer_errorbar_x_from_y(&y)?;
Ok((target_axes, x, y, None, None, err.clone(), err, vec![third]))
} else {
Ok((
target_axes,
first,
second,
None,
None,
third.clone(),
third,
Vec::new(),
))
}
}
(Some(third), Some(fourth)) => {
if is_styleish(&third) {
let y = first;
let err = second;
let x = infer_errorbar_x_from_y(&y)?;
let mut rest = vec![third, fourth];
rest.extend(it);
Ok((target_axes, x, y, None, None, err.clone(), err, rest))
} else if is_styleish(&fourth) {
let mut rest = vec![fourth];
rest.extend(it);
Ok((
target_axes,
first,
second,
None,
None,
third.clone(),
third,
rest,
))
} else {
let rest = it.collect::<Vec<_>>();
match rest.as_slice() {
[] => Ok((target_axes, first, second, None, None, third, fourth, rest)),
[fifth, ..] if is_styleish(fifth) => {
Ok((target_axes, first, second, None, None, third, fourth, rest))
}
[fifth, sixth, tail @ ..] if is_numericish(fifth) && is_numericish(sixth) => {
Ok((
target_axes,
first,
second,
Some(fifth.clone()),
Some(sixth.clone()),
third,
fourth,
tail.to_vec(),
))
}
[fifth, ..] if is_numericish(fifth) => Err(plotting_error(
BUILTIN_NAME,
"errorbar: expected positive X error data after negative X error data",
)),
_ => Ok((target_axes, first, second, None, None, third, fourth, rest)),
}
}
}
}
}
fn is_styleish(value: &Value) -> bool {
matches!(value, Value::String(_) | Value::CharArray(_))
}
fn is_numericish(value: &Value) -> bool {
matches!(
value,
Value::Tensor(_) | Value::GpuTensor(_) | Value::Num(_) | Value::Int(_) | Value::Bool(_)
)
}
fn infer_errorbar_x_from_y(y: &Value) -> crate::BuiltinResult<Value> {
let len = Tensor::try_from(y)
.map_err(|e| plotting_error(BUILTIN_NAME, format!("errorbar: {e}")))?
.data
.len();
Ok(Value::Tensor(Tensor {
data: (1..=len).map(|i| i as f64).collect(),
shape: vec![len],
rows: len,
cols: 1,
dtype: runmat_builtins::NumericDType::F64,
}))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::builtins::plotting::get::get_builtin;
use crate::builtins::plotting::set::set_builtin;
use crate::builtins::plotting::tests::{ensure_plot_test_env, lock_plot_registry};
use crate::builtins::plotting::{
clear_figure, clone_figure, current_figure_handle, reset_hold_state_for_run,
subplot::subplot_builtin,
};
use runmat_plot::plots::PlotElement;
fn vec_tensor(data: &[f64]) -> Tensor {
Tensor {
data: data.to_vec(),
shape: vec![data.len()],
rows: data.len(),
cols: 1,
dtype: runmat_builtins::NumericDType::F64,
}
}
#[test]
fn errorbar_builds_vertical_plot() {
let _guard = lock_plot_registry();
ensure_plot_test_env();
reset_hold_state_for_run();
let _ = clear_figure(None);
let _ = errorbar_builtin(vec![
Value::Tensor(vec_tensor(&[1.0, 2.0])),
Value::Tensor(vec_tensor(&[0.1, 0.2])),
]);
let fig = clone_figure(current_figure_handle()).unwrap();
assert!(matches!(
fig.plots().next().unwrap(),
PlotElement::ErrorBar(_)
));
}
#[test]
fn errorbar_supports_axes_target_and_capsize() {
let _guard = lock_plot_registry();
ensure_plot_test_env();
reset_hold_state_for_run();
let _ = clear_figure(None);
let ax = subplot_builtin(Value::Num(1.0), Value::Num(2.0), Value::Num(2.0)).unwrap();
let _ = errorbar_builtin(vec![
Value::Num(ax),
Value::Tensor(vec_tensor(&[1.0, 2.0])),
Value::Tensor(vec_tensor(&[3.0, 4.0])),
Value::Tensor(vec_tensor(&[0.2, 0.3])),
Value::String("CapSize".into()),
Value::Num(10.0),
]);
let fig = clone_figure(current_figure_handle()).unwrap();
assert_eq!(fig.plot_axes_indices()[0], 1);
}
#[test]
fn errorbar_supports_both_direction_form() {
let _guard = lock_plot_registry();
ensure_plot_test_env();
reset_hold_state_for_run();
let _ = clear_figure(None);
let _ = errorbar_builtin(vec![
Value::Tensor(vec_tensor(&[1.0, 2.0])),
Value::Tensor(vec_tensor(&[3.0, 4.0])),
Value::Tensor(vec_tensor(&[0.2, 0.3])),
Value::Tensor(vec_tensor(&[0.2, 0.3])),
Value::Tensor(vec_tensor(&[0.1, 0.2])),
Value::Tensor(vec_tensor(&[0.1, 0.2])),
]);
let fig = clone_figure(current_figure_handle()).unwrap();
let PlotElement::ErrorBar(error) = fig.plots().next().unwrap() else {
panic!("expected errorbar");
};
assert_eq!(
error.orientation,
runmat_plot::plots::errorbar::ErrorBarOrientation::Both
);
assert_eq!(error.x_neg, vec![0.1, 0.2]);
assert_eq!(error.y_pos, vec![0.2, 0.3]);
}
#[test]
fn errorbar_accepts_line_spec_before_name_value_pairs() {
let _guard = lock_plot_registry();
ensure_plot_test_env();
reset_hold_state_for_run();
let _ = clear_figure(None);
errorbar_builtin(vec![
Value::Tensor(vec_tensor(&[1.0, 2.0])),
Value::Tensor(vec_tensor(&[3.0, 4.0])),
Value::Tensor(vec_tensor(&[0.2, 0.3])),
Value::Tensor(vec_tensor(&[0.4, 0.5])),
Value::String("o-".into()),
Value::String("LineWidth".into()),
Value::Num(1.5),
])
.unwrap();
let fig = clone_figure(current_figure_handle()).unwrap();
let PlotElement::ErrorBar(error) = fig.plots().next().unwrap() else {
panic!("expected errorbar");
};
assert_eq!(
error.orientation,
runmat_plot::plots::errorbar::ErrorBarOrientation::Vertical
);
assert_eq!(error.line_style, runmat_plot::plots::LineStyle::Solid);
assert_eq!(error.line_width, 1.5);
let marker = error.marker.as_ref().expect("expected marker");
assert_eq!(
marker.kind,
runmat_plot::plots::scatter::MarkerStyle::Circle
);
}
#[test]
fn errorbar_accepts_name_value_pairs_after_asymmetric_vertical_data() {
let _guard = lock_plot_registry();
ensure_plot_test_env();
reset_hold_state_for_run();
let _ = clear_figure(None);
errorbar_builtin(vec![
Value::Tensor(vec_tensor(&[1.0, 2.0])),
Value::Tensor(vec_tensor(&[3.0, 4.0])),
Value::Tensor(vec_tensor(&[0.2, 0.3])),
Value::Tensor(vec_tensor(&[0.4, 0.5])),
Value::String("LineWidth".into()),
Value::Num(1.5),
])
.unwrap();
let fig = clone_figure(current_figure_handle()).unwrap();
let PlotElement::ErrorBar(error) = fig.plots().next().unwrap() else {
panic!("expected errorbar");
};
assert_eq!(
error.orientation,
runmat_plot::plots::errorbar::ErrorBarOrientation::Vertical
);
assert_eq!(error.line_width, 1.5);
}
#[test]
fn errorbar_accepts_y_only_line_spec_before_name_value_pairs() {
let _guard = lock_plot_registry();
ensure_plot_test_env();
reset_hold_state_for_run();
let _ = clear_figure(None);
errorbar_builtin(vec![
Value::Tensor(vec_tensor(&[3.0, 4.0])),
Value::Tensor(vec_tensor(&[0.2, 0.3])),
Value::String("o-".into()),
Value::String("LineWidth".into()),
Value::Num(1.5),
])
.unwrap();
let fig = clone_figure(current_figure_handle()).unwrap();
let PlotElement::ErrorBar(error) = fig.plots().next().unwrap() else {
panic!("expected errorbar");
};
assert_eq!(error.x, vec![1.0, 2.0]);
assert_eq!(error.y, vec![3.0, 4.0]);
assert_eq!(error.y_neg, vec![0.2, 0.3]);
assert_eq!(error.y_pos, vec![0.2, 0.3]);
assert_eq!(error.line_width, 1.5);
let marker = error.marker.as_ref().expect("expected marker");
assert_eq!(
marker.kind,
runmat_plot::plots::scatter::MarkerStyle::Circle
);
}
#[test]
fn errorbar_preserves_explicit_x_y_err_with_trailing_line_spec() {
let _guard = lock_plot_registry();
ensure_plot_test_env();
reset_hold_state_for_run();
let _ = clear_figure(None);
errorbar_builtin(vec![
Value::Tensor(vec_tensor(&[1.0, 2.0])),
Value::Tensor(vec_tensor(&[3.0, 4.0])),
Value::Tensor(vec_tensor(&[0.2, 0.3])),
Value::String("o-".into()),
Value::String("LineWidth".into()),
Value::Num(1.5),
])
.unwrap();
let fig = clone_figure(current_figure_handle()).unwrap();
let PlotElement::ErrorBar(error) = fig.plots().next().unwrap() else {
panic!("expected errorbar");
};
assert_eq!(
error.orientation,
runmat_plot::plots::errorbar::ErrorBarOrientation::Vertical
);
assert_eq!(error.x, vec![1.0, 2.0]);
assert_eq!(error.y, vec![3.0, 4.0]);
assert_eq!(error.y_neg, vec![0.2, 0.3]);
assert_eq!(error.y_pos, vec![0.2, 0.3]);
assert_eq!(error.line_width, 1.5);
let marker = error.marker.as_ref().expect("expected marker");
assert_eq!(
marker.kind,
runmat_plot::plots::scatter::MarkerStyle::Circle
);
}
#[test]
fn errorbar_preserves_both_direction_form_with_trailing_style() {
let _guard = lock_plot_registry();
ensure_plot_test_env();
reset_hold_state_for_run();
let _ = clear_figure(None);
errorbar_builtin(vec![
Value::Tensor(vec_tensor(&[1.0, 2.0])),
Value::Tensor(vec_tensor(&[3.0, 4.0])),
Value::Tensor(vec_tensor(&[0.2, 0.3])),
Value::Tensor(vec_tensor(&[0.4, 0.5])),
Value::Tensor(vec_tensor(&[0.1, 0.2])),
Value::Tensor(vec_tensor(&[0.2, 0.3])),
Value::String("o-".into()),
Value::String("LineWidth".into()),
Value::Num(1.5),
])
.unwrap();
let fig = clone_figure(current_figure_handle()).unwrap();
let PlotElement::ErrorBar(error) = fig.plots().next().unwrap() else {
panic!("expected errorbar");
};
assert_eq!(
error.orientation,
runmat_plot::plots::errorbar::ErrorBarOrientation::Both
);
assert_eq!(error.x_neg, vec![0.1, 0.2]);
assert_eq!(error.x_pos, vec![0.2, 0.3]);
assert_eq!(error.line_width, 1.5);
let marker = error.marker.as_ref().expect("expected marker");
assert_eq!(
marker.kind,
runmat_plot::plots::scatter::MarkerStyle::Circle
);
}
#[test]
fn errorbar_handle_exposes_runtime_properties() {
let _guard = lock_plot_registry();
ensure_plot_test_env();
reset_hold_state_for_run();
let _ = clear_figure(None);
let handle = errorbar_builtin(vec![
Value::Tensor(vec_tensor(&[1.0, 2.0])),
Value::Tensor(vec_tensor(&[0.1, 0.2])),
])
.unwrap();
let props = get_builtin(vec![Value::Num(handle)]).unwrap();
let Value::Struct(st) = props else {
panic!("expected struct");
};
assert_eq!(
st.fields.get("Type"),
Some(&Value::String("errorbar".into()))
);
assert!(st.fields.contains_key("CapSize"));
}
#[test]
fn errorbar_handle_set_updates_properties() {
let _guard = lock_plot_registry();
ensure_plot_test_env();
reset_hold_state_for_run();
let _ = clear_figure(None);
let handle = errorbar_builtin(vec![
Value::Tensor(vec_tensor(&[1.0, 2.0])),
Value::Tensor(vec_tensor(&[0.1, 0.2])),
])
.unwrap();
set_builtin(vec![
Value::Num(handle),
Value::String("CapSize".into()),
Value::Num(12.0),
])
.unwrap();
let cap = get_builtin(vec![Value::Num(handle), Value::String("CapSize".into())]).unwrap();
assert_eq!(cap, Value::Num(12.0));
}
#[test]
fn errorbar_accepts_scalar_point() {
let _guard = lock_plot_registry();
ensure_plot_test_env();
reset_hold_state_for_run();
let _ = clear_figure(None);
let _ = errorbar_builtin(vec![Value::Num(1.0), Value::Num(2.0), Value::Num(0.3)]).unwrap();
let fig = clone_figure(current_figure_handle()).unwrap();
let PlotElement::ErrorBar(plot) = fig.plots().next().unwrap() else {
panic!("expected errorbar")
};
assert_eq!(plot.x, vec![1.0]);
assert_eq!(plot.y, vec![2.0]);
}
#[test]
fn errorbar_descriptor_includes_core_signatures() {
let labels: Vec<&str> = ERRORBAR_DESCRIPTOR
.signatures
.iter()
.map(|sig| sig.label)
.collect();
assert!(labels.contains(&"h = errorbar(Y, E)"));
assert!(labels.contains(&"h = errorbar(X, Y, E)"));
assert!(labels.contains(&"h = errorbar(X, Y, XNeg, XPos, YNeg, YPos)"));
}
#[test]
fn errorbar_missing_data_uses_stable_identifier() {
let err = errorbar_builtin(vec![Value::Num(1.0)])
.expect_err("expected errorbar argument validation error");
assert_eq!(err.identifier(), ERRORBAR_ERROR_INVALID_ARGUMENT.identifier);
}
}