use scirs2_core::ndarray::{Array1, Array2};
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::path::Path;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::thread;
use crate::error::{IoError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MMDataType {
Real,
Complex,
Integer,
Pattern,
}
impl FromStr for MMDataType {
type Err = IoError;
fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"real" => Ok(MMDataType::Real),
"complex" => Ok(MMDataType::Complex),
"integer" => Ok(MMDataType::Integer),
"pattern" => Ok(MMDataType::Pattern),
_ => Err(IoError::FormatError(format!("Unknown data type: {}", s))),
}
}
}
impl std::fmt::Display for MMDataType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MMDataType::Real => write!(f, "real"),
MMDataType::Complex => write!(f, "complex"),
MMDataType::Integer => write!(f, "integer"),
MMDataType::Pattern => write!(f, "pattern"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MMFormat {
Coordinate,
Array,
}
impl FromStr for MMFormat {
type Err = IoError;
fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"coordinate" => Ok(MMFormat::Coordinate),
"array" => Ok(MMFormat::Array),
_ => Err(IoError::FormatError(format!("Unknown format: {}", s))),
}
}
}
impl std::fmt::Display for MMFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MMFormat::Coordinate => write!(f, "coordinate"),
MMFormat::Array => write!(f, "array"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MMSymmetry {
General,
Symmetric,
Hermitian,
SkewSymmetric,
}
impl FromStr for MMSymmetry {
type Err = IoError;
fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"general" => Ok(MMSymmetry::General),
"symmetric" => Ok(MMSymmetry::Symmetric),
"hermitian" => Ok(MMSymmetry::Hermitian),
"skew-symmetric" => Ok(MMSymmetry::SkewSymmetric),
_ => Err(IoError::FormatError(format!("Unknown symmetry: {}", s))),
}
}
}
impl std::fmt::Display for MMSymmetry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MMSymmetry::General => write!(f, "general"),
MMSymmetry::Symmetric => write!(f, "symmetric"),
MMSymmetry::Hermitian => write!(f, "hermitian"),
MMSymmetry::SkewSymmetric => write!(f, "skew-symmetric"),
}
}
}
#[derive(Debug, Clone)]
pub struct MMHeader {
pub object: String,
pub format: MMFormat,
pub data_type: MMDataType,
pub symmetry: MMSymmetry,
pub comments: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct SparseEntry<T> {
pub row: usize,
pub col: usize,
pub value: T,
}
#[derive(Debug, Clone)]
pub struct MMSparseMatrix<T> {
pub header: MMHeader,
pub rows: usize,
pub cols: usize,
pub nnz: usize,
pub entries: Vec<SparseEntry<T>>,
}
#[derive(Debug, Clone)]
pub struct MMDenseMatrix<T> {
pub header: MMHeader,
pub rows: usize,
pub cols: usize,
pub data: Array2<T>,
}
#[derive(Debug, Clone)]
pub struct ParallelConfig {
pub num_threads: usize,
pub chunk_size: usize,
pub buffer_size: usize,
pub use_memory_mapping: bool,
}
impl Default for ParallelConfig {
fn default() -> Self {
Self {
num_threads: thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4),
chunk_size: 100_000, buffer_size: 64 * 1024 * 1024, use_memory_mapping: false,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct IOStats {
pub io_time_ms: f64,
pub entries_processed: usize,
pub throughput_eps: f64,
pub memory_usage_bytes: usize,
}
impl MMHeader {
pub fn parse_header(line: &str) -> Result<Self> {
if !line.starts_with("%%MatrixMarket") {
return Err(IoError::FormatError(
"Invalid Matrix Market header".to_string(),
));
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() != 5 {
return Err(IoError::FormatError(
"Invalid Matrix Market header format".to_string(),
));
}
let object = parts[1].to_string();
let format = MMFormat::from_str(parts[2])?;
let data_type = MMDataType::from_str(parts[3])?;
let symmetry = MMSymmetry::from_str(parts[4])?;
if object.to_lowercase() != "matrix" {
return Err(IoError::FormatError(format!(
"Unsupported object type: {}",
object
)));
}
Ok(MMHeader {
object,
format,
data_type,
symmetry,
comments: Vec::new(),
})
}
pub fn to_header_line(&self) -> String {
format!(
"%%MatrixMarket {} {} {} {}",
self.object, self.format, self.data_type, self.symmetry
)
}
}
#[allow(dead_code)]
pub fn read_sparse_matrix<P: AsRef<Path>>(path: P) -> Result<MMSparseMatrix<f64>> {
let file = File::open(path).map_err(|e| IoError::FileError(e.to_string()))?;
let reader = BufReader::new(file);
let mut lines = reader.lines();
let header_line = lines
.next()
.ok_or_else(|| IoError::FormatError("Empty file".to_string()))?
.map_err(|e| IoError::FileError(e.to_string()))?;
let mut header = MMHeader::parse_header(&header_line)?;
for line in &mut lines {
let line = line.map_err(|e| IoError::FileError(e.to_string()))?;
if line.starts_with('%') {
header.comments.push(
line.strip_prefix('%')
.expect("Operation failed")
.trim()
.to_string(),
);
} else {
let size_parts: Vec<&str> = line.split_whitespace().collect();
if size_parts.len() < 2 {
return Err(IoError::FormatError("Invalid size line format".to_string()));
}
let rows = size_parts[0]
.parse::<usize>()
.map_err(|_| IoError::FormatError("Invalid row count".to_string()))?;
let cols = size_parts[1]
.parse::<usize>()
.map_err(|_| IoError::FormatError("Invalid column count".to_string()))?;
let nnz = if size_parts.len() > 2 {
size_parts[2]
.parse::<usize>()
.map_err(|_| IoError::FormatError("Invalid nnz count".to_string()))?
} else {
rows * cols
};
if header.format != MMFormat::Coordinate {
return Err(IoError::FormatError(
"Only coordinate format is supported for sparse matrices".to_string(),
));
}
let mut entries = Vec::with_capacity(nnz);
for line in lines {
let line = line.map_err(|e| IoError::FileError(e.to_string()))?;
let line = line.trim();
if line.is_empty() {
continue;
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 2 {
return Err(IoError::FormatError("Invalid entry format".to_string()));
}
let row = parts[0]
.parse::<usize>()
.map_err(|_| IoError::FormatError("Invalid row index".to_string()))?
- 1; let col = parts[1]
.parse::<usize>()
.map_err(|_| IoError::FormatError("Invalid column index".to_string()))?
- 1;
let value = if header.data_type == MMDataType::Pattern {
1.0 } else if parts.len() > 2 {
parts[2]
.parse::<f64>()
.map_err(|_| IoError::FormatError("Invalid value".to_string()))?
} else {
return Err(IoError::FormatError(
"Missing value for non-pattern matrix".to_string(),
));
};
entries.push(SparseEntry { row, col, value });
}
return Ok(MMSparseMatrix {
header,
rows,
cols,
nnz,
entries,
});
}
}
Err(IoError::FormatError("Missing size information".to_string()))
}
#[allow(dead_code)]
pub fn write_sparse_matrix<P: AsRef<Path>>(path: P, matrix: &MMSparseMatrix<f64>) -> Result<()> {
let file = File::create(path).map_err(|e| IoError::FileError(e.to_string()))?;
let mut writer = BufWriter::new(file);
writeln!(writer, "{}", matrix.header.to_header_line())
.map_err(|e| IoError::FileError(e.to_string()))?;
for comment in &matrix.header.comments {
writeln!(writer, "%{}", comment).map_err(|e| IoError::FileError(e.to_string()))?;
}
writeln!(writer, "{} {} {}", matrix.rows, matrix.cols, matrix.nnz)
.map_err(|e| IoError::FileError(e.to_string()))?;
for entry in &matrix.entries {
if matrix.header.data_type == MMDataType::Pattern {
writeln!(writer, "{} {}", entry.row + 1, entry.col + 1)
.map_err(|e| IoError::FileError(e.to_string()))?;
} else {
writeln!(
writer,
"{} {} {}",
entry.row + 1,
entry.col + 1,
entry.value
)
.map_err(|e| IoError::FileError(e.to_string()))?;
}
}
writer
.flush()
.map_err(|e| IoError::FileError(e.to_string()))?;
Ok(())
}
#[allow(dead_code)]
pub fn read_dense_matrix<P: AsRef<Path>>(path: P) -> Result<MMDenseMatrix<f64>> {
let file = File::open(path).map_err(|e| IoError::FileError(e.to_string()))?;
let reader = BufReader::new(file);
let mut lines = reader.lines();
let header_line = lines
.next()
.ok_or_else(|| IoError::FormatError("Empty file".to_string()))?
.map_err(|e| IoError::FileError(e.to_string()))?;
let mut header = MMHeader::parse_header(&header_line)?;
if header.format != MMFormat::Array {
return Err(IoError::FormatError(
"Only array format is supported for dense matrices".to_string(),
));
}
for line in &mut lines {
let line = line.map_err(|e| IoError::FileError(e.to_string()))?;
if line.starts_with('%') {
header.comments.push(
line.strip_prefix('%')
.expect("Operation failed")
.trim()
.to_string(),
);
} else {
let size_parts: Vec<&str> = line.split_whitespace().collect();
if size_parts.len() < 2 {
return Err(IoError::FormatError("Invalid size line format".to_string()));
}
let rows = size_parts[0]
.parse::<usize>()
.map_err(|_| IoError::FormatError("Invalid row count".to_string()))?;
let cols = size_parts[1]
.parse::<usize>()
.map_err(|_| IoError::FormatError("Invalid column count".to_string()))?;
let mut data = Vec::with_capacity(rows * cols);
for line in lines {
let line = line.map_err(|e| IoError::FileError(e.to_string()))?;
let line = line.trim();
if line.is_empty() {
continue;
}
let value = line
.parse::<f64>()
.map_err(|_| IoError::FormatError("Invalid matrix value".to_string()))?;
data.push(value);
}
if data.len() != rows * cols {
return Err(IoError::FormatError(format!(
"Expected {} values, got {}",
rows * cols,
data.len()
)));
}
let mut matrix_data = Array2::zeros((rows, cols));
for col in 0..cols {
for row in 0..rows {
matrix_data[[row, col]] = data[col * rows + row];
}
}
return Ok(MMDenseMatrix {
header,
rows,
cols,
data: matrix_data,
});
}
}
Err(IoError::FormatError("Missing size information".to_string()))
}
#[allow(dead_code)]
pub fn write_dense_matrix<P: AsRef<Path>>(path: P, matrix: &MMDenseMatrix<f64>) -> Result<()> {
let file = File::create(path).map_err(|e| IoError::FileError(e.to_string()))?;
let mut writer = BufWriter::new(file);
writeln!(writer, "{}", matrix.header.to_header_line())
.map_err(|e| IoError::FileError(e.to_string()))?;
for comment in &matrix.header.comments {
writeln!(writer, "%{}", comment).map_err(|e| IoError::FileError(e.to_string()))?;
}
writeln!(writer, "{} {}", matrix.rows, matrix.cols)
.map_err(|e| IoError::FileError(e.to_string()))?;
for col in 0..matrix.cols {
for row in 0..matrix.rows {
writeln!(writer, "{}", matrix.data[[row, col]])
.map_err(|e| IoError::FileError(e.to_string()))?;
}
}
writer
.flush()
.map_err(|e| IoError::FileError(e.to_string()))?;
Ok(())
}
#[allow(dead_code)]
pub fn sparse_to_coo(matrix: &MMSparseMatrix<f64>) -> (Array1<usize>, Array1<usize>, Array1<f64>) {
let rows: Vec<usize> = matrix.entries.iter().map(|e| e.row).collect();
let cols: Vec<usize> = matrix.entries.iter().map(|e| e.col).collect();
let values: Vec<f64> = matrix.entries.iter().map(|e| e.value).collect();
(Array1::from(rows), Array1::from(cols), Array1::from(values))
}
#[allow(dead_code)]
pub fn coo_to_sparse(
rows: &Array1<usize>,
cols: &Array1<usize>,
values: &Array1<f64>,
shape: (usize, usize),
header: MMHeader,
) -> MMSparseMatrix<f64> {
let entries: Vec<SparseEntry<f64>> = rows
.iter()
.zip(cols.iter())
.zip(values.iter())
.map(|((&row, &col), &value)| SparseEntry { row, col, value })
.collect();
MMSparseMatrix {
header,
rows: shape.0,
cols: shape.1,
nnz: entries.len(),
entries,
}
}
#[allow(dead_code)]
pub fn read_sparse_matrix_parallel<P: AsRef<Path>>(
path: P,
config: ParallelConfig,
) -> Result<(MMSparseMatrix<f64>, IOStats)> {
let start_time = std::time::Instant::now();
let mut stats = IOStats::default();
let file = File::open(&path).map_err(|e| IoError::FileError(e.to_string()))?;
let mut reader = BufReader::new(file);
let mut header_line = String::new();
reader
.read_line(&mut header_line)
.map_err(|e| IoError::FileError(e.to_string()))?;
let mut header = MMHeader::parse_header(&header_line)?;
let mut line = String::new();
let size_line;
loop {
line.clear();
let bytes_read = reader
.read_line(&mut line)
.map_err(|e| IoError::FileError(e.to_string()))?;
if bytes_read == 0 {
return Err(IoError::FormatError("Unexpected end of file".to_string()));
}
if line.starts_with('%') {
header.comments.push(
line.strip_prefix('%')
.expect("Operation failed")
.trim()
.to_string(),
);
} else {
size_line = line.clone();
break;
}
}
let size_parts: Vec<&str> = size_line.split_whitespace().collect();
if size_parts.len() < 2 {
return Err(IoError::FormatError("Invalid size line format".to_string()));
}
let rows = size_parts[0]
.parse::<usize>()
.map_err(|_| IoError::FormatError("Invalid row count".to_string()))?;
let cols = size_parts[1]
.parse::<usize>()
.map_err(|_| IoError::FormatError("Invalid column count".to_string()))?;
let nnz = if size_parts.len() > 2 {
size_parts[2]
.parse::<usize>()
.map_err(|_| IoError::FormatError("Invalid nnz count".to_string()))?
} else {
rows * cols
};
if header.format != MMFormat::Coordinate {
return Err(IoError::FormatError(
"Only coordinate format is supported for sparse matrices".to_string(),
));
}
if nnz <= config.chunk_size {
let mut entries = Vec::with_capacity(nnz);
for line in reader.lines() {
let line = line.map_err(|e| IoError::FileError(e.to_string()))?;
let line = line.trim();
if line.is_empty() {
continue;
}
let entry = parse_matrix_entry(line, &header)?;
entries.push(entry);
}
stats.entries_processed = entries.len();
stats.io_time_ms = start_time.elapsed().as_secs_f64() * 1000.0;
stats.throughput_eps = if stats.io_time_ms > 0.0 {
stats.entries_processed as f64 / (stats.io_time_ms / 1000.0)
} else {
0.0
};
stats.memory_usage_bytes = std::mem::size_of::<SparseEntry<f64>>() * entries.len();
return Ok((
MMSparseMatrix {
header,
rows,
cols,
nnz,
entries,
},
stats,
));
}
let lines: Vec<String> = reader
.lines()
.collect::<std::io::Result<Vec<_>>>()
.map_err(|e| IoError::FileError(e.to_string()))?;
let non_empty_lines: Vec<&str> = lines
.iter()
.map(|l| l.trim())
.filter(|l| !l.is_empty())
.collect();
let entries = Arc::new(Mutex::new(Vec::with_capacity(nnz)));
let chunk_size = config
.chunk_size
.min(non_empty_lines.len() / config.num_threads + 1);
let mut handles = Vec::new();
for chunk in non_empty_lines.chunks(chunk_size) {
let chunk_lines: Vec<String> = chunk.iter().map(|&s| s.to_string()).collect();
let entries_clone = Arc::clone(&entries);
let header_clone = header.clone();
let handle = thread::spawn(move || -> Result<()> {
let mut local_entries = Vec::new();
for line in chunk_lines {
let entry = parse_matrix_entry(&line, &header_clone)?;
local_entries.push(entry);
}
let mut shared_entries = entries_clone.lock().expect("Operation failed");
shared_entries.extend(local_entries);
Ok(())
});
handles.push(handle);
}
for handle in handles {
handle
.join()
.map_err(|_| IoError::FormatError("Thread join failed".to_string()))??;
}
let final_entries = Arc::try_unwrap(entries)
.map_err(|_| IoError::FormatError("Failed to unwrap entries".to_string()))?
.into_inner()
.expect("Operation failed");
stats.entries_processed = final_entries.len();
stats.io_time_ms = start_time.elapsed().as_secs_f64() * 1000.0;
stats.throughput_eps = if stats.io_time_ms > 0.0 {
stats.entries_processed as f64 / (stats.io_time_ms / 1000.0)
} else {
0.0
};
stats.memory_usage_bytes = std::mem::size_of::<SparseEntry<f64>>() * final_entries.len();
Ok((
MMSparseMatrix {
header,
rows,
cols,
nnz,
entries: final_entries,
},
stats,
))
}
#[allow(dead_code)]
fn parse_matrix_entry(line: &str, header: &MMHeader) -> Result<SparseEntry<f64>> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 2 {
return Err(IoError::FormatError("Invalid entry format".to_string()));
}
let row = parts[0]
.parse::<usize>()
.map_err(|_| IoError::FormatError("Invalid row index".to_string()))?
- 1; let col = parts[1]
.parse::<usize>()
.map_err(|_| IoError::FormatError("Invalid column index".to_string()))?
- 1;
let value = if header.data_type == MMDataType::Pattern {
1.0 } else if parts.len() > 2 {
parts[2]
.parse::<f64>()
.map_err(|_| IoError::FormatError("Invalid value".to_string()))?
} else {
return Err(IoError::FormatError(
"Missing value for non-pattern matrix".to_string(),
));
};
Ok(SparseEntry { row, col, value })
}
#[allow(dead_code)]
pub fn write_sparse_matrix_parallel<P: AsRef<Path>>(
path: P,
matrix: &MMSparseMatrix<f64>,
config: ParallelConfig,
) -> Result<IOStats> {
let start_time = std::time::Instant::now();
let mut stats = IOStats::default();
let file = File::create(&path).map_err(|e| IoError::FileError(e.to_string()))?;
let mut writer = BufWriter::with_capacity(config.buffer_size, file);
writeln!(writer, "{}", matrix.header.to_header_line())
.map_err(|e| IoError::FileError(e.to_string()))?;
for comment in &matrix.header.comments {
writeln!(writer, "%{}", comment).map_err(|e| IoError::FileError(e.to_string()))?;
}
writeln!(writer, "{} {} {}", matrix.rows, matrix.cols, matrix.nnz)
.map_err(|e| IoError::FileError(e.to_string()))?;
if matrix.entries.len() <= config.chunk_size {
for entry in &matrix.entries {
write_matrix_entry(&mut writer, entry, &matrix.header)?;
}
} else {
let chunk_size = config
.chunk_size
.min(matrix.entries.len() / config.num_threads + 1);
let chunks: Vec<&[SparseEntry<f64>]> = matrix.entries.chunks(chunk_size).collect();
let formatted_chunks = Arc::new(Mutex::new(Vec::new()));
let mut handles = Vec::new();
for (chunk_idx, chunk) in chunks.iter().enumerate() {
let chunk_entries = chunk.to_vec();
let header_clone = matrix.header.clone();
let formatted_chunks_clone = Arc::clone(&formatted_chunks);
let handle = thread::spawn(move || -> Result<()> {
let mut local_lines = Vec::new();
for entry in &chunk_entries {
let line = format_matrix_entry(entry, &header_clone);
local_lines.push(line);
}
let mut shared_chunks = formatted_chunks_clone.lock().expect("Operation failed");
shared_chunks.push((chunk_idx, local_lines));
Ok(())
});
handles.push(handle);
}
for handle in handles {
handle
.join()
.map_err(|_| IoError::FormatError("Thread join failed".to_string()))??;
}
let mut all_formatted = Arc::try_unwrap(formatted_chunks)
.map_err(|_| IoError::FormatError("Failed to unwrap formatted chunks".to_string()))?
.into_inner()
.expect("Operation failed");
all_formatted.sort_by_key(|&(idx_, _)| idx_);
for (_, lines) in all_formatted {
for line in lines {
writeln!(writer, "{}", line).map_err(|e| IoError::FileError(e.to_string()))?;
}
}
}
writer
.flush()
.map_err(|e| IoError::FileError(e.to_string()))?;
stats.entries_processed = matrix.entries.len();
stats.io_time_ms = start_time.elapsed().as_secs_f64() * 1000.0;
stats.throughput_eps = if stats.io_time_ms > 0.0 {
stats.entries_processed as f64 / (stats.io_time_ms / 1000.0)
} else {
0.0
};
stats.memory_usage_bytes = std::mem::size_of::<SparseEntry<f64>>() * matrix.entries.len();
Ok(stats)
}
#[allow(dead_code)]
fn write_matrix_entry<W: Write>(
writer: &mut W,
entry: &SparseEntry<f64>,
header: &MMHeader,
) -> Result<()> {
if header.data_type == MMDataType::Pattern {
writeln!(writer, "{} {}", entry.row + 1, entry.col + 1)
.map_err(|e| IoError::FileError(e.to_string()))?;
} else {
writeln!(
writer,
"{} {} {}",
entry.row + 1,
entry.col + 1,
entry.value
)
.map_err(|e| IoError::FileError(e.to_string()))?;
}
Ok(())
}
#[allow(dead_code)]
fn format_matrix_entry(entry: &SparseEntry<f64>, header: &MMHeader) -> String {
if header.data_type == MMDataType::Pattern {
format!("{} {}", entry.row + 1, entry.col + 1)
} else {
format!("{} {} {}", entry.row + 1, entry.col + 1, entry.value)
}
}
#[allow(dead_code)]
pub fn create_optimal_parallel_config(
nnz: usize,
available_memory: Option<usize>,
) -> ParallelConfig {
let mut config = ParallelConfig::default();
if nnz < 10_000 {
config.num_threads = 1;
config.chunk_size = nnz;
} else if nnz < 1_000_000 {
config.chunk_size = 50_000;
} else {
config.chunk_size = 200_000;
config.use_memory_mapping = true;
}
if let Some(_memory) = available_memory {
let entry_size = std::mem::size_of::<SparseEntry<f64>>();
let max_entries_in_memory = _memory / (entry_size * 4);
if nnz > max_entries_in_memory {
config.chunk_size = config
.chunk_size
.min(max_entries_in_memory / config.num_threads);
config.use_memory_mapping = true;
}
}
config
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_header_parsing() {
let header_line = "%%MatrixMarket matrix coordinate real general";
let header = MMHeader::parse_header(header_line).expect("Operation failed");
assert_eq!(header.object, "matrix");
assert_eq!(header.format, MMFormat::Coordinate);
assert_eq!(header.data_type, MMDataType::Real);
assert_eq!(header.symmetry, MMSymmetry::General);
}
#[test]
fn test_header_generation() {
let header = MMHeader {
object: "matrix".to_string(),
format: MMFormat::Coordinate,
data_type: MMDataType::Real,
symmetry: MMSymmetry::General,
comments: vec!["Test comment".to_string()],
};
let header_line = header.to_header_line();
assert_eq!(header_line, "%%MatrixMarket matrix coordinate real general");
}
#[test]
fn test_sparse_matrix_creation() {
let header = MMHeader {
object: "matrix".to_string(),
format: MMFormat::Coordinate,
data_type: MMDataType::Real,
symmetry: MMSymmetry::General,
comments: Vec::new(),
};
let mut entries = Vec::new();
entries.push(SparseEntry {
row: 0,
col: 0,
value: 1.0,
});
entries.push(SparseEntry {
row: 1,
col: 1,
value: 2.0,
});
entries.push(SparseEntry {
row: 0,
col: 1,
value: 3.0,
});
let matrix = MMSparseMatrix {
header,
rows: 2,
cols: 2,
nnz: 3,
entries,
};
assert_eq!(matrix.rows, 2);
assert_eq!(matrix.cols, 2);
assert_eq!(matrix.nnz, 3);
assert_eq!(matrix.entries.len(), 3);
}
#[test]
fn test_sparse_to_coo_conversion() {
let header = MMHeader {
object: "matrix".to_string(),
format: MMFormat::Coordinate,
data_type: MMDataType::Real,
symmetry: MMSymmetry::General,
comments: Vec::new(),
};
let entries = vec![
SparseEntry {
row: 0,
col: 0,
value: 1.0,
},
SparseEntry {
row: 1,
col: 1,
value: 2.0,
},
];
let matrix = MMSparseMatrix {
header,
rows: 2,
cols: 2,
nnz: 2,
entries,
};
let (rows, cols, values) = sparse_to_coo(&matrix);
assert_eq!(rows.len(), 2);
assert_eq!(cols.len(), 2);
assert_eq!(values.len(), 2);
assert_eq!(rows[0], 0);
assert_eq!(cols[0], 0);
assert_eq!(values[0], 1.0);
assert_eq!(rows[1], 1);
assert_eq!(cols[1], 1);
assert_eq!(values[1], 2.0);
}
#[test]
fn test_parallel_config_default() {
let config = ParallelConfig::default();
assert!(config.num_threads > 0);
assert!(config.chunk_size > 0);
assert!(config.buffer_size > 0);
}
#[test]
fn test_optimal_parallel_config() {
let config = create_optimal_parallel_config(5000, None);
assert_eq!(config.num_threads, 1);
assert_eq!(config.chunk_size, 5000);
let config = create_optimal_parallel_config(500_000, None);
assert!(config.num_threads > 1);
assert_eq!(config.chunk_size, 50_000);
let config = create_optimal_parallel_config(5_000_000, None);
assert!(config.num_threads > 1);
assert_eq!(config.chunk_size, 200_000);
assert!(config.use_memory_mapping);
let config = create_optimal_parallel_config(1_000_000, Some(100_000)); assert!(config.use_memory_mapping);
assert!(config.chunk_size < 1_000_000);
}
#[test]
fn test_parse_matrix_entry() {
let header = MMHeader {
object: "matrix".to_string(),
format: MMFormat::Coordinate,
data_type: MMDataType::Real,
symmetry: MMSymmetry::General,
comments: Vec::new(),
};
let entry = parse_matrix_entry(&format!("1 2 {}", std::f64::consts::PI), &header)
.expect("Operation failed");
assert_eq!(entry.row, 0); assert_eq!(entry.col, 1); assert!((entry.value - std::f64::consts::PI).abs() < 1e-10);
let mut pattern_header = header.clone();
pattern_header.data_type = MMDataType::Pattern;
let entry = parse_matrix_entry("5 10", &pattern_header).expect("Operation failed");
assert_eq!(entry.row, 4);
assert_eq!(entry.col, 9);
assert_eq!(entry.value, 1.0);
}
#[test]
fn test_format_matrix_entry() {
let header = MMHeader {
object: "matrix".to_string(),
format: MMFormat::Coordinate,
data_type: MMDataType::Real,
symmetry: MMSymmetry::General,
comments: Vec::new(),
};
let entry = SparseEntry {
row: 0,
col: 1,
value: 2.5,
};
let formatted = format_matrix_entry(&entry, &header);
assert_eq!(formatted, "1 2 2.5");
let mut pattern_header = header.clone();
pattern_header.data_type = MMDataType::Pattern;
let formatted = format_matrix_entry(&entry, &pattern_header);
assert_eq!(formatted, "1 2");
}
#[test]
fn test_io_stats() {
let mut stats = IOStats::default();
assert_eq!(stats.entries_processed, 0);
assert_eq!(stats.io_time_ms, 0.0);
assert_eq!(stats.throughput_eps, 0.0);
assert_eq!(stats.memory_usage_bytes, 0);
stats.entries_processed = 1000;
stats.io_time_ms = 100.0;
stats.throughput_eps = stats.entries_processed as f64 / (stats.io_time_ms / 1000.0);
assert_eq!(stats.throughput_eps, 10000.0); }
}