use extendr_api::prelude::*;
use faer::{Mat, MatRef};
use num_traits::NumCast;
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;
fn r_index_slice(
value: Option<&Robj>,
missing: &'static str,
invalid: &'static str,
) -> Result<Vec<usize>, BixverseErrors> {
let raw = value
.and_then(|v| v.as_integer_slice())
.ok_or(BixverseErrors::RListParse(missing))?;
raw.iter()
.map(|&x| {
if x < 0 {
Err(BixverseErrors::RListParse(invalid))
} else {
Ok(x as usize)
}
})
.collect()
}
fn r_dimension_zero(value: Option<&Robj>) -> Option<usize> {
let value = value?;
let is_zero = value.as_integer().map(|v| v == 0).unwrap_or(false)
|| value.as_real().map(|v| v == 0.0).unwrap_or(false);
is_zero.then_some(0)
}
fn validate_sparse_layout(
indptr: &[usize],
indices: &[usize],
nnz: usize,
shape: (usize, usize),
cs_type: CompressedSparseFormat,
) -> Result<(), BixverseErrors> {
let (major, minor) = match cs_type {
CompressedSparseFormat::Csr => shape,
CompressedSparseFormat::Csc => (shape.1, shape.0),
};
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 {
return Err(BixverseErrors::RListParse(
"the last indptr entry must equal the number of stored values",
));
}
if indices.iter().any(|&j| j >= minor) {
return Err(BixverseErrors::RListParse(
"an index sits outside the minor dimension",
));
}
Ok(())
}
#[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_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<String> = names_attr.into_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 ncol = x.ncols();
let nrow = x.nrows();
let data = x.data();
let data_fp32 = data.iter().map(|x| *x as f32).collect::<Vec<f32>>();
Mat::from_fn(nrow, ncol, |i, j| data_fp32[i + j * nrow])
}
pub fn sparse_data_to_list<T>(sparse: CompressedSparseData2<T>) -> Result<List, BixverseErrors>
where
T: Into<Robj> + Clone + Default + Into<f64> + Sync + Add + PartialEq + Mul,
{
let data: Vec<f64> = sparse.data.into_iter().map(Into::into).collect();
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 index_buffer_to_r(buffer: &[u32], message: &'static str) -> Result<Vec<i32>, BixverseErrors> {
if buffer.iter().any(|&x| x > MAX_R_SPARSE_INDEX) {
return Err(BixverseErrors::RListParse(message));
}
Ok(buffer.iter().map(|&x| x as i32).collect())
}
pub fn list_to_sparse_matrix<T>(
r_list: List,
populate_data_2: bool,
) -> Result<CompressedSparseData2<T>, BixverseErrors>
where
T: Clone + Default + NumCast,
{
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 indptr = r_index_slice(
r_data.get("indptr"),
"indptr missing or not integer",
"indptr holds NA or negative values",
)?;
let indices = r_index_slice(
r_data.get("indices"),
"indices missing or not integer",
"indices hold NA or negative values",
)?;
let data: Vec<T> = r_data
.get("data")
.and_then(|v| v.as_real_slice())
.ok_or(BixverseErrors::RListParse("data missing or not double"))?
.iter()
.map(|&x| T::from(x).ok_or(BixverseErrors::RListParse("data value out of range")))
.collect::<Result<Vec<T>, _>>()?;
let nrow = r_list_count(&r_data, "nrow")
.ok()
.flatten()
.or_else(|| r_dimension_zero(r_data.get("nrow")))
.ok_or(BixverseErrors::RListParse(
"nrow missing or not a non-negative whole number",
))?;
let ncol = r_list_count(&r_data, "ncol")
.ok()
.flatten()
.or_else(|| r_dimension_zero(r_data.get("ncol")))
.ok_or(BixverseErrors::RListParse(
"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'",
))?;
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: indices.index_cast(),
indptr: indptr.index_cast(),
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 = [0usize, 2, 2, 3];
let indices = [1usize, 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 = [0usize, 2, 2, 3];
let indices = [1usize, 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(
&[0usize, 2],
&[0usize, 1],
1,
(1, 2),
CompressedSparseFormat::Csr
)
.is_err()
);
assert!(
validate_sparse_layout(
&[0usize, 1],
&[0usize],
1,
(3, 2),
CompressedSparseFormat::Csr
)
.is_err()
);
assert!(
validate_sparse_layout(
&[0usize, 5, 2, 7],
&[0usize; 7],
7,
(3, 3),
CompressedSparseFormat::Csr
)
.is_err()
);
assert!(
validate_sparse_layout(
&[0usize, 1, 1],
&[0usize, 1],
2,
(2, 2),
CompressedSparseFormat::Csr
)
.is_err()
);
assert!(
validate_sparse_layout(
&[0usize, 2],
&[0usize, 9],
2,
(1, 2),
CompressedSparseFormat::Csr
)
.is_err()
);
}
#[test]
fn test_validate_sparse_layout_accepts_an_empty_matrix() {
assert!(
validate_sparse_layout(&[0usize], &[], 0, (0, 0), CompressedSparseFormat::Csr).is_ok()
);
}
}