use std::collections::BTreeMap;
use runmat_builtins::{
BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
LogicalArray, ResolveContext, SparseTensor, Tensor, Type, Value,
};
use runmat_macros::runtime_builtin;
use crate::builtins::common::gpu_helpers;
use crate::builtins::common::spec::{
BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
};
use crate::{build_runtime_error, BuiltinResult, RuntimeError};
const NAME: &str = "sparse";
const SPARSE_DENSE_INPUT_VECTOR_LIMIT: usize = 10_000_000;
const SPARSE_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
name: "S",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "Sparse double matrix.",
}];
const SPARSE_INPUT_A: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
name: "A",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "Full or sparse matrix to convert.",
}];
const SPARSE_INPUT_DIMS: [BuiltinParamDescriptor; 2] = [
BuiltinParamDescriptor {
name: "m",
ty: BuiltinParamType::SizeArg,
arity: BuiltinParamArity::Required,
default: None,
description: "Number of rows.",
},
BuiltinParamDescriptor {
name: "n",
ty: BuiltinParamType::SizeArg,
arity: BuiltinParamArity::Required,
default: None,
description: "Number of columns.",
},
];
const SPARSE_INPUT_TRIPLETS: [BuiltinParamDescriptor; 3] = [
BuiltinParamDescriptor {
name: "i",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "One-based row subscripts.",
},
BuiltinParamDescriptor {
name: "j",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "One-based column subscripts.",
},
BuiltinParamDescriptor {
name: "v",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "Values for each row/column pair.",
},
];
const SPARSE_INPUT_TRIPLETS_DIMS: [BuiltinParamDescriptor; 5] = [
BuiltinParamDescriptor {
name: "i",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "One-based row subscripts.",
},
BuiltinParamDescriptor {
name: "j",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "One-based column subscripts.",
},
BuiltinParamDescriptor {
name: "v",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "Values for each row/column pair.",
},
BuiltinParamDescriptor {
name: "m",
ty: BuiltinParamType::SizeArg,
arity: BuiltinParamArity::Required,
default: None,
description: "Number of rows.",
},
BuiltinParamDescriptor {
name: "n",
ty: BuiltinParamType::SizeArg,
arity: BuiltinParamArity::Required,
default: None,
description: "Number of columns.",
},
];
const SPARSE_INPUT_TRIPLETS_DIMS_NZMAX: [BuiltinParamDescriptor; 6] = [
BuiltinParamDescriptor {
name: "i",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "One-based row subscripts.",
},
BuiltinParamDescriptor {
name: "j",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "One-based column subscripts.",
},
BuiltinParamDescriptor {
name: "v",
ty: BuiltinParamType::NumericArray,
arity: BuiltinParamArity::Required,
default: None,
description: "Values for each row/column pair.",
},
BuiltinParamDescriptor {
name: "m",
ty: BuiltinParamType::SizeArg,
arity: BuiltinParamArity::Required,
default: None,
description: "Number of rows.",
},
BuiltinParamDescriptor {
name: "n",
ty: BuiltinParamType::SizeArg,
arity: BuiltinParamArity::Required,
default: None,
description: "Number of columns.",
},
BuiltinParamDescriptor {
name: "nzmax",
ty: BuiltinParamType::SizeArg,
arity: BuiltinParamArity::Optional,
default: None,
description: "Allocation hint accepted for MATLAB compatibility.",
},
];
const SPARSE_SIGNATURES: [BuiltinSignatureDescriptor; 5] = [
BuiltinSignatureDescriptor {
label: "S = sparse(A)",
inputs: &SPARSE_INPUT_A,
outputs: &SPARSE_OUTPUT,
},
BuiltinSignatureDescriptor {
label: "S = sparse(m, n)",
inputs: &SPARSE_INPUT_DIMS,
outputs: &SPARSE_OUTPUT,
},
BuiltinSignatureDescriptor {
label: "S = sparse(i, j, v)",
inputs: &SPARSE_INPUT_TRIPLETS,
outputs: &SPARSE_OUTPUT,
},
BuiltinSignatureDescriptor {
label: "S = sparse(i, j, v, m, n)",
inputs: &SPARSE_INPUT_TRIPLETS_DIMS,
outputs: &SPARSE_OUTPUT,
},
BuiltinSignatureDescriptor {
label: "S = sparse(i, j, v, m, n, nzmax)",
inputs: &SPARSE_INPUT_TRIPLETS_DIMS_NZMAX,
outputs: &SPARSE_OUTPUT,
},
];
const SPARSE_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
code: "RM.SPARSE.INVALID_INPUT",
identifier: Some("RunMat:sparse:InvalidInput"),
when: "Inputs are not a supported sparse construction form.",
message: "sparse: invalid input",
};
const SPARSE_ERROR_INVALID_INDEX: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
code: "RM.SPARSE.INVALID_INDEX",
identifier: Some("RunMat:sparse:InvalidIndex"),
when: "Row or column subscripts are nonpositive, noninteger, or outside explicit dimensions.",
message: "sparse: invalid index",
};
const SPARSE_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
code: "RM.SPARSE.INTERNAL",
identifier: Some("RunMat:sparse:Internal"),
when: "Sparse matrix materialisation fails internally.",
message: "sparse: internal error",
};
const SPARSE_ERRORS: [BuiltinErrorDescriptor; 3] = [
SPARSE_ERROR_INVALID_INPUT,
SPARSE_ERROR_INVALID_INDEX,
SPARSE_ERROR_INTERNAL,
];
pub const SPARSE_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
signatures: &SPARSE_SIGNATURES,
output_mode: BuiltinOutputMode::Fixed,
completion_policy: BuiltinCompletionPolicy::Public,
errors: &SPARSE_ERRORS,
};
#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::array::creation::sparse")]
pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
name: NAME,
op_kind: GpuOpKind::Custom("sparse"),
supported_precisions: &[ScalarType::F32, ScalarType::F64],
broadcast: BroadcastSemantics::None,
provider_hooks: &[],
constant_strategy: ConstantStrategy::InlineLiteral,
residency: ResidencyPolicy::GatherImmediately,
nan_mode: ReductionNaN::Include,
two_pass_threshold: None,
workgroup_size: None,
accepts_nan_mode: false,
notes: "Sparse matrices are host-resident CSC values. GPU inputs are gathered before sparse construction because RunMat's acceleration API currently exposes dense tensor handles only.",
};
#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::array::creation::sparse")]
pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
name: NAME,
shape: ShapeRequirements::Any,
constant_strategy: ConstantStrategy::InlineLiteral,
elementwise: None,
reduction: None,
emits_nan: false,
notes: "Sparse construction is a representation-changing operation and is not fused.",
};
fn sparse_type(args: &[Type], _ctx: &ResolveContext) -> Type {
match args {
[Type::Tensor { shape: Some(shape) }] => Type::Tensor {
shape: Some(shape.clone()),
},
[Type::Logical { shape: Some(shape) }] => Type::Tensor {
shape: Some(shape.clone()),
},
[Type::Num | Type::Int, Type::Num | Type::Int] => Type::Tensor {
shape: Some(vec![None, None]),
},
_ => Type::tensor(),
}
}
fn sparse_error(
error: &'static BuiltinErrorDescriptor,
message: impl Into<String>,
) -> RuntimeError {
let mut builder = build_runtime_error(message).with_builtin(NAME);
if let Some(identifier) = error.identifier {
builder = builder.with_identifier(identifier);
}
builder.build()
}
#[runtime_builtin(
name = "sparse",
category = "array/creation",
summary = "Create sparse double matrices from full arrays or row/column/value triplets.",
keywords = "sparse,csc,matrix,nonzero,gpu",
accel = "custom",
type_resolver(sparse_type),
descriptor(crate::builtins::array::creation::sparse::SPARSE_DESCRIPTOR),
builtin_path = "crate::builtins::array::creation::sparse"
)]
async fn sparse_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
let mut gathered = Vec::with_capacity(args.len());
for arg in args {
gathered.push(gpu_helpers::gather_value_async(&arg).await?);
}
construct_sparse(gathered).map(Value::SparseTensor)
}
fn construct_sparse(args: Vec<Value>) -> BuiltinResult<SparseTensor> {
match args.len() {
1 => sparse_from_value(args.into_iter().next().expect("one argument")),
2 => {
let mut iter = args.into_iter();
let rows = parse_size_arg(iter.next().as_ref().expect("rows"), "m")?;
let cols = parse_size_arg(iter.next().as_ref().expect("cols"), "n")?;
Ok(SparseTensor::zeros(rows, cols))
}
3 | 5 | 6 => sparse_from_triplet_form(args),
_ => Err(sparse_error(
&SPARSE_ERROR_INVALID_INPUT,
"sparse: expected sparse(A), sparse(m,n), or sparse(i,j,v[,m,n[,nzmax]])",
)),
}
}
fn sparse_from_value(value: Value) -> BuiltinResult<SparseTensor> {
match value {
Value::SparseTensor(sparse) => Ok(sparse),
Value::Tensor(tensor) => {
if tensor.shape.len() != 2 {
return Err(sparse_error(
&SPARSE_ERROR_INVALID_INPUT,
format!(
"sparse: input must be a 2-D matrix, got {}-D tensor",
tensor.shape.len()
),
));
}
sparse_from_dense_tensor(&tensor)
}
Value::LogicalArray(logical) => {
if logical.shape.len() != 2 {
return Err(sparse_error(
&SPARSE_ERROR_INVALID_INPUT,
format!(
"sparse: input must be a 2-D matrix, got {}-D logical array",
logical.shape.len()
),
));
}
sparse_from_logical_array(&logical)
}
Value::Num(n) => sparse_from_dense_tensor(
&Tensor::new(vec![n], vec![1, 1])
.map_err(|err| sparse_error(&SPARSE_ERROR_INTERNAL, format!("sparse: {err}")))?,
),
Value::Int(i) => sparse_from_dense_tensor(
&Tensor::new(vec![i.to_f64()], vec![1, 1])
.map_err(|err| sparse_error(&SPARSE_ERROR_INTERNAL, format!("sparse: {err}")))?,
),
Value::Bool(b) => sparse_from_dense_tensor(
&Tensor::new(vec![if b { 1.0 } else { 0.0 }], vec![1, 1])
.map_err(|err| sparse_error(&SPARSE_ERROR_INTERNAL, format!("sparse: {err}")))?,
),
other => Err(sparse_error(
&SPARSE_ERROR_INVALID_INPUT,
format!("sparse: unsupported conversion input {other:?}"),
)),
}
}
fn sparse_from_dense_tensor(tensor: &Tensor) -> BuiltinResult<SparseTensor> {
let rows = tensor.rows();
let cols = tensor.cols();
let mut col_ptrs = Vec::with_capacity(cols.saturating_add(1));
let mut row_indices = Vec::new();
let mut values = Vec::new();
col_ptrs.push(0);
for col in 0..cols {
for row in 0..rows {
let value = tensor.data[row + col * rows];
if is_stored_value(value) {
row_indices.push(row);
values.push(value);
}
}
col_ptrs.push(values.len());
}
SparseTensor::new(rows, cols, col_ptrs, row_indices, values)
.map_err(|err| sparse_error(&SPARSE_ERROR_INTERNAL, format!("sparse: {err}")))
}
fn sparse_from_logical_array(logical: &LogicalArray) -> BuiltinResult<SparseTensor> {
if logical.shape.len() != 2 {
return Err(sparse_error(
&SPARSE_ERROR_INVALID_INPUT,
format!(
"sparse: input must be a 2-D matrix, got {}-D logical array",
logical.shape.len()
),
));
}
let shape = match logical.shape.as_slice() {
[] => vec![1, 1],
[n] => vec![1, *n],
[rows, cols, ..] => vec![*rows, *cols],
};
let data = logical
.data
.iter()
.map(|&bit| if bit != 0 { 1.0 } else { 0.0 })
.collect();
let tensor = Tensor::new(data, shape)
.map_err(|err| sparse_error(&SPARSE_ERROR_INTERNAL, format!("sparse: {err}")))?;
sparse_from_dense_tensor(&tensor)
}
fn sparse_from_triplet_form(args: Vec<Value>) -> BuiltinResult<SparseTensor> {
let rows_vec = numeric_vector(&args[0], "i")?;
let cols_vec = numeric_vector(&args[1], "j")?;
let values_vec = numeric_vector(&args[2], "v")?;
let target_length = rows_vec.len().max(cols_vec.len()).max(values_vec.len());
let rows_vec = if rows_vec.len() == 1 && target_length > 1 {
vec![rows_vec[0]; target_length]
} else {
rows_vec
};
let cols_vec = if cols_vec.len() == 1 && target_length > 1 {
vec![cols_vec[0]; target_length]
} else {
cols_vec
};
let values_vec = if values_vec.len() == 1 && target_length > 1 {
vec![values_vec[0]; target_length]
} else {
values_vec
};
if rows_vec.len() != cols_vec.len() || rows_vec.len() != values_vec.len() {
return Err(sparse_error(
&SPARSE_ERROR_INVALID_INPUT,
"sparse: i, j, and v must have the same number of elements",
));
}
let explicit_dims = args.len() >= 5;
let mut rows = if explicit_dims {
parse_size_arg(&args[3], "m")?
} else {
0
};
let mut cols = if explicit_dims {
parse_size_arg(&args[4], "n")?
} else {
0
};
if args.len() == 6 {
let _ = parse_size_arg(&args[5], "nzmax")?;
}
let mut entries: BTreeMap<(usize, usize), f64> = BTreeMap::new();
for ((&row_raw, &col_raw), &value) in
rows_vec.iter().zip(cols_vec.iter()).zip(values_vec.iter())
{
let row = parse_subscript(row_raw, "row")?;
let col = parse_subscript(col_raw, "column")?;
if explicit_dims {
if row > rows || col > cols {
return Err(sparse_error(
&SPARSE_ERROR_INVALID_INDEX,
"sparse: subscript exceeds matrix dimensions",
));
}
} else {
rows = rows.max(row);
cols = cols.max(col);
}
if is_stored_value(value) {
let key = (col - 1, row - 1);
let entry = entries.entry(key).or_insert(0.0);
*entry += value;
}
}
let mut col_ptrs = Vec::with_capacity(cols.saturating_add(1));
let mut row_indices = Vec::new();
let mut values = Vec::new();
col_ptrs.push(0);
for col in 0..cols {
for (&(entry_col, row), value) in entries.range((col, 0)..=(col, usize::MAX)) {
if entry_col != col {
break;
}
if row >= rows {
return Err(sparse_error(
&SPARSE_ERROR_INVALID_INDEX,
"sparse: row index exceeds matrix dimensions",
));
}
if is_stored_value(*value) {
row_indices.push(row);
values.push(*value);
}
}
col_ptrs.push(values.len());
}
SparseTensor::new(rows, cols, col_ptrs, row_indices, values)
.map_err(|err| sparse_error(&SPARSE_ERROR_INTERNAL, format!("sparse: {err}")))
}
fn numeric_vector(value: &Value, name: &str) -> BuiltinResult<Vec<f64>> {
match value {
Value::Tensor(tensor) => {
if !is_vector_shape(&tensor.shape) {
return Err(numeric_vector_error(value, name));
}
Ok(tensor.data.clone())
}
Value::SparseTensor(sparse) => {
let shape = [sparse.rows, sparse.cols];
if !is_vector_shape(&shape) {
return Err(numeric_vector_error(value, name));
}
let total_elements = sparse.rows.checked_mul(sparse.cols).ok_or_else(|| {
sparse_error(
&SPARSE_ERROR_INVALID_INPUT,
format!("sparse: {name} sparse vector dimensions overflow"),
)
})?;
if total_elements > SPARSE_DENSE_INPUT_VECTOR_LIMIT {
return Err(sparse_error(
&SPARSE_ERROR_INVALID_INPUT,
format!(
"sparse: cannot densify sparse {name} vector {}x{} with {} stored entries ({} elements exceeds safe threshold)",
sparse.rows,
sparse.cols,
sparse.nnz(),
total_elements
),
));
}
sparse
.to_dense()
.map(|dense| dense.data)
.map_err(|err| sparse_error(&SPARSE_ERROR_INTERNAL, format!("sparse: {err}")))
}
Value::LogicalArray(logical) => {
if !is_vector_shape(&logical.shape) {
return Err(numeric_vector_error(value, name));
}
Ok(logical
.data
.iter()
.map(|&bit| if bit != 0 { 1.0 } else { 0.0 })
.collect())
}
Value::Num(n) => Ok(vec![*n]),
Value::Int(i) => Ok(vec![i.to_f64()]),
Value::Bool(b) => Ok(vec![if *b { 1.0 } else { 0.0 }]),
other => Err(numeric_vector_error(other, name)),
}
}
fn numeric_vector_error(value: &Value, name: &str) -> RuntimeError {
sparse_error(
&SPARSE_ERROR_INVALID_INPUT,
format!("sparse: {name} must be a real numeric vector, got {value:?}"),
)
}
fn is_vector_shape(shape: &[usize]) -> bool {
if shape.len() > 2 {
return false;
}
shape.iter().filter(|&&dim| dim != 1).count() <= 1
}
fn parse_size_arg(value: &Value, name: &str) -> BuiltinResult<usize> {
let raw = match value {
Value::Num(n) => *n,
Value::Int(i) => i.to_f64(),
Value::Bool(b) => {
if *b {
1.0
} else {
0.0
}
}
_ => {
return Err(sparse_error(
&SPARSE_ERROR_INVALID_INPUT,
format!("sparse: {name} must be a scalar size"),
))
}
};
if !raw.is_finite() || raw < 0.0 || raw.fract() != 0.0 {
return Err(sparse_error(
&SPARSE_ERROR_INVALID_INPUT,
format!("sparse: {name} must be a nonnegative integer"),
));
}
if raw > max_usize_cast_value() {
return Err(sparse_error(
&SPARSE_ERROR_INVALID_INPUT,
format!("sparse: {name} exceeds the maximum supported size"),
));
}
Ok(raw as usize)
}
fn parse_subscript(raw: f64, name: &str) -> BuiltinResult<usize> {
if !raw.is_finite() || raw < 1.0 || raw.fract() != 0.0 {
return Err(sparse_error(
&SPARSE_ERROR_INVALID_INDEX,
format!("sparse: {name} indices must be positive integers"),
));
}
if raw > max_usize_cast_value() {
return Err(sparse_error(
&SPARSE_ERROR_INVALID_INPUT,
format!("sparse: {name} index exceeds the maximum supported size"),
));
}
Ok(raw as usize)
}
fn max_usize_cast_value() -> f64 {
if usize::BITS <= f64::MANTISSA_DIGITS {
usize::MAX as f64
} else {
f64::from_bits((usize::MAX as f64).to_bits() - 1)
}
}
fn is_stored_value(value: f64) -> bool {
value.is_nan() || value != 0.0
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::builtins::common::test_support;
use futures::executor::block_on;
use runmat_accelerate_api::HostTensorView;
use runmat_builtins::IntValue;
fn sparse_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
block_on(super::sparse_builtin(args))
}
fn expect_sparse(value: Value) -> SparseTensor {
match value {
Value::SparseTensor(sparse) => sparse,
other => panic!("expected sparse tensor, got {other:?}"),
}
}
#[test]
fn sparse_dims_constructs_empty_matrix() {
let sparse = expect_sparse(
sparse_builtin(vec![
Value::Int(IntValue::I32(3)),
Value::Int(IntValue::I32(4)),
])
.expect("sparse"),
);
assert_eq!(sparse.shape(), vec![3, 4]);
assert_eq!(sparse.nnz(), 0);
assert_eq!(sparse.col_ptrs, vec![0, 0, 0, 0, 0]);
}
#[test]
fn sparse_triplets_sum_duplicates_and_drop_zeros() {
let i = Tensor::new(vec![1.0, 2.0, 1.0, 2.0], vec![4, 1]).unwrap();
let j = Tensor::new(vec![1.0, 1.0, 1.0, 3.0], vec![4, 1]).unwrap();
let v = Tensor::new(vec![2.0, 0.0, 5.0, 9.0], vec![4, 1]).unwrap();
let sparse = expect_sparse(
sparse_builtin(vec![
Value::Tensor(i),
Value::Tensor(j),
Value::Tensor(v),
Value::Num(3.0),
Value::Num(4.0),
])
.expect("sparse"),
);
assert_eq!(sparse.shape(), vec![3, 4]);
assert_eq!(sparse.nnz(), 2);
assert_eq!(sparse.get(0, 0), Some(7.0));
assert_eq!(sparse.get(1, 2), Some(9.0));
}
#[test]
fn sparse_triplets_reject_matrix_inputs() {
let matrix = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap();
let vector = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![4, 1]).unwrap();
let err = sparse_builtin(vec![
Value::Tensor(matrix),
Value::Tensor(vector.clone()),
Value::Tensor(vector.clone()),
])
.unwrap_err();
assert_eq!(err.identifier(), Some("RunMat:sparse:InvalidInput"));
assert!(err.message().contains("i must be a real numeric vector"));
let sparse_matrix =
SparseTensor::new(2, 2, vec![0, 1, 2], vec![0, 1], vec![1.0, 2.0]).unwrap();
let err = sparse_builtin(vec![
Value::SparseTensor(sparse_matrix),
Value::Tensor(vector.clone()),
Value::Tensor(vector.clone()),
])
.unwrap_err();
assert_eq!(err.identifier(), Some("RunMat:sparse:InvalidInput"));
assert!(err.message().contains("i must be a real numeric vector"));
let logical_matrix = LogicalArray::new(vec![1, 0, 0, 1], vec![2, 2]).unwrap();
let err = sparse_builtin(vec![
Value::LogicalArray(logical_matrix),
Value::Tensor(vector.clone()),
Value::Tensor(vector),
])
.unwrap_err();
assert_eq!(err.identifier(), Some("RunMat:sparse:InvalidInput"));
assert!(err.message().contains("i must be a real numeric vector"));
}
#[test]
fn sparse_from_dense_preserves_column_major_entries() {
let dense = Tensor::new(vec![0.0, 4.0, 5.0, 0.0], vec![2, 2]).unwrap();
let sparse = expect_sparse(sparse_builtin(vec![Value::Tensor(dense)]).expect("sparse"));
assert_eq!(sparse.col_ptrs, vec![0, 1, 2]);
assert_eq!(sparse.row_indices, vec![1, 0]);
assert_eq!(sparse.values, vec![4.0, 5.0]);
}
#[test]
fn sparse_gathers_gpu_input() {
test_support::with_test_provider(|provider| {
let dense = Tensor::new(vec![0.0, 8.0, 0.0, 3.0], vec![2, 2]).unwrap();
let handle = provider
.upload(&HostTensorView {
data: &dense.data,
shape: &dense.shape,
})
.expect("upload");
let sparse = expect_sparse(sparse_builtin(vec![Value::GpuTensor(handle)]).unwrap());
assert_eq!(sparse.nnz(), 2);
assert_eq!(sparse.get(1, 0), Some(8.0));
assert_eq!(sparse.get(1, 1), Some(3.0));
});
}
#[test]
fn sparse_rejects_size_and_subscript_values_too_large_for_usize() {
let too_large = max_usize_cast_value() * 2.0;
let size_err = parse_size_arg(&Value::Num(too_large), "m").unwrap_err();
assert_eq!(size_err.identifier(), Some("RunMat:sparse:InvalidInput"));
assert!(size_err.message().contains("maximum supported size"));
let index_err = parse_subscript(too_large, "row").unwrap_err();
assert_eq!(index_err.identifier(), Some("RunMat:sparse:InvalidInput"));
assert!(index_err.message().contains("maximum supported size"));
}
#[test]
fn sparse_triplets_reject_oversized_sparse_vector_before_densifying() {
let i = SparseTensor::zeros(SPARSE_DENSE_INPUT_VECTOR_LIMIT + 1, 1);
let j = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
let v = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
let err = sparse_builtin(vec![
Value::SparseTensor(i),
Value::Tensor(j),
Value::Tensor(v),
])
.unwrap_err();
assert_eq!(err.identifier(), Some("RunMat:sparse:InvalidInput"));
assert!(err.message().contains("exceeds safe threshold"));
}
}