Skip to main content

scirs2_io/matrix_market/
mod.rs

1//! Matrix Market file format support
2//!
3//! This module provides functionality for reading and writing Matrix Market files,
4//! which are commonly used for storing sparse matrices in scientific computing.
5//!
6//! Matrix Market is a standard format for representing sparse matrices,
7//! supporting both coordinate format (COO) and array format.
8//!
9//! This implementation provides:
10//! - Reading and writing Matrix Market files
11//! - Support for coordinate format sparse matrices
12//! - Support for dense array format
13//! - Real and complex number support
14//! - Different matrix types (general, symmetric, hermitian, skew-symmetric)
15
16use scirs2_core::ndarray::{Array1, Array2};
17use std::fs::File;
18use std::io::{BufRead, BufReader, BufWriter, Write};
19use std::path::Path;
20use std::str::FromStr;
21use std::sync::{Arc, Mutex};
22use std::thread;
23
24use crate::error::{IoError, Result};
25
26/// Matrix Market data types
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum MMDataType {
29    /// Real numbers
30    Real,
31    /// Complex numbers
32    Complex,
33    /// Integer numbers
34    Integer,
35    /// Pattern (just structure, no values)
36    Pattern,
37}
38
39impl FromStr for MMDataType {
40    type Err = IoError;
41
42    fn from_str(s: &str) -> Result<Self> {
43        match s.to_lowercase().as_str() {
44            "real" => Ok(MMDataType::Real),
45            "complex" => Ok(MMDataType::Complex),
46            "integer" => Ok(MMDataType::Integer),
47            "pattern" => Ok(MMDataType::Pattern),
48            _ => Err(IoError::FormatError(format!("Unknown data type: {}", s))),
49        }
50    }
51}
52
53impl std::fmt::Display for MMDataType {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        match self {
56            MMDataType::Real => write!(f, "real"),
57            MMDataType::Complex => write!(f, "complex"),
58            MMDataType::Integer => write!(f, "integer"),
59            MMDataType::Pattern => write!(f, "pattern"),
60        }
61    }
62}
63
64/// Matrix Market storage format
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum MMFormat {
67    /// Coordinate format (sparse)
68    Coordinate,
69    /// Array format (dense)
70    Array,
71}
72
73impl FromStr for MMFormat {
74    type Err = IoError;
75
76    fn from_str(s: &str) -> Result<Self> {
77        match s.to_lowercase().as_str() {
78            "coordinate" => Ok(MMFormat::Coordinate),
79            "array" => Ok(MMFormat::Array),
80            _ => Err(IoError::FormatError(format!("Unknown format: {}", s))),
81        }
82    }
83}
84
85impl std::fmt::Display for MMFormat {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        match self {
88            MMFormat::Coordinate => write!(f, "coordinate"),
89            MMFormat::Array => write!(f, "array"),
90        }
91    }
92}
93
94/// Matrix Market matrix type
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum MMSymmetry {
97    /// General matrix (no symmetry)
98    General,
99    /// Symmetric matrix
100    Symmetric,
101    /// Hermitian matrix (complex symmetric)
102    Hermitian,
103    /// Skew-symmetric matrix
104    SkewSymmetric,
105}
106
107impl FromStr for MMSymmetry {
108    type Err = IoError;
109
110    fn from_str(s: &str) -> Result<Self> {
111        match s.to_lowercase().as_str() {
112            "general" => Ok(MMSymmetry::General),
113            "symmetric" => Ok(MMSymmetry::Symmetric),
114            "hermitian" => Ok(MMSymmetry::Hermitian),
115            "skew-symmetric" => Ok(MMSymmetry::SkewSymmetric),
116            _ => Err(IoError::FormatError(format!("Unknown symmetry: {}", s))),
117        }
118    }
119}
120
121impl std::fmt::Display for MMSymmetry {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        match self {
124            MMSymmetry::General => write!(f, "general"),
125            MMSymmetry::Symmetric => write!(f, "symmetric"),
126            MMSymmetry::Hermitian => write!(f, "hermitian"),
127            MMSymmetry::SkewSymmetric => write!(f, "skew-symmetric"),
128        }
129    }
130}
131
132/// Matrix Market header information
133#[derive(Debug, Clone)]
134pub struct MMHeader {
135    /// Object type (always "matrix" for Matrix Market)
136    pub object: String,
137    /// Storage format
138    pub format: MMFormat,
139    /// Data type
140    pub data_type: MMDataType,
141    /// Symmetry type
142    pub symmetry: MMSymmetry,
143    /// Comments from the file
144    pub comments: Vec<String>,
145}
146
147/// Sparse matrix entry (coordinate format)
148#[derive(Debug, Clone)]
149pub struct SparseEntry<T> {
150    /// Row index (1-based in file, 0-based in memory)
151    pub row: usize,
152    /// Column index (1-based in file, 0-based in memory)
153    pub col: usize,
154    /// Value
155    pub value: T,
156}
157
158/// Matrix Market sparse matrix (COO format)
159#[derive(Debug, Clone)]
160pub struct MMSparseMatrix<T> {
161    /// Matrix header
162    pub header: MMHeader,
163    /// Number of rows
164    pub rows: usize,
165    /// Number of columns
166    pub cols: usize,
167    /// Number of non-zero entries
168    pub nnz: usize,
169    /// Sparse entries
170    pub entries: Vec<SparseEntry<T>>,
171}
172
173/// Matrix Market dense matrix
174#[derive(Debug, Clone)]
175pub struct MMDenseMatrix<T> {
176    /// Matrix header
177    pub header: MMHeader,
178    /// Number of rows
179    pub rows: usize,
180    /// Number of columns
181    pub cols: usize,
182    /// Dense matrix data (column-major order)
183    pub data: Array2<T>,
184}
185
186/// Configuration for parallel Matrix Market I/O
187#[derive(Debug, Clone)]
188pub struct ParallelConfig {
189    /// Number of worker threads
190    pub num_threads: usize,
191    /// Chunk size for parallel processing (number of entries per chunk)
192    pub chunk_size: usize,
193    /// Buffer size for I/O operations
194    pub buffer_size: usize,
195    /// Enable memory mapping for large files
196    pub use_memory_mapping: bool,
197}
198
199impl Default for ParallelConfig {
200    fn default() -> Self {
201        Self {
202            num_threads: thread::available_parallelism()
203                .map(|n| n.get())
204                .unwrap_or(4),
205            chunk_size: 100_000,           // 100k entries per chunk
206            buffer_size: 64 * 1024 * 1024, // 64MB buffer
207            use_memory_mapping: false,
208        }
209    }
210}
211
212/// Statistics for Matrix Market I/O operations
213#[derive(Debug, Clone, Default)]
214pub struct IOStats {
215    /// Time taken for I/O operation in milliseconds
216    pub io_time_ms: f64,
217    /// Number of entries processed
218    pub entries_processed: usize,
219    /// Throughput in entries per second
220    pub throughput_eps: f64,
221    /// Memory usage in bytes
222    pub memory_usage_bytes: usize,
223}
224
225impl MMHeader {
226    /// Parse Matrix Market header line
227    pub fn parse_header(line: &str) -> Result<Self> {
228        if !line.starts_with("%%MatrixMarket") {
229            return Err(IoError::FormatError(
230                "Invalid Matrix Market header".to_string(),
231            ));
232        }
233
234        let parts: Vec<&str> = line.split_whitespace().collect();
235        if parts.len() != 5 {
236            return Err(IoError::FormatError(
237                "Invalid Matrix Market header format".to_string(),
238            ));
239        }
240
241        let object = parts[1].to_string();
242        let format = MMFormat::from_str(parts[2])?;
243        let data_type = MMDataType::from_str(parts[3])?;
244        let symmetry = MMSymmetry::from_str(parts[4])?;
245
246        if object.to_lowercase() != "matrix" {
247            return Err(IoError::FormatError(format!(
248                "Unsupported object type: {}",
249                object
250            )));
251        }
252
253        Ok(MMHeader {
254            object,
255            format,
256            data_type,
257            symmetry,
258            comments: Vec::new(),
259        })
260    }
261
262    /// Generate Matrix Market header line
263    pub fn to_header_line(&self) -> String {
264        format!(
265            "%%MatrixMarket {} {} {} {}",
266            self.object, self.format, self.data_type, self.symmetry
267        )
268    }
269}
270
271/// Read a Matrix Market file containing a sparse matrix
272///
273/// # Arguments
274///
275/// * `path` - Path to the Matrix Market file
276///
277/// # Returns
278///
279/// * `Result<MMSparseMatrix<f64>>` - The sparse matrix or an error
280///
281/// # Examples
282///
283/// ```no_run
284/// use scirs2_io::matrix_market::read_sparse_matrix;
285///
286/// let matrix = read_sparse_matrix("matrix.mtx").expect("Operation failed");
287/// println!("Matrix: {}x{} with {} non-zeros", matrix.rows, matrix.cols, matrix.nnz);
288/// ```
289#[allow(dead_code)]
290pub fn read_sparse_matrix<P: AsRef<Path>>(path: P) -> Result<MMSparseMatrix<f64>> {
291    let file = File::open(path).map_err(|e| IoError::FileError(e.to_string()))?;
292    let reader = BufReader::new(file);
293
294    let mut lines = reader.lines();
295
296    // Read header line
297    let header_line = lines
298        .next()
299        .ok_or_else(|| IoError::FormatError("Empty file".to_string()))?
300        .map_err(|e| IoError::FileError(e.to_string()))?;
301
302    let mut header = MMHeader::parse_header(&header_line)?;
303
304    // Read comments
305    for line in &mut lines {
306        let line = line.map_err(|e| IoError::FileError(e.to_string()))?;
307        if line.starts_with('%') {
308            header.comments.push(
309                line.strip_prefix('%')
310                    .expect("Operation failed")
311                    .trim()
312                    .to_string(),
313            );
314        } else {
315            // This is the size line, put it back
316            let size_parts: Vec<&str> = line.split_whitespace().collect();
317            if size_parts.len() < 2 {
318                return Err(IoError::FormatError("Invalid size line format".to_string()));
319            }
320
321            let rows = size_parts[0]
322                .parse::<usize>()
323                .map_err(|_| IoError::FormatError("Invalid row count".to_string()))?;
324            let cols = size_parts[1]
325                .parse::<usize>()
326                .map_err(|_| IoError::FormatError("Invalid column count".to_string()))?;
327            let nnz = if size_parts.len() > 2 {
328                size_parts[2]
329                    .parse::<usize>()
330                    .map_err(|_| IoError::FormatError("Invalid nnz count".to_string()))?
331            } else {
332                // For array format, nnz is rows * cols
333                rows * cols
334            };
335
336            if header.format != MMFormat::Coordinate {
337                return Err(IoError::FormatError(
338                    "Only coordinate format is supported for sparse matrices".to_string(),
339                ));
340            }
341
342            // Read entries
343            let mut entries = Vec::with_capacity(nnz);
344
345            for line in lines {
346                let line = line.map_err(|e| IoError::FileError(e.to_string()))?;
347                let line = line.trim();
348                if line.is_empty() {
349                    continue;
350                }
351
352                let parts: Vec<&str> = line.split_whitespace().collect();
353                if parts.len() < 2 {
354                    return Err(IoError::FormatError("Invalid entry format".to_string()));
355                }
356
357                let row = parts[0]
358                    .parse::<usize>()
359                    .map_err(|_| IoError::FormatError("Invalid row index".to_string()))?
360                    - 1; // Convert to 0-based
361                let col = parts[1]
362                    .parse::<usize>()
363                    .map_err(|_| IoError::FormatError("Invalid column index".to_string()))?
364                    - 1; // Convert to 0-based
365
366                let value = if header.data_type == MMDataType::Pattern {
367                    1.0 // Pattern matrices have implicit value of 1
368                } else if parts.len() > 2 {
369                    parts[2]
370                        .parse::<f64>()
371                        .map_err(|_| IoError::FormatError("Invalid value".to_string()))?
372                } else {
373                    return Err(IoError::FormatError(
374                        "Missing value for non-pattern matrix".to_string(),
375                    ));
376                };
377
378                entries.push(SparseEntry { row, col, value });
379            }
380
381            return Ok(MMSparseMatrix {
382                header,
383                rows,
384                cols,
385                nnz,
386                entries,
387            });
388        }
389    }
390
391    Err(IoError::FormatError("Missing size information".to_string()))
392}
393
394/// Write a sparse matrix to a Matrix Market file
395///
396/// # Arguments
397///
398/// * `path` - Path to the output Matrix Market file
399/// * `matrix` - The sparse matrix to write
400///
401/// # Returns
402///
403/// * `Result<()>` - Success or an error
404///
405/// # Examples
406///
407/// ```no_run
408/// use scirs2_io::matrix_market::{write_sparse_matrix, MMSparseMatrix, MMHeader, SparseEntry};
409/// use scirs2_io::matrix_market::{MMFormat, MMDataType, MMSymmetry};
410///
411/// let header = MMHeader {
412///     object: "matrix".to_string(),
413///     format: MMFormat::Coordinate,
414///     data_type: MMDataType::Real,
415///     symmetry: MMSymmetry::General,
416///     comments: vec!["Generated by scirs2-io".to_string()],
417/// };
418///
419/// let mut entries = Vec::new();
420/// entries.push(SparseEntry { row: 0, col: 0, value: 1.0 });
421/// entries.push(SparseEntry { row: 1, col: 1, value: 2.0 });
422///
423/// let matrix = MMSparseMatrix {
424///     header,
425///     rows: 2,
426///     cols: 2,
427///     nnz: 2,
428///     entries,
429/// };
430///
431/// write_sparse_matrix("output.mtx", &matrix).expect("Operation failed");
432/// ```
433#[allow(dead_code)]
434pub fn write_sparse_matrix<P: AsRef<Path>>(path: P, matrix: &MMSparseMatrix<f64>) -> Result<()> {
435    let file = File::create(path).map_err(|e| IoError::FileError(e.to_string()))?;
436    let mut writer = BufWriter::new(file);
437
438    // Write header
439    writeln!(writer, "{}", matrix.header.to_header_line())
440        .map_err(|e| IoError::FileError(e.to_string()))?;
441
442    // Write comments
443    for comment in &matrix.header.comments {
444        writeln!(writer, "%{}", comment).map_err(|e| IoError::FileError(e.to_string()))?;
445    }
446
447    // Write size line
448    writeln!(writer, "{} {} {}", matrix.rows, matrix.cols, matrix.nnz)
449        .map_err(|e| IoError::FileError(e.to_string()))?;
450
451    // Write entries
452    for entry in &matrix.entries {
453        if matrix.header.data_type == MMDataType::Pattern {
454            // Pattern matrices only have row and column indices
455            writeln!(writer, "{} {}", entry.row + 1, entry.col + 1)
456                .map_err(|e| IoError::FileError(e.to_string()))?;
457        } else {
458            // Write row, column, and value (convert to 1-based indexing)
459            writeln!(
460                writer,
461                "{} {} {}",
462                entry.row + 1,
463                entry.col + 1,
464                entry.value
465            )
466            .map_err(|e| IoError::FileError(e.to_string()))?;
467        }
468    }
469
470    writer
471        .flush()
472        .map_err(|e| IoError::FileError(e.to_string()))?;
473
474    Ok(())
475}
476
477/// Read a Matrix Market file containing a dense matrix
478///
479/// # Arguments
480///
481/// * `path` - Path to the Matrix Market file
482///
483/// # Returns
484///
485/// * `Result<MMDenseMatrix<f64>>` - The dense matrix or an error
486#[allow(dead_code)]
487pub fn read_dense_matrix<P: AsRef<Path>>(path: P) -> Result<MMDenseMatrix<f64>> {
488    let file = File::open(path).map_err(|e| IoError::FileError(e.to_string()))?;
489    let reader = BufReader::new(file);
490
491    let mut lines = reader.lines();
492
493    // Read header line
494    let header_line = lines
495        .next()
496        .ok_or_else(|| IoError::FormatError("Empty file".to_string()))?
497        .map_err(|e| IoError::FileError(e.to_string()))?;
498
499    let mut header = MMHeader::parse_header(&header_line)?;
500
501    if header.format != MMFormat::Array {
502        return Err(IoError::FormatError(
503            "Only array format is supported for dense matrices".to_string(),
504        ));
505    }
506
507    // Read comments
508    for line in &mut lines {
509        let line = line.map_err(|e| IoError::FileError(e.to_string()))?;
510        if line.starts_with('%') {
511            header.comments.push(
512                line.strip_prefix('%')
513                    .expect("Operation failed")
514                    .trim()
515                    .to_string(),
516            );
517        } else {
518            // This is the size line
519            let size_parts: Vec<&str> = line.split_whitespace().collect();
520            if size_parts.len() < 2 {
521                return Err(IoError::FormatError("Invalid size line format".to_string()));
522            }
523
524            let rows = size_parts[0]
525                .parse::<usize>()
526                .map_err(|_| IoError::FormatError("Invalid row count".to_string()))?;
527            let cols = size_parts[1]
528                .parse::<usize>()
529                .map_err(|_| IoError::FormatError("Invalid column count".to_string()))?;
530
531            // Read matrix data (column-major order)
532            let mut data = Vec::with_capacity(rows * cols);
533
534            for line in lines {
535                let line = line.map_err(|e| IoError::FileError(e.to_string()))?;
536                let line = line.trim();
537                if line.is_empty() {
538                    continue;
539                }
540
541                let value = line
542                    .parse::<f64>()
543                    .map_err(|_| IoError::FormatError("Invalid matrix value".to_string()))?;
544                data.push(value);
545            }
546
547            if data.len() != rows * cols {
548                return Err(IoError::FormatError(format!(
549                    "Expected {} values, got {}",
550                    rows * cols,
551                    data.len()
552                )));
553            }
554
555            // Convert to Array2 (column-major to row-major)
556            let mut matrix_data = Array2::zeros((rows, cols));
557            for col in 0..cols {
558                for row in 0..rows {
559                    matrix_data[[row, col]] = data[col * rows + row];
560                }
561            }
562
563            return Ok(MMDenseMatrix {
564                header,
565                rows,
566                cols,
567                data: matrix_data,
568            });
569        }
570    }
571
572    Err(IoError::FormatError("Missing size information".to_string()))
573}
574
575/// Write a dense matrix to a Matrix Market file
576///
577/// # Arguments
578///
579/// * `path` - Path to the output Matrix Market file
580/// * `matrix` - The dense matrix to write
581///
582/// # Returns
583///
584/// * `Result<()>` - Success or an error
585#[allow(dead_code)]
586pub fn write_dense_matrix<P: AsRef<Path>>(path: P, matrix: &MMDenseMatrix<f64>) -> Result<()> {
587    let file = File::create(path).map_err(|e| IoError::FileError(e.to_string()))?;
588    let mut writer = BufWriter::new(file);
589
590    // Write header
591    writeln!(writer, "{}", matrix.header.to_header_line())
592        .map_err(|e| IoError::FileError(e.to_string()))?;
593
594    // Write comments
595    for comment in &matrix.header.comments {
596        writeln!(writer, "%{}", comment).map_err(|e| IoError::FileError(e.to_string()))?;
597    }
598
599    // Write size line
600    writeln!(writer, "{} {}", matrix.rows, matrix.cols)
601        .map_err(|e| IoError::FileError(e.to_string()))?;
602
603    // Write matrix data in column-major order
604    for col in 0..matrix.cols {
605        for row in 0..matrix.rows {
606            writeln!(writer, "{}", matrix.data[[row, col]])
607                .map_err(|e| IoError::FileError(e.to_string()))?;
608        }
609    }
610
611    writer
612        .flush()
613        .map_err(|e| IoError::FileError(e.to_string()))?;
614
615    Ok(())
616}
617
618/// Convert a sparse matrix to ndarray coordinate format
619///
620/// # Arguments
621///
622/// * `matrix` - The Matrix Market sparse matrix
623///
624/// # Returns
625///
626/// * `(Array1<usize>, Array1<usize>, Array1<f64>)` - Row indices, column indices, and values
627#[allow(dead_code)]
628pub fn sparse_to_coo(matrix: &MMSparseMatrix<f64>) -> (Array1<usize>, Array1<usize>, Array1<f64>) {
629    let rows: Vec<usize> = matrix.entries.iter().map(|e| e.row).collect();
630    let cols: Vec<usize> = matrix.entries.iter().map(|e| e.col).collect();
631    let values: Vec<f64> = matrix.entries.iter().map(|e| e.value).collect();
632
633    (Array1::from(rows), Array1::from(cols), Array1::from(values))
634}
635
636/// Create a sparse matrix from ndarray coordinate format
637///
638/// # Arguments
639///
640/// * `rows` - Row indices
641/// * `cols` - Column indices
642/// * `values` - Values
643/// * `shape` - Matrix shape (rows, cols)
644/// * `header` - Matrix Market header
645///
646/// # Returns
647///
648/// * `MMSparseMatrix<f64>` - The Matrix Market sparse matrix
649#[allow(dead_code)]
650pub fn coo_to_sparse(
651    rows: &Array1<usize>,
652    cols: &Array1<usize>,
653    values: &Array1<f64>,
654    shape: (usize, usize),
655    header: MMHeader,
656) -> MMSparseMatrix<f64> {
657    let entries: Vec<SparseEntry<f64>> = rows
658        .iter()
659        .zip(cols.iter())
660        .zip(values.iter())
661        .map(|((&row, &col), &value)| SparseEntry { row, col, value })
662        .collect();
663
664    MMSparseMatrix {
665        header,
666        rows: shape.0,
667        cols: shape.1,
668        nnz: entries.len(),
669        entries,
670    }
671}
672
673/// Read a Matrix Market file in parallel for better performance with large matrices
674///
675/// # Arguments
676///
677/// * `path` - Path to the Matrix Market file
678/// * `config` - Parallel processing configuration
679///
680/// # Returns
681///
682/// * `Result<(MMSparseMatrix<f64>, IOStats)>` - The sparse matrix and I/O statistics
683///
684/// # Examples
685///
686/// ```no_run
687/// use scirs2_io::matrix_market::{read_sparse_matrix_parallel, ParallelConfig};
688///
689/// let config = ParallelConfig::default();
690/// let (matrix, stats) = read_sparse_matrix_parallel("large_matrix.mtx", config).expect("Operation failed");
691/// println!("Read {} entries in {:.2}ms", stats.entries_processed, stats.io_time_ms);
692/// ```
693#[allow(dead_code)]
694pub fn read_sparse_matrix_parallel<P: AsRef<Path>>(
695    path: P,
696    config: ParallelConfig,
697) -> Result<(MMSparseMatrix<f64>, IOStats)> {
698    let start_time = std::time::Instant::now();
699    let mut stats = IOStats::default();
700
701    let file = File::open(&path).map_err(|e| IoError::FileError(e.to_string()))?;
702    let mut reader = BufReader::new(file);
703
704    // Read header sequentially (always small)
705    let mut header_line = String::new();
706    reader
707        .read_line(&mut header_line)
708        .map_err(|e| IoError::FileError(e.to_string()))?;
709
710    let mut header = MMHeader::parse_header(&header_line)?;
711
712    // Read comments sequentially
713    let mut line = String::new();
714    let size_line;
715    loop {
716        line.clear();
717        let bytes_read = reader
718            .read_line(&mut line)
719            .map_err(|e| IoError::FileError(e.to_string()))?;
720
721        if bytes_read == 0 {
722            return Err(IoError::FormatError("Unexpected end of file".to_string()));
723        }
724
725        if line.starts_with('%') {
726            header.comments.push(
727                line.strip_prefix('%')
728                    .expect("Operation failed")
729                    .trim()
730                    .to_string(),
731            );
732        } else {
733            size_line = line.clone();
734            break;
735        }
736    }
737
738    // Parse size information
739    let size_parts: Vec<&str> = size_line.split_whitespace().collect();
740    if size_parts.len() < 2 {
741        return Err(IoError::FormatError("Invalid size line format".to_string()));
742    }
743
744    let rows = size_parts[0]
745        .parse::<usize>()
746        .map_err(|_| IoError::FormatError("Invalid row count".to_string()))?;
747    let cols = size_parts[1]
748        .parse::<usize>()
749        .map_err(|_| IoError::FormatError("Invalid column count".to_string()))?;
750    let nnz = if size_parts.len() > 2 {
751        size_parts[2]
752            .parse::<usize>()
753            .map_err(|_| IoError::FormatError("Invalid nnz count".to_string()))?
754    } else {
755        rows * cols
756    };
757
758    if header.format != MMFormat::Coordinate {
759        return Err(IoError::FormatError(
760            "Only coordinate format is supported for sparse matrices".to_string(),
761        ));
762    }
763
764    // For small matrices, use sequential reading
765    if nnz <= config.chunk_size {
766        let mut entries = Vec::with_capacity(nnz);
767
768        for line in reader.lines() {
769            let line = line.map_err(|e| IoError::FileError(e.to_string()))?;
770            let line = line.trim();
771            if line.is_empty() {
772                continue;
773            }
774
775            let entry = parse_matrix_entry(line, &header)?;
776            entries.push(entry);
777        }
778
779        stats.entries_processed = entries.len();
780        stats.io_time_ms = start_time.elapsed().as_secs_f64() * 1000.0;
781        stats.throughput_eps = if stats.io_time_ms > 0.0 {
782            stats.entries_processed as f64 / (stats.io_time_ms / 1000.0)
783        } else {
784            0.0
785        };
786        stats.memory_usage_bytes = std::mem::size_of::<SparseEntry<f64>>() * entries.len();
787
788        return Ok((
789            MMSparseMatrix {
790                header,
791                rows,
792                cols,
793                nnz,
794                entries,
795            },
796            stats,
797        ));
798    }
799
800    // Parallel reading for large matrices
801    // Read all lines into memory first (for parallel processing)
802    let lines: Vec<String> = reader
803        .lines()
804        .collect::<std::io::Result<Vec<_>>>()
805        .map_err(|e| IoError::FileError(e.to_string()))?;
806
807    let non_empty_lines: Vec<&str> = lines
808        .iter()
809        .map(|l| l.trim())
810        .filter(|l| !l.is_empty())
811        .collect();
812
813    // Process lines in parallel chunks
814    let entries = Arc::new(Mutex::new(Vec::with_capacity(nnz)));
815    let chunk_size = config
816        .chunk_size
817        .min(non_empty_lines.len() / config.num_threads + 1);
818
819    let mut handles = Vec::new();
820
821    for chunk in non_empty_lines.chunks(chunk_size) {
822        let chunk_lines: Vec<String> = chunk.iter().map(|&s| s.to_string()).collect();
823        let entries_clone = Arc::clone(&entries);
824        let header_clone = header.clone();
825
826        let handle = thread::spawn(move || -> Result<()> {
827            let mut local_entries = Vec::new();
828
829            for line in chunk_lines {
830                let entry = parse_matrix_entry(&line, &header_clone)?;
831                local_entries.push(entry);
832            }
833
834            // Lock and append to shared vector
835            let mut shared_entries = entries_clone.lock().expect("Operation failed");
836            shared_entries.extend(local_entries);
837
838            Ok(())
839        });
840
841        handles.push(handle);
842    }
843
844    // Wait for all threads to complete
845    for handle in handles {
846        handle
847            .join()
848            .map_err(|_| IoError::FormatError("Thread join failed".to_string()))??;
849    }
850
851    let final_entries = Arc::try_unwrap(entries)
852        .map_err(|_| IoError::FormatError("Failed to unwrap entries".to_string()))?
853        .into_inner()
854        .expect("Operation failed");
855
856    stats.entries_processed = final_entries.len();
857    stats.io_time_ms = start_time.elapsed().as_secs_f64() * 1000.0;
858    stats.throughput_eps = if stats.io_time_ms > 0.0 {
859        stats.entries_processed as f64 / (stats.io_time_ms / 1000.0)
860    } else {
861        0.0
862    };
863    stats.memory_usage_bytes = std::mem::size_of::<SparseEntry<f64>>() * final_entries.len();
864
865    Ok((
866        MMSparseMatrix {
867            header,
868            rows,
869            cols,
870            nnz,
871            entries: final_entries,
872        },
873        stats,
874    ))
875}
876
877/// Parse a single matrix entry line
878#[allow(dead_code)]
879fn parse_matrix_entry(line: &str, header: &MMHeader) -> Result<SparseEntry<f64>> {
880    let parts: Vec<&str> = line.split_whitespace().collect();
881    if parts.len() < 2 {
882        return Err(IoError::FormatError("Invalid entry format".to_string()));
883    }
884
885    let row = parts[0]
886        .parse::<usize>()
887        .map_err(|_| IoError::FormatError("Invalid row index".to_string()))?
888        - 1; // Convert to 0-based
889    let col = parts[1]
890        .parse::<usize>()
891        .map_err(|_| IoError::FormatError("Invalid column index".to_string()))?
892        - 1; // Convert to 0-based
893
894    let value = if header.data_type == MMDataType::Pattern {
895        1.0 // Pattern matrices have implicit value of 1
896    } else if parts.len() > 2 {
897        parts[2]
898            .parse::<f64>()
899            .map_err(|_| IoError::FormatError("Invalid value".to_string()))?
900    } else {
901        return Err(IoError::FormatError(
902            "Missing value for non-pattern matrix".to_string(),
903        ));
904    };
905
906    Ok(SparseEntry { row, col, value })
907}
908
909/// Write a sparse matrix to a Matrix Market file in parallel
910///
911/// # Arguments
912///
913/// * `path` - Path to the output Matrix Market file
914/// * `matrix` - The sparse matrix to write
915/// * `config` - Parallel processing configuration
916///
917/// # Returns
918///
919/// * `Result<IOStats>` - I/O statistics
920///
921/// # Examples
922///
923/// ```no_run
924/// use scirs2_io::matrix_market::{write_sparse_matrix_parallel, MMSparseMatrix, ParallelConfig};
925/// # use scirs2_io::matrix_market::{MMHeader, MMFormat, MMDataType, MMSymmetry, SparseEntry};
926///
927/// # let header = MMHeader {
928/// #     object: "matrix".to_string(),
929/// #     format: MMFormat::Coordinate,
930/// #     data_type: MMDataType::Real,
931/// #     symmetry: MMSymmetry::General,
932/// #     comments: Vec::new(),
933/// # };
934/// # let matrix = MMSparseMatrix {
935/// #     header,
936/// #     rows: 2,
937/// #     cols: 2,
938/// #     nnz: 2,
939/// #     entries: vec![
940/// #         SparseEntry { row: 0, col: 0, value: 1.0 },
941/// #         SparseEntry { row: 1, col: 1, value: 2.0 },
942/// #     ],
943/// # };
944/// let config = ParallelConfig::default();
945/// let stats = write_sparse_matrix_parallel("output.mtx", &matrix, config).expect("Operation failed");
946/// println!("Wrote {} entries in {:.2}ms", stats.entries_processed, stats.io_time_ms);
947/// ```
948#[allow(dead_code)]
949pub fn write_sparse_matrix_parallel<P: AsRef<Path>>(
950    path: P,
951    matrix: &MMSparseMatrix<f64>,
952    config: ParallelConfig,
953) -> Result<IOStats> {
954    let start_time = std::time::Instant::now();
955    let mut stats = IOStats::default();
956
957    let file = File::create(&path).map_err(|e| IoError::FileError(e.to_string()))?;
958    let mut writer = BufWriter::with_capacity(config.buffer_size, file);
959
960    // Write header and metadata sequentially
961    writeln!(writer, "{}", matrix.header.to_header_line())
962        .map_err(|e| IoError::FileError(e.to_string()))?;
963
964    for comment in &matrix.header.comments {
965        writeln!(writer, "%{}", comment).map_err(|e| IoError::FileError(e.to_string()))?;
966    }
967
968    writeln!(writer, "{} {} {}", matrix.rows, matrix.cols, matrix.nnz)
969        .map_err(|e| IoError::FileError(e.to_string()))?;
970
971    // For small matrices, write sequentially
972    if matrix.entries.len() <= config.chunk_size {
973        for entry in &matrix.entries {
974            write_matrix_entry(&mut writer, entry, &matrix.header)?;
975        }
976    } else {
977        // For large matrices, format entries in parallel then write sequentially
978        let chunk_size = config
979            .chunk_size
980            .min(matrix.entries.len() / config.num_threads + 1);
981        let chunks: Vec<&[SparseEntry<f64>]> = matrix.entries.chunks(chunk_size).collect();
982
983        let formatted_chunks = Arc::new(Mutex::new(Vec::new()));
984        let mut handles = Vec::new();
985
986        for (chunk_idx, chunk) in chunks.iter().enumerate() {
987            let chunk_entries = chunk.to_vec();
988            let header_clone = matrix.header.clone();
989            let formatted_chunks_clone = Arc::clone(&formatted_chunks);
990
991            let handle = thread::spawn(move || -> Result<()> {
992                let mut local_lines = Vec::new();
993
994                for entry in &chunk_entries {
995                    let line = format_matrix_entry(entry, &header_clone);
996                    local_lines.push(line);
997                }
998
999                let mut shared_chunks = formatted_chunks_clone.lock().expect("Operation failed");
1000                shared_chunks.push((chunk_idx, local_lines));
1001
1002                Ok(())
1003            });
1004
1005            handles.push(handle);
1006        }
1007
1008        // Wait for all formatting to complete
1009        for handle in handles {
1010            handle
1011                .join()
1012                .map_err(|_| IoError::FormatError("Thread join failed".to_string()))??;
1013        }
1014
1015        // Sort chunks by index and write sequentially
1016        let mut all_formatted = Arc::try_unwrap(formatted_chunks)
1017            .map_err(|_| IoError::FormatError("Failed to unwrap formatted chunks".to_string()))?
1018            .into_inner()
1019            .expect("Operation failed");
1020
1021        all_formatted.sort_by_key(|&(idx_, _)| idx_);
1022
1023        for (_, lines) in all_formatted {
1024            for line in lines {
1025                writeln!(writer, "{}", line).map_err(|e| IoError::FileError(e.to_string()))?;
1026            }
1027        }
1028    }
1029
1030    writer
1031        .flush()
1032        .map_err(|e| IoError::FileError(e.to_string()))?;
1033
1034    stats.entries_processed = matrix.entries.len();
1035    stats.io_time_ms = start_time.elapsed().as_secs_f64() * 1000.0;
1036    stats.throughput_eps = if stats.io_time_ms > 0.0 {
1037        stats.entries_processed as f64 / (stats.io_time_ms / 1000.0)
1038    } else {
1039        0.0
1040    };
1041    stats.memory_usage_bytes = std::mem::size_of::<SparseEntry<f64>>() * matrix.entries.len();
1042
1043    Ok(stats)
1044}
1045
1046/// Write a single matrix entry to the writer
1047#[allow(dead_code)]
1048fn write_matrix_entry<W: Write>(
1049    writer: &mut W,
1050    entry: &SparseEntry<f64>,
1051    header: &MMHeader,
1052) -> Result<()> {
1053    if header.data_type == MMDataType::Pattern {
1054        writeln!(writer, "{} {}", entry.row + 1, entry.col + 1)
1055            .map_err(|e| IoError::FileError(e.to_string()))?;
1056    } else {
1057        writeln!(
1058            writer,
1059            "{} {} {}",
1060            entry.row + 1,
1061            entry.col + 1,
1062            entry.value
1063        )
1064        .map_err(|e| IoError::FileError(e.to_string()))?;
1065    }
1066    Ok(())
1067}
1068
1069/// Format a single matrix entry as a string
1070#[allow(dead_code)]
1071fn format_matrix_entry(entry: &SparseEntry<f64>, header: &MMHeader) -> String {
1072    if header.data_type == MMDataType::Pattern {
1073        format!("{} {}", entry.row + 1, entry.col + 1)
1074    } else {
1075        format!("{} {} {}", entry.row + 1, entry.col + 1, entry.value)
1076    }
1077}
1078
1079/// Create optimal parallel configuration based on matrix size
1080///
1081/// # Arguments
1082///
1083/// * `nnz` - Number of non-zero entries
1084/// * `available_memory` - Available memory in bytes (optional)
1085///
1086/// # Returns
1087///
1088/// * `ParallelConfig` - Optimized configuration
1089#[allow(dead_code)]
1090pub fn create_optimal_parallel_config(
1091    nnz: usize,
1092    available_memory: Option<usize>,
1093) -> ParallelConfig {
1094    let mut config = ParallelConfig::default();
1095
1096    // Adjust chunk size based on matrix size
1097    if nnz < 10_000 {
1098        // Small matrices - use sequential processing
1099        config.num_threads = 1;
1100        config.chunk_size = nnz;
1101    } else if nnz < 1_000_000 {
1102        // Medium matrices
1103        config.chunk_size = 50_000;
1104    } else {
1105        // Large matrices
1106        config.chunk_size = 200_000;
1107        config.use_memory_mapping = true;
1108    }
1109
1110    // Adjust based on available _memory
1111    if let Some(_memory) = available_memory {
1112        let entry_size = std::mem::size_of::<SparseEntry<f64>>();
1113        let max_entries_in_memory = _memory / (entry_size * 4); // Use 25% of available _memory
1114
1115        if nnz > max_entries_in_memory {
1116            config.chunk_size = config
1117                .chunk_size
1118                .min(max_entries_in_memory / config.num_threads);
1119            config.use_memory_mapping = true;
1120        }
1121    }
1122
1123    config
1124}
1125
1126#[cfg(test)]
1127mod tests {
1128    use super::*;
1129
1130    #[test]
1131    fn test_header_parsing() {
1132        let header_line = "%%MatrixMarket matrix coordinate real general";
1133        let header = MMHeader::parse_header(header_line).expect("Operation failed");
1134
1135        assert_eq!(header.object, "matrix");
1136        assert_eq!(header.format, MMFormat::Coordinate);
1137        assert_eq!(header.data_type, MMDataType::Real);
1138        assert_eq!(header.symmetry, MMSymmetry::General);
1139    }
1140
1141    #[test]
1142    fn test_header_generation() {
1143        let header = MMHeader {
1144            object: "matrix".to_string(),
1145            format: MMFormat::Coordinate,
1146            data_type: MMDataType::Real,
1147            symmetry: MMSymmetry::General,
1148            comments: vec!["Test comment".to_string()],
1149        };
1150
1151        let header_line = header.to_header_line();
1152        assert_eq!(header_line, "%%MatrixMarket matrix coordinate real general");
1153    }
1154
1155    #[test]
1156    fn test_sparse_matrix_creation() {
1157        let header = MMHeader {
1158            object: "matrix".to_string(),
1159            format: MMFormat::Coordinate,
1160            data_type: MMDataType::Real,
1161            symmetry: MMSymmetry::General,
1162            comments: Vec::new(),
1163        };
1164
1165        let mut entries = Vec::new();
1166        entries.push(SparseEntry {
1167            row: 0,
1168            col: 0,
1169            value: 1.0,
1170        });
1171        entries.push(SparseEntry {
1172            row: 1,
1173            col: 1,
1174            value: 2.0,
1175        });
1176        entries.push(SparseEntry {
1177            row: 0,
1178            col: 1,
1179            value: 3.0,
1180        });
1181
1182        let matrix = MMSparseMatrix {
1183            header,
1184            rows: 2,
1185            cols: 2,
1186            nnz: 3,
1187            entries,
1188        };
1189
1190        assert_eq!(matrix.rows, 2);
1191        assert_eq!(matrix.cols, 2);
1192        assert_eq!(matrix.nnz, 3);
1193        assert_eq!(matrix.entries.len(), 3);
1194    }
1195
1196    #[test]
1197    fn test_sparse_to_coo_conversion() {
1198        let header = MMHeader {
1199            object: "matrix".to_string(),
1200            format: MMFormat::Coordinate,
1201            data_type: MMDataType::Real,
1202            symmetry: MMSymmetry::General,
1203            comments: Vec::new(),
1204        };
1205
1206        let entries = vec![
1207            SparseEntry {
1208                row: 0,
1209                col: 0,
1210                value: 1.0,
1211            },
1212            SparseEntry {
1213                row: 1,
1214                col: 1,
1215                value: 2.0,
1216            },
1217        ];
1218
1219        let matrix = MMSparseMatrix {
1220            header,
1221            rows: 2,
1222            cols: 2,
1223            nnz: 2,
1224            entries,
1225        };
1226
1227        let (rows, cols, values) = sparse_to_coo(&matrix);
1228
1229        assert_eq!(rows.len(), 2);
1230        assert_eq!(cols.len(), 2);
1231        assert_eq!(values.len(), 2);
1232        assert_eq!(rows[0], 0);
1233        assert_eq!(cols[0], 0);
1234        assert_eq!(values[0], 1.0);
1235        assert_eq!(rows[1], 1);
1236        assert_eq!(cols[1], 1);
1237        assert_eq!(values[1], 2.0);
1238    }
1239
1240    #[test]
1241    fn test_parallel_config_default() {
1242        let config = ParallelConfig::default();
1243        assert!(config.num_threads > 0);
1244        assert!(config.chunk_size > 0);
1245        assert!(config.buffer_size > 0);
1246    }
1247
1248    #[test]
1249    fn test_optimal_parallel_config() {
1250        // Small matrix
1251        let config = create_optimal_parallel_config(5000, None);
1252        assert_eq!(config.num_threads, 1);
1253        assert_eq!(config.chunk_size, 5000);
1254
1255        // Medium matrix
1256        let config = create_optimal_parallel_config(500_000, None);
1257        assert!(config.num_threads > 1);
1258        assert_eq!(config.chunk_size, 50_000);
1259
1260        // Large matrix
1261        let config = create_optimal_parallel_config(5_000_000, None);
1262        assert!(config.num_threads > 1);
1263        assert_eq!(config.chunk_size, 200_000);
1264        assert!(config.use_memory_mapping);
1265
1266        // Memory constrained
1267        let config = create_optimal_parallel_config(1_000_000, Some(100_000)); // 100KB memory
1268        assert!(config.use_memory_mapping);
1269        assert!(config.chunk_size < 1_000_000);
1270    }
1271
1272    #[test]
1273    fn test_parse_matrix_entry() {
1274        let header = MMHeader {
1275            object: "matrix".to_string(),
1276            format: MMFormat::Coordinate,
1277            data_type: MMDataType::Real,
1278            symmetry: MMSymmetry::General,
1279            comments: Vec::new(),
1280        };
1281
1282        // Test normal entry
1283        let entry = parse_matrix_entry(&format!("1 2 {}", std::f64::consts::PI), &header)
1284            .expect("Operation failed");
1285        assert_eq!(entry.row, 0); // 0-based
1286        assert_eq!(entry.col, 1); // 0-based
1287        assert!((entry.value - std::f64::consts::PI).abs() < 1e-10);
1288
1289        // Test pattern entry
1290        let mut pattern_header = header.clone();
1291        pattern_header.data_type = MMDataType::Pattern;
1292        let entry = parse_matrix_entry("5 10", &pattern_header).expect("Operation failed");
1293        assert_eq!(entry.row, 4);
1294        assert_eq!(entry.col, 9);
1295        assert_eq!(entry.value, 1.0);
1296    }
1297
1298    #[test]
1299    fn test_format_matrix_entry() {
1300        let header = MMHeader {
1301            object: "matrix".to_string(),
1302            format: MMFormat::Coordinate,
1303            data_type: MMDataType::Real,
1304            symmetry: MMSymmetry::General,
1305            comments: Vec::new(),
1306        };
1307
1308        let entry = SparseEntry {
1309            row: 0,
1310            col: 1,
1311            value: 2.5,
1312        };
1313
1314        let formatted = format_matrix_entry(&entry, &header);
1315        assert_eq!(formatted, "1 2 2.5"); // 1-based indexing
1316
1317        // Test pattern entry
1318        let mut pattern_header = header.clone();
1319        pattern_header.data_type = MMDataType::Pattern;
1320        let formatted = format_matrix_entry(&entry, &pattern_header);
1321        assert_eq!(formatted, "1 2");
1322    }
1323
1324    #[test]
1325    fn test_io_stats() {
1326        let mut stats = IOStats::default();
1327        assert_eq!(stats.entries_processed, 0);
1328        assert_eq!(stats.io_time_ms, 0.0);
1329        assert_eq!(stats.throughput_eps, 0.0);
1330        assert_eq!(stats.memory_usage_bytes, 0);
1331
1332        // Test throughput calculation
1333        stats.entries_processed = 1000;
1334        stats.io_time_ms = 100.0;
1335        stats.throughput_eps = stats.entries_processed as f64 / (stats.io_time_ms / 1000.0);
1336        assert_eq!(stats.throughput_eps, 10000.0); // 10k entries per second
1337    }
1338}