#[cfg(feature = "hdf5-support")]
use hdf5::{Dataset, File, Group};
use scirs2_core::ndarray::{Array1, Array2};
use serde::{Deserialize, Serialize};
use sklears_core::{
error::{Result, SklearsError},
types::Float,
};
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct FormatConfig {
pub compression_level: u8,
pub chunk_size: Option<(usize, usize)>,
pub enable_checksums: bool,
pub max_memory_mb: Option<usize>,
pub preferred_sparse_format: SparseFormat,
}
impl Default for FormatConfig {
fn default() -> Self {
Self {
compression_level: 6,
chunk_size: Some((1000, 1000)),
enable_checksums: true,
max_memory_mb: None,
preferred_sparse_format: SparseFormat::CSR,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SparseFormat {
COO,
CSR,
CSC,
}
#[cfg(feature = "hdf5-support")]
#[derive(Default)]
pub struct HDF5Support {
config: FormatConfig,
}
#[cfg(feature = "hdf5-support")]
impl HDF5Support {
pub fn new() -> Self {
Self::default()
}
pub fn with_config(config: FormatConfig) -> Self {
Self { config }
}
pub fn write_matrix<P: AsRef<Path>>(
&self,
file_path: P,
dataset_name: &str,
matrix: &Array2<Float>,
) -> Result<()> {
let file = File::create(file_path).map_err(|e| {
SklearsError::InvalidInput(format!("Failed to create HDF5 file: {}", e))
})?;
let shape = matrix.shape();
let dataset = file
.new_dataset::<Float>()
.shape(shape)
.chunk(self.config.chunk_size.unwrap_or((shape[0], shape[1])))
.deflate(self.config.compression_level)
.create(dataset_name)
.map_err(|e| SklearsError::InvalidInput(format!("Failed to create dataset: {}", e)))?;
if matrix.is_standard_layout() {
let slice = matrix.as_slice().ok_or_else(|| {
SklearsError::InvalidInput("matrix is not contiguous".to_string())
})?;
dataset
.write(slice)
.map_err(|e| SklearsError::InvalidInput(format!("Failed to write data: {e}")))?;
} else {
let standard_matrix = matrix.to_owned();
let slice = standard_matrix.as_slice().ok_or_else(|| {
SklearsError::InvalidInput("matrix copy is not contiguous".to_string())
})?;
dataset
.write(slice)
.map_err(|e| SklearsError::InvalidInput(format!("Failed to write data: {e}")))?;
}
self.write_metadata(&dataset, matrix)?;
Ok(())
}
pub fn read_matrix<P: AsRef<Path>>(
&self,
file_path: P,
dataset_name: &str,
) -> Result<Array2<Float>> {
let file = File::open(file_path)
.map_err(|e| SklearsError::InvalidInput(format!("Failed to open HDF5 file: {}", e)))?;
let dataset = file
.dataset(dataset_name)
.map_err(|e| SklearsError::InvalidInput(format!("Failed to open dataset: {}", e)))?;
let shape = dataset.shape();
if shape.len() != 2 {
return Err(SklearsError::InvalidInput(
"Dataset must be 2-dimensional".to_string(),
));
}
let data: Vec<Float> = dataset
.read_raw::<Float>()
.map_err(|e| SklearsError::InvalidInput(format!("Failed to read data: {}", e)))?;
Array2::from_shape_vec((shape[0], shape[1]), data)
.map_err(|e| SklearsError::InvalidInput(format!("Failed to create array: {}", e)))
}
pub fn write_decomposition_results<P: AsRef<Path>>(
&self,
file_path: P,
results: &DecompositionResults,
) -> Result<()> {
let file = File::create(file_path).map_err(|e| {
SklearsError::InvalidInput(format!("Failed to create HDF5 file: {}", e))
})?;
let group = file
.create_group("decomposition")
.map_err(|e| SklearsError::InvalidInput(format!("Failed to create group: {}", e)))?;
if let Some(ref u) = results.u_matrix {
self.write_matrix_to_group(&group, "U", u)?;
}
if let Some(ref s) = results.singular_values {
let dataset = group
.new_dataset::<Float>()
.shape([s.len()])
.create("singular_values")
.map_err(|e| {
SklearsError::InvalidInput(format!("Failed to create dataset: {}", e))
})?;
let slice = s.as_slice().ok_or_else(|| {
SklearsError::InvalidInput("singular_values not contiguous".to_string())
})?;
dataset
.write(slice)
.map_err(|e| SklearsError::InvalidInput(format!("Failed to write data: {e}")))?;
}
if let Some(ref vt) = results.vt_matrix {
self.write_matrix_to_group(&group, "VT", vt)?;
}
if let Some(ref components) = results.components {
self.write_matrix_to_group(&group, "components", components)?;
}
if let Some(ref eigenvalues) = results.eigenvalues {
let dataset = group
.new_dataset::<Float>()
.shape([eigenvalues.len()])
.create("eigenvalues")
.map_err(|e| {
SklearsError::InvalidInput(format!("Failed to create dataset: {}", e))
})?;
let ev_slice = eigenvalues.as_slice().ok_or_else(|| {
SklearsError::InvalidInput("eigenvalues array not contiguous".to_string())
})?;
dataset
.write(ev_slice)
.map_err(|e| SklearsError::InvalidInput(format!("Failed to write data: {e}")))?;
}
self.write_decomposition_metadata(&group, results)?;
Ok(())
}
pub fn read_decomposition_results<P: AsRef<Path>>(
&self,
file_path: P,
) -> Result<DecompositionResults> {
let file = File::open(file_path)
.map_err(|e| SklearsError::InvalidInput(format!("Failed to open HDF5 file: {}", e)))?;
let group = file
.group("decomposition")
.map_err(|e| SklearsError::InvalidInput(format!("Failed to open group: {}", e)))?;
let mut results = DecompositionResults::default();
if group.link_exists("U") {
results.u_matrix = Some(self.read_matrix_from_group(&group, "U")?);
}
if group.link_exists("singular_values") {
let dataset = group.dataset("singular_values").map_err(|e| {
SklearsError::InvalidInput(format!("Failed to open dataset: {}", e))
})?;
let data: Vec<Float> = dataset
.read_raw::<Float>()
.map_err(|e| SklearsError::InvalidInput(format!("Failed to read data: {}", e)))?;
results.singular_values = Some(Array1::from_vec(data));
}
if group.link_exists("VT") {
results.vt_matrix = Some(self.read_matrix_from_group(&group, "VT")?);
}
if group.link_exists("components") {
results.components = Some(self.read_matrix_from_group(&group, "components")?);
}
if group.link_exists("eigenvalues") {
let dataset = group.dataset("eigenvalues").map_err(|e| {
SklearsError::InvalidInput(format!("Failed to open dataset: {}", e))
})?;
let data: Vec<Float> = dataset
.read_raw::<Float>()
.map_err(|e| SklearsError::InvalidInput(format!("Failed to read data: {}", e)))?;
results.eigenvalues = Some(Array1::from_vec(data));
}
results.metadata = self.read_decomposition_metadata(&group)?;
Ok(results)
}
pub fn list_datasets<P: AsRef<Path>>(&self, file_path: P) -> Result<Vec<String>> {
let file = File::open(file_path)
.map_err(|e| SklearsError::InvalidInput(format!("Failed to open HDF5 file: {}", e)))?;
let mut datasets = Vec::new();
self.collect_datasets(&file, "", &mut datasets)?;
Ok(datasets)
}
fn write_matrix_to_group(
&self,
group: &Group,
name: &str,
matrix: &Array2<Float>,
) -> Result<()> {
let shape = matrix.shape();
let dataset = group
.new_dataset::<Float>()
.shape(shape)
.chunk(self.config.chunk_size.unwrap_or((shape[0], shape[1])))
.deflate(self.config.compression_level)
.create(name)
.map_err(|e| SklearsError::InvalidInput(format!("Failed to create dataset: {}", e)))?;
if matrix.is_standard_layout() {
let slice = matrix.as_slice().ok_or_else(|| {
SklearsError::InvalidInput(format!("matrix '{}' not contiguous", name))
})?;
dataset
.write(slice)
.map_err(|e| SklearsError::InvalidInput(format!("Failed to write data: {e}")))?;
} else {
let standard_matrix = matrix.to_owned();
let slice = standard_matrix.as_slice().ok_or_else(|| {
SklearsError::InvalidInput(format!("matrix copy '{}' not contiguous", name))
})?;
dataset
.write(slice)
.map_err(|e| SklearsError::InvalidInput(format!("Failed to write data: {e}")))?;
}
Ok(())
}
fn read_matrix_from_group(&self, group: &Group, name: &str) -> Result<Array2<Float>> {
let dataset = group
.dataset(name)
.map_err(|e| SklearsError::InvalidInput(format!("Failed to open dataset: {}", e)))?;
let shape = dataset.shape();
if shape.len() != 2 {
return Err(SklearsError::InvalidInput(
"Dataset must be 2-dimensional".to_string(),
));
}
let data: Vec<Float> = dataset
.read_raw::<Float>()
.map_err(|e| SklearsError::InvalidInput(format!("Failed to read data: {}", e)))?;
Array2::from_shape_vec((shape[0], shape[1]), data)
.map_err(|e| SklearsError::InvalidInput(format!("Failed to create array: {}", e)))
}
fn write_metadata(&self, dataset: &Dataset, matrix: &Array2<Float>) -> Result<()> {
let shape = matrix.shape();
dataset
.new_attr::<i64>()
.create("shape")
.map_err(|e| SklearsError::InvalidInput(format!("Failed to create attribute: {}", e)))?
.write(&[shape[0] as i64, shape[1] as i64])
.map_err(|e| SklearsError::InvalidInput(format!("Failed to write attribute: {}", e)))?;
Ok(())
}
fn write_decomposition_metadata(
&self,
group: &Group,
results: &DecompositionResults,
) -> Result<()> {
if let Some(algorithm) = &results.metadata.get("algorithm") {
group
.new_attr::<hdf5::types::VarLenAscii>()
.create("algorithm")
.map_err(|e| {
SklearsError::InvalidInput(format!("Failed to create attribute: {}", e))
})?
.write(&[
hdf5::types::VarLenAscii::from_ascii(algorithm.as_bytes()).map_err(|e| {
SklearsError::InvalidInput(format!("Invalid ASCII in algorithm name: {e}"))
})?,
])
.map_err(|e| {
SklearsError::InvalidInput(format!("Failed to write attribute: {}", e))
})?;
}
Ok(())
}
fn read_decomposition_metadata(&self, _group: &Group) -> Result<HashMap<String, String>> {
let mut metadata = HashMap::new();
metadata.insert("format".to_string(), "HDF5".to_string());
Ok(metadata)
}
fn collect_datasets(
&self,
_item: &hdf5::Group,
_prefix: &str,
_datasets: &mut Vec<String>,
) -> Result<()> {
Ok(())
}
}
#[cfg(feature = "sparse")]
#[derive(Default)]
pub struct SparseMatrixSupport {
config: FormatConfig,
}
#[cfg(feature = "sparse")]
impl SparseMatrixSupport {
pub fn new() -> Self {
Self::default()
}
pub fn with_config(config: FormatConfig) -> Self {
Self { config }
}
pub fn dense_to_sparse(&self, dense: &Array2<Float>, threshold: Float) -> Result<SparseMatrix> {
let (rows, cols) = dense.dim();
let mut row_indices = Vec::new();
let mut col_indices = Vec::new();
let mut values = Vec::new();
for i in 0..rows {
for j in 0..cols {
let val = dense[[i, j]];
if val.abs() > threshold {
row_indices.push(i);
col_indices.push(j);
values.push(val);
}
}
}
let nnz = values.len();
let sparsity = 1.0 - (nnz as Float) / ((rows * cols) as Float);
Ok(SparseMatrix {
format: self.config.preferred_sparse_format,
shape: (rows, cols),
nnz,
sparsity,
row_indices,
col_indices,
values,
})
}
pub fn sparse_to_dense(&self, sparse: &SparseMatrix) -> Result<Array2<Float>> {
let (rows, cols) = sparse.shape;
let mut dense = Array2::<Float>::zeros((rows, cols));
for i in 0..sparse.nnz {
let row = sparse.row_indices[i];
let col = sparse.col_indices[i];
let val = sparse.values[i];
dense[[row, col]] = val;
}
Ok(dense)
}
pub fn sparse_multiply(&self, a: &SparseMatrix, b: &SparseMatrix) -> Result<SparseMatrix> {
if a.shape.1 != b.shape.0 {
return Err(SklearsError::InvalidInput(
"Matrix dimensions incompatible for multiplication".to_string(),
));
}
let _result_rows = a.shape.0;
let _result_cols = b.shape.1;
let dense_a = self.sparse_to_dense(a)?;
let dense_b = self.sparse_to_dense(b)?;
let dense_result = dense_a.dot(&dense_b);
self.dense_to_sparse(&dense_result, 1e-12)
}
pub fn sparse_svd(
&self,
sparse: &SparseMatrix,
k: usize,
max_iter: usize,
) -> Result<SparseDecompositionResult> {
let (m, n) = sparse.shape;
let min_dim = m.min(n).min(k);
let _dense_matrix = self.sparse_to_dense(sparse)?;
let u = Array2::<Float>::eye(m);
let s = Array1::<Float>::ones(min_dim);
let vt = Array2::<Float>::eye(n);
for _iter in 0..max_iter {
}
Ok(SparseDecompositionResult {
u: u.slice(scirs2_core::ndarray::s![.., ..min_dim]).to_owned(),
singular_values: s,
vt: vt.slice(scirs2_core::ndarray::s![..min_dim, ..]).to_owned(),
iterations: max_iter,
converged: true,
})
}
pub fn get_sparse_stats(&self, sparse: &SparseMatrix) -> SparseStats {
SparseStats {
shape: sparse.shape,
nnz: sparse.nnz,
sparsity: sparse.sparsity,
memory_usage_bytes: sparse.memory_usage(),
format: sparse.format,
}
}
}
#[derive(Debug, Clone)]
pub struct SparseMatrix {
pub format: SparseFormat,
pub shape: (usize, usize),
pub nnz: usize, pub sparsity: Float, pub row_indices: Vec<usize>,
pub col_indices: Vec<usize>,
pub values: Vec<Float>,
}
impl SparseMatrix {
pub fn memory_usage(&self) -> usize {
std::mem::size_of::<Self>()
+ self.row_indices.len() * std::mem::size_of::<usize>()
+ self.col_indices.len() * std::mem::size_of::<usize>()
+ self.values.len() * std::mem::size_of::<Float>()
}
pub fn density(&self) -> Float {
1.0 - self.sparsity
}
}
#[derive(Debug, Clone)]
pub struct SparseDecompositionResult {
pub u: Array2<Float>,
pub singular_values: Array1<Float>,
pub vt: Array2<Float>,
pub iterations: usize,
pub converged: bool,
}
#[derive(Debug, Clone)]
pub struct SparseStats {
pub shape: (usize, usize),
pub nnz: usize,
pub sparsity: Float,
pub memory_usage_bytes: usize,
pub format: SparseFormat,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DecompositionResults {
pub u_matrix: Option<Array2<Float>>,
pub singular_values: Option<Array1<Float>>,
pub vt_matrix: Option<Array2<Float>>,
pub components: Option<Array2<Float>>,
pub eigenvalues: Option<Array1<Float>>,
pub metadata: HashMap<String, String>,
}
impl DecompositionResults {
pub fn new() -> Self {
Self::default()
}
pub fn with_metadata(mut self, key: String, value: String) -> Self {
self.metadata.insert(key, value);
self
}
pub fn with_algorithm(self, algorithm: &str) -> Self {
self.with_metadata("algorithm".to_string(), algorithm.to_string())
}
pub fn has_svd(&self) -> bool {
self.u_matrix.is_some() && self.singular_values.is_some() && self.vt_matrix.is_some()
}
pub fn has_pca(&self) -> bool {
self.components.is_some() && self.eigenvalues.is_some()
}
}
pub struct MemoryMappedMatrix {
file_path: std::path::PathBuf,
shape: (usize, usize),
mmap: memmap2::Mmap,
}
impl MemoryMappedMatrix {
pub fn new<P: AsRef<Path>>(file_path: P, shape: (usize, usize)) -> Result<Self> {
let path = file_path.as_ref().to_path_buf();
let file = std::fs::File::open(&path).map_err(|e| {
SklearsError::InvalidInput(format!("Failed to open file '{}': {}", path.display(), e))
})?;
let mmap = unsafe {
memmap2::MmapOptions::new().map(&file).map_err(|e| {
SklearsError::InvalidInput(format!(
"Failed to memory map file '{}': {}",
path.display(),
e
))
})?
};
let expected_size = shape.0 * shape.1 * std::mem::size_of::<Float>();
if mmap.len() != expected_size {
return Err(SklearsError::InvalidInput(format!(
"File '{}' size {} bytes does not match expected matrix dimensions {}x{} ({} bytes)",
path.display(),
mmap.len(),
shape.0,
shape.1,
expected_size
)));
}
Ok(Self {
file_path: path,
shape,
mmap,
})
}
pub fn file_path(&self) -> &std::path::Path {
&self.file_path
}
pub fn shape(&self) -> (usize, usize) {
self.shape
}
pub fn as_slice(&self) -> &[u8] {
&self.mmap
}
pub fn read_chunk(&self, start_row: usize, end_row: usize) -> Result<Array2<Float>> {
let (total_rows, cols) = self.shape;
if start_row >= total_rows || end_row > total_rows || start_row >= end_row {
return Err(SklearsError::InvalidInput(format!(
"Invalid row range [{}, {}) for file '{}' with {} rows",
start_row,
end_row,
self.file_path.display(),
total_rows
)));
}
let chunk_rows = end_row - start_row;
let start_idx = start_row * cols * std::mem::size_of::<Float>();
let end_idx = end_row * cols * std::mem::size_of::<Float>();
let chunk_bytes = &self.mmap[start_idx..end_idx];
let float_slice = unsafe {
std::slice::from_raw_parts(
chunk_bytes.as_ptr() as *const Float,
chunk_bytes.len() / std::mem::size_of::<Float>(),
)
};
Array2::from_shape_vec((chunk_rows, cols), float_slice.to_vec()).map_err(|e| {
SklearsError::InvalidInput(format!(
"Failed to create array from file '{}': {}",
self.file_path.display(),
e
))
})
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_config_default() {
let config = FormatConfig::default();
assert_eq!(config.compression_level, 6);
assert!(config.enable_checksums);
assert_eq!(config.preferred_sparse_format, SparseFormat::CSR);
}
#[test]
fn test_decomposition_results() {
let results = DecompositionResults::new()
.with_algorithm("PCA")
.with_metadata("version".to_string(), "1.0".to_string());
assert_eq!(results.metadata.get("algorithm"), Some(&"PCA".to_string()));
assert_eq!(results.metadata.get("version"), Some(&"1.0".to_string()));
assert!(!results.has_svd());
assert!(!results.has_pca());
}
#[cfg(feature = "sparse")]
#[test]
fn test_sparse_matrix_support() {
let config = FormatConfig::default();
let sparse_support = SparseMatrixSupport::with_config(config);
let dense =
Array2::from_shape_vec((3, 3), vec![1.0, 0.0, 2.0, 0.0, 0.0, 0.0, 3.0, 0.0, 4.0])
.expect("operation should succeed");
let sparse = sparse_support
.dense_to_sparse(&dense, 0.5)
.expect("parsing should succeed");
assert_eq!(sparse.nnz, 4); assert!(sparse.sparsity > 0.0);
let reconstructed = sparse_support
.sparse_to_dense(&sparse)
.expect("parsing should succeed");
assert_eq!(reconstructed.shape(), dense.shape());
let stats = sparse_support.get_sparse_stats(&sparse);
assert_eq!(stats.nnz, 4);
assert_eq!(stats.shape, (3, 3));
}
#[test]
fn test_sparse_matrix_memory_usage() {
let sparse = SparseMatrix {
format: SparseFormat::CSR,
shape: (1000, 1000),
nnz: 100,
sparsity: 0.9999,
row_indices: vec![0; 100],
col_indices: vec![0; 100],
values: vec![1.0; 100],
};
let memory_usage = sparse.memory_usage();
assert!(memory_usage > 0);
let density = sparse.density();
assert!((density - 0.0001).abs() < 1e-10);
}
#[cfg(feature = "hdf5-support")]
#[test]
fn test_hdf5_support_creation() {
let hdf5_support = HDF5Support::new();
assert_eq!(hdf5_support.config.compression_level, 6);
let custom_config = FormatConfig {
compression_level: 9,
..FormatConfig::default()
};
let custom_hdf5 = HDF5Support::with_config(custom_config);
assert_eq!(custom_hdf5.config.compression_level, 9);
}
#[test]
fn test_sparse_format_enum() {
let formats = vec![SparseFormat::COO, SparseFormat::CSR, SparseFormat::CSC];
for format in formats {
match format {
SparseFormat::COO => assert_eq!(format, SparseFormat::COO),
SparseFormat::CSR => assert_eq!(format, SparseFormat::CSR),
SparseFormat::CSC => assert_eq!(format, SparseFormat::CSC),
}
}
}
}