use extendr_api::prelude::*;
use faer::{Mat, MatRef};
use num_traits::NumCast;
use rayon::prelude::*;
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
use std::collections::{BTreeMap, HashMap};
use std::ops::{Add, Mul};
use crate::prelude::*;
use crate::utils::traits::FaerRType;
pub type NestedHashMap = FxHashMap<String, FxHashMap<String, FxHashSet<String>>>;
pub type NestedBtreeMap = BTreeMap<String, BTreeMap<String, FxHashSet<String>>>;
const MAX_R_COUNT: f64 = 9_007_199_254_740_992.0;
const MAX_R_SPARSE_INDEX: u32 = i32::MAX as u32;
const PARALLEL_PARSE_THRESHOLD: usize = 1 << 16;
const FILL_CHUNK: usize = 1 << 15;
const TRANSPOSE_TILE: usize = 8;
fn validate_sparse_layout(
indptr: &[u32],
indices: &[u32],
nnz: usize,
shape: (usize, usize),
cs_type: CompressedSparseFormat,
) -> Result<(), BixverseErrors> {
let major = match cs_type {
CompressedSparseFormat::Csr => shape.0,
CompressedSparseFormat::Csc => shape.1,
};
if indices.len() != nnz {
return Err(BixverseErrors::RListParse(
"indices and data must have the same length",
));
}
if indptr.len() != major + 1 {
return Err(BixverseErrors::RListParse(
"indptr length must be the major dimension plus one",
));
}
if indptr.windows(2).any(|w| w[0] > w[1]) {
return Err(BixverseErrors::RListParse("indptr must be non-decreasing"));
}
if indptr[indptr.len() - 1] != nnz as u32 {
return Err(BixverseErrors::RListParse(
"the last indptr entry must equal the number of stored values",
));
}
Ok(())
}
fn try_map_into<S, D, E, F>(dst: &mut [D], src: &[S], f: F) -> Result<(), E>
where
S: Copy + Sync,
D: Send,
E: Send,
F: Fn(S) -> Result<D, E> + Sync,
{
debug_assert_eq!(dst.len(), src.len());
let fill = |(d, s): (&mut [D], &[S])| {
d.iter_mut().zip(s).try_for_each(|(d, &s)| {
*d = f(s)?;
Ok(())
})
};
if src.len() >= PARALLEL_PARSE_THRESHOLD {
dst.par_chunks_mut(FILL_CHUNK)
.zip(src.par_chunks(FILL_CHUNK))
.try_for_each(fill)
} else {
fill((dst, src))
}
}
pub(crate) fn fill_column_major<S, D, F>(dst: &mut [D], x: MatRef<S>, f: F)
where
S: Copy + Sync,
D: Send,
F: Fn(S) -> D + Sync,
{
let (nrow, ncol) = (x.nrows(), x.ncols());
debug_assert_eq!(dst.len(), nrow * ncol);
if nrow == 0 || ncol == 0 {
return;
}
let parallel = dst.len() >= PARALLEL_PARSE_THRESHOLD;
if x.row_stride() == 1 {
let fill = |(c, chunk): (usize, &mut [D])| {
let mut flat = c * FILL_CHUNK;
let mut rest = chunk;
while !rest.is_empty() {
let (i, j) = (flat % nrow, flat / nrow);
let len = (nrow - i).min(rest.len());
let src = x
.col(j)
.try_as_col_major()
.expect("row stride is 1")
.as_slice();
let (head, tail) = rest.split_at_mut(len);
head.iter_mut()
.zip(&src[i..i + len])
.for_each(|(d, &s)| *d = f(s));
rest = tail;
flat += len;
}
};
if parallel {
dst.par_chunks_mut(FILL_CHUNK).enumerate().for_each(fill);
} else {
dst.chunks_mut(FILL_CHUNK).enumerate().for_each(fill);
}
} else {
let fill = |(b, block): (usize, &mut [D])| {
let j0 = b * TRANSPOSE_TILE;
let width = block.len() / nrow;
for i in 0..nrow {
let row = x.row(i).subcols(j0, width);
match row.try_as_row_major() {
Some(row) => {
for (jj, &v) in row.as_slice().iter().enumerate() {
block[jj * nrow + i] = f(v);
}
}
None => {
for jj in 0..width {
block[jj * nrow + i] = f(row[jj]);
}
}
}
}
};
if parallel {
dst.par_chunks_mut(nrow * TRANSPOSE_TILE)
.enumerate()
.for_each(fill);
} else {
dst.chunks_mut(nrow * TRANSPOSE_TILE)
.enumerate()
.for_each(fill);
}
}
}
fn alloc_r_vector(rtype: Rtype, len: usize) -> Robj {
Robj::alloc_vector(extendr_api::rtype_to_sxp(rtype), len)
}
#[derive(Debug)]
pub enum NamedVecError {
NotNumeric,
NoNames,
MissingValues,
}
impl std::fmt::Display for NamedVecError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
NamedVecError::NotNumeric => write!(f, "Input is not a numeric vector"),
NamedVecError::NoNames => write!(f, "Vector has no names attribute"),
NamedVecError::MissingValues => write!(f, "Vector contains missing values"),
}
}
}
impl std::error::Error for NamedVecError {}
impl From<NamedVecError> for extendr_api::Error {
fn from(err: NamedVecError) -> Self {
extendr_api::Error::Other(err.to_string())
}
}
pub fn r_list_to_map<'a>(r_list: List) -> extendr_api::Result<HashMap<&'a str, Robj>> {
if !r_list.is_empty() && r_list.names().is_none() {
return Err(Error::Other(
"The parameter list has no names; did an `unname()` slip in?".to_string(),
));
}
r_list.try_into()
}
fn r_list_bounded_count(
params: &HashMap<&str, Robj>,
key: &str,
min: usize,
) -> extendr_api::Result<Option<usize>> {
let Some(value) = params.get(key) else {
return Ok(None);
};
if value.is_null() {
return Ok(None);
}
let raw = if let Some(v) = value.as_integer() {
if v == i32::MIN {
return Ok(None);
}
v as f64
} else if let Some(v) = value.as_real() {
if v.is_nan() {
return Ok(None);
}
v
} else {
return Err(Error::Other(format!(
"'{key}' must be a single number, got something else"
)));
};
if raw < min as f64 || raw.fract() != 0.0 || raw > MAX_R_COUNT {
return Err(Error::Other(format!(
"'{key}' must be a whole number >= {min}, got {raw}"
)));
}
Ok(Some(raw as usize))
}
pub fn r_list_count(params: &HashMap<&str, Robj>, key: &str) -> extendr_api::Result<Option<usize>> {
r_list_bounded_count(params, key, 1)
}
pub fn r_list_count_allow_zero(
params: &HashMap<&str, Robj>,
key: &str,
) -> extendr_api::Result<Option<usize>> {
r_list_bounded_count(params, key, 0)
}
pub fn r_list_real(params: &HashMap<&str, Robj>, key: &str) -> extendr_api::Result<Option<f64>> {
let Some(value) = params.get(key) else {
return Ok(None);
};
if value.is_null() {
return Ok(None);
}
if let Some(v) = value.as_integer() {
if v == i32::MIN {
return Ok(None);
}
return Ok(Some(v as f64));
}
if let Some(v) = value.as_real() {
if v.is_nan() {
return Ok(None);
}
return Ok(Some(v));
}
if let Some(slice) = value.as_real_slice()
&& slice.len() == 1
{
return Ok(None);
}
Err(Error::Other(format!(
"'{key}' must be a single number, got something else"
)))
}
pub fn r_list_required_real(
params: &HashMap<&str, Robj>,
key: &str,
what: &str,
) -> extendr_api::Result<f64> {
if !params.contains_key(key) {
return Err(Error::Other(format!("{what} is missing '{key}'")));
}
r_list_real(params, key)?
.ok_or_else(|| Error::Other(format!("{what} field '{key}' is not a single number")))
}
pub fn r_list_to_hashmap(r_list: List) -> extendr_api::Result<FxHashMap<String, Vec<String>>> {
let mut result = FxHashMap::with_capacity_and_hasher(r_list.len(), FxBuildHasher);
for (n, s) in r_list {
let s_vec = s.as_string_vector().ok_or_else(|| {
Error::Other(format!(
"Failed to convert value for key '{}' to string vector",
n
))
})?;
result.insert(n.to_string(), s_vec);
}
Ok(result)
}
pub fn r_list_to_hashmap_set(
r_list: List,
) -> extendr_api::Result<FxHashMap<String, FxHashSet<String>>> {
let mut result = FxHashMap::with_capacity_and_hasher(r_list.len(), FxBuildHasher);
for (n, s) in r_list {
let s_vec = s.as_string_vector().ok_or_else(|| {
Error::Other(format!(
"Failed to convert value for key '{}' to string vector",
n
))
})?;
let mut s_hash = FxHashSet::with_capacity_and_hasher(s_vec.len(), FxBuildHasher);
for item in s_vec {
s_hash.insert(item);
}
result.insert(n.to_string(), s_hash);
}
Ok(result)
}
#[allow(dead_code)]
pub fn r_nested_list_to_nested_hashmap(r_nested_list: List) -> extendr_api::Result<NestedHashMap> {
let mut result = FxHashMap::with_capacity_and_hasher(r_nested_list.len(), FxBuildHasher);
for (n, obj) in r_nested_list {
let inner_list = obj.as_list().ok_or_else(|| {
Error::Other(format!("Failed to convert value for key '{}' to list", n))
})?;
let inner_hashmap = r_list_to_hashmap_set(inner_list)?;
result.insert(n.to_string(), inner_hashmap);
}
Ok(result)
}
pub fn r_list_to_hash_vec(r_list: List) -> extendr_api::Result<Vec<FxHashSet<String>>> {
let mut res = Vec::with_capacity(r_list.len());
for (n, s) in r_list {
let s_vec = s.as_string_vector().ok_or_else(|| {
Error::Other(format!(
"Failed to convert value for key '{}' to string vector",
n
))
})?;
let mut s_hash = FxHashSet::with_capacity_and_hasher(s_vec.len(), FxBuildHasher);
for item in s_vec {
s_hash.insert(item);
}
res.push(s_hash)
}
Ok(res)
}
pub fn r_list_to_btree_set(
r_list: List,
) -> extendr_api::Result<BTreeMap<String, FxHashSet<String>>> {
let mut result = BTreeMap::new();
for (n, s) in r_list {
let s_vec = s.as_string_vector().ok_or_else(|| {
Error::Other(format!(
"Failed to convert value for key '{}' to string vector",
n
))
})?;
let mut s_hash = FxHashSet::with_capacity_and_hasher(s_vec.len(), FxBuildHasher);
for item in s_vec {
s_hash.insert(item);
}
result.insert(n.to_string(), s_hash);
}
Ok(result)
}
pub fn r_nested_list_to_btree_nest(r_nested_list: List) -> extendr_api::Result<NestedBtreeMap> {
let mut result = BTreeMap::new();
for (n, obj) in r_nested_list {
let inner_list = obj.as_list().ok_or_else(|| {
Error::Other(format!("Failed to convert value for key '{}' to list", n))
})?;
let inner_tree = r_list_to_btree_set(inner_list)?;
result.insert(n.to_string(), inner_tree);
}
Ok(result)
}
pub type NamedNumericVec = (Vec<String>, Vec<f64>);
pub fn r_list_to_str_vec(r_list: List) -> extendr_api::Result<Vec<Vec<String>>> {
let mut result = Vec::with_capacity(r_list.len());
for (n, s) in r_list.into_iter() {
let s_vec = s.as_string_vector().ok_or_else(|| {
Error::Other(format!(
"Failed to convert value to string vector at key '{}'",
n
))
})?;
result.push(s_vec);
}
Ok(result)
}
pub fn r_named_vec_data(named_vec: Robj) -> extendr_api::Result<NamedNumericVec> {
let values = named_vec
.as_real_vector()
.ok_or(NamedVecError::NotNumeric)?;
if values.iter().any(|v| v.is_nan()) {
return Err(NamedVecError::MissingValues.into());
}
let names_attr = named_vec.names().ok_or(NamedVecError::NoNames)?;
let names: Vec<&str> = names_attr.collect();
let names: Vec<String> = if names.len() >= PARALLEL_PARSE_THRESHOLD {
names.par_iter().map(|s| s.to_string()).collect()
} else {
names.iter().map(|s| s.to_string()).collect()
};
Ok((names, values))
}
pub fn r_matrix_to_faer<T>(x: &RMatrix<T>) -> MatRef<'_, T>
where
T: Copy + Clone,
extendr_api::Robj: for<'a> extendr_api::AsTypedSlice<'a, T>,
{
let ncol = x.ncols();
let nrow = x.nrows();
let data = x.data();
MatRef::from_column_major_slice(data, nrow, ncol)
}
pub fn r_matrix_to_vec_bool(x: &RMatrix<Rbool>) -> Vec<Vec<bool>> {
let ncol = x.ncols();
let nrow = x.nrows();
let data = x.data();
(0..ncol)
.map(|j| (0..nrow).map(|i| data[i + j * nrow].to_bool()).collect())
.collect()
}
pub fn faer_to_r_matrix<T>(x: MatRef<T>) -> extendr_api::RArray<T::RType, 2>
where
T: FaerRType,
{
T::to_r_matrix(x)
}
pub fn r_matrix_to_faer_fp32(x: &RMatrix<f64>) -> Mat<f32> {
let src = r_matrix_to_faer(x);
let (nrow, ncol) = (src.nrows(), src.ncols());
let mut out = Mat::<f32>::zeros(nrow, ncol);
if nrow == 0 || ncol == 0 {
return out;
}
let width = (FILL_CHUNK / nrow).max(1);
let height = nrow.min(FILL_CHUNK);
let fill = |(d, s): (faer::MatMut<f32>, MatRef<f64>)| {
for (d, s) in d.col_iter_mut().zip(s.col_iter()) {
let s = s
.try_as_col_major()
.expect("R matrices are dense")
.as_slice();
d.try_as_col_major_mut()
.expect("Mat columns are contiguous")
.as_slice_mut()
.iter_mut()
.zip(s)
.for_each(|(d, &s)| *d = s as f32);
}
};
if nrow * ncol >= PARALLEL_PARSE_THRESHOLD {
out.par_col_chunks_mut(width)
.zip(src.par_col_chunks(width))
.for_each(|(d, s)| {
d.par_row_chunks_mut(height)
.zip(s.par_row_chunks(height))
.for_each(fill)
});
} else {
fill((out.as_mut(), src));
}
out
}
pub fn sparse_data_to_list<T>(sparse: CompressedSparseData2<T>) -> Result<List, BixverseErrors>
where
T: Into<Robj> + Copy + Default + Into<f64> + Sync + Add + PartialEq + Mul,
{
let mut data = alloc_r_vector(Rtype::Doubles, sparse.data.len());
try_map_into(
data.as_real_slice_mut().expect("allocated as doubles"),
&sparse.data,
|v| Ok::<f64, BixverseErrors>(v.into()),
)?;
let indptr = index_buffer_to_r(
&sparse.indptr,
"indptr exceeds what R's integer vectors can hold",
)?;
let indices = index_buffer_to_r(
&sparse.indices,
"indices exceed what R's integer vectors can hold",
)?;
let nrow = i32::try_from(sparse.shape.0)
.map_err(|_| BixverseErrors::RListParse("nrow exceeds what R can represent"))?;
let ncol = i32::try_from(sparse.shape.1)
.map_err(|_| BixverseErrors::RListParse("ncol exceeds what R can represent"))?;
let cs_type = match sparse.cs_type {
CompressedSparseFormat::Csr => "csr",
CompressedSparseFormat::Csc => "csc",
};
Ok(list!(
data = data,
indptr = indptr,
indices = indices,
nrow = nrow,
ncol = ncol,
cs_type = cs_type.to_string()
))
}
fn r_dimension(value: Option<&Robj>, message: &'static str) -> Result<usize, BixverseErrors> {
let value = value.ok_or(BixverseErrors::RListParse(message))?;
let dimension = value
.as_integer()
.map(<f64 as From<i32>>::from)
.or_else(|| value.as_real())
.ok_or(BixverseErrors::RListParse(message))?;
if !dimension.is_finite() || dimension < 0.0 || dimension.fract() != 0.0 {
return Err(BixverseErrors::RListParse(message));
}
Ok(dimension as usize)
}
fn r_index_buffer<I>(
value: Option<&Robj>,
exclusive_bound: usize,
missing: &'static str,
invalid: &'static str,
) -> Result<Vec<I>, BixverseErrors>
where
I: TryFrom<i32> + Default + Clone + Send,
{
let slice = value
.and_then(|v| v.as_integer_slice())
.ok_or(BixverseErrors::RListParse(missing))?;
#[inline(always)]
fn convert<I>(
x: i32,
exclusive_bound: usize,
invalid: &'static str,
) -> Result<I, BixverseErrors>
where
I: TryFrom<i32>,
{
let index = I::try_from(x).map_err(|_| BixverseErrors::RListParse(invalid))?;
if (x as usize) >= exclusive_bound {
return Err(BixverseErrors::RListParse(invalid));
}
Ok(index)
}
let mut buffer = vec![I::default(); slice.len()];
try_map_into(&mut buffer, slice, |x| convert(x, exclusive_bound, invalid))?;
Ok(buffer)
}
fn r_value_buffer<T>(
value: Option<&Robj>,
missing: &'static str,
out_of_range: &'static str,
) -> Result<Vec<T>, BixverseErrors>
where
T: NumCast + Default + Clone + Send,
{
let slice = value
.and_then(|v| v.as_real_slice())
.ok_or(BixverseErrors::RListParse(missing))?;
let mut buffer = vec![T::default(); slice.len()];
try_map_into(&mut buffer, slice, |x| {
T::from(x).ok_or(BixverseErrors::RListParse(out_of_range))
})?;
Ok(buffer)
}
fn index_buffer_to_r(buffer: &[u32], message: &'static str) -> Result<Robj, BixverseErrors> {
let mut out = alloc_r_vector(Rtype::Integers, buffer.len());
try_map_into(
out.as_integer_slice_mut().expect("allocated as integers"),
buffer,
|x| {
if x > MAX_R_SPARSE_INDEX {
return Err(BixverseErrors::RListParse(message));
}
Ok(x as i32)
},
)?;
Ok(out)
}
pub fn list_to_sparse_matrix<T>(
r_list: List,
populate_data_2: bool,
) -> Result<CompressedSparseData2<T>, BixverseErrors>
where
T: Clone + Default + NumCast + Send,
{
if !r_list.is_empty() && r_list.names().is_none() {
return Err(BixverseErrors::RListParse("not a named list"));
}
let r_data: HashMap<&str, Robj> = r_list
.try_into()
.map_err(|_| BixverseErrors::RListParse("not a named list"))?;
let nrow = r_dimension(
r_data.get("nrow"),
"nrow missing or not a non-negative whole number",
)?;
let ncol = r_dimension(
r_data.get("ncol"),
"ncol missing or not a non-negative whole number",
)?;
let cs_type = r_data
.get("cs_type")
.and_then(|v| v.as_str())
.and_then(parse_compressed_sparse_format)
.ok_or(BixverseErrors::RListParse(
"cs_type missing or not one of 'csr' / 'csc'",
))?;
let data: Vec<T> = r_value_buffer(
r_data.get("data"),
"data missing or not double",
"data value out of range",
)?;
let indptr: Vec<u32> = r_index_buffer(
r_data.get("indptr"),
data.len() + 1,
"indptr missing or not integer",
"indptr holds NA, negative or out-of-range values",
)?;
let minor_dimension = match cs_type {
CompressedSparseFormat::Csr => ncol,
CompressedSparseFormat::Csc => nrow,
};
let indices: Vec<u32> = r_index_buffer(
r_data.get("indices"),
minor_dimension,
"indices missing or not integer",
"indices hold NA, negative or out-of-range values",
)?;
validate_sparse_layout(&indptr, &indices, data.len(), (nrow, ncol), cs_type)?;
let data_2 = if populate_data_2 {
Some(data.clone())
} else {
None
};
Ok(CompressedSparseData2 {
data,
indices,
indptr,
cs_type,
data_2,
shape: (nrow, ncol),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_sparse_layout_accepts_a_sound_csr() {
let indptr = [0u32, 2, 2, 3];
let indices = [1u32, 3, 0];
assert!(
validate_sparse_layout(&indptr, &indices, 3, (3, 4), CompressedSparseFormat::Csr)
.is_ok()
);
}
#[test]
fn test_validate_sparse_layout_reads_csc_the_other_way_round() {
let indptr = [0u32, 2, 2, 3];
let indices = [1u32, 3, 0];
assert!(
validate_sparse_layout(&indptr, &indices, 3, (4, 3), CompressedSparseFormat::Csc)
.is_ok()
);
assert!(
validate_sparse_layout(&indptr, &indices, 3, (4, 3), CompressedSparseFormat::Csr)
.is_err()
);
}
#[test]
fn test_validate_sparse_layout_catches_each_fault() {
assert!(
validate_sparse_layout(
&[0u32, 2],
&[0u32, 1],
1,
(1, 2),
CompressedSparseFormat::Csr
)
.is_err()
);
assert!(
validate_sparse_layout(&[0u32, 1], &[0u32], 1, (3, 2), CompressedSparseFormat::Csr)
.is_err()
);
assert!(
validate_sparse_layout(
&[0u32, 5, 2, 7],
&[0u32; 7],
7,
(3, 3),
CompressedSparseFormat::Csr
)
.is_err()
);
assert!(
validate_sparse_layout(
&[0u32, 1, 1],
&[0u32, 1],
2,
(2, 2),
CompressedSparseFormat::Csr
)
.is_err()
);
}
#[test]
fn test_validate_sparse_layout_accepts_an_empty_matrix() {
assert!(
validate_sparse_layout(&[0u32], &[], 0, (0, 0), CompressedSparseFormat::Csr).is_ok()
);
}
}