1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum MMDataType {
29 Real,
31 Complex,
33 Integer,
35 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum MMFormat {
67 Coordinate,
69 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum MMSymmetry {
97 General,
99 Symmetric,
101 Hermitian,
103 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#[derive(Debug, Clone)]
134pub struct MMHeader {
135 pub object: String,
137 pub format: MMFormat,
139 pub data_type: MMDataType,
141 pub symmetry: MMSymmetry,
143 pub comments: Vec<String>,
145}
146
147#[derive(Debug, Clone)]
149pub struct SparseEntry<T> {
150 pub row: usize,
152 pub col: usize,
154 pub value: T,
156}
157
158#[derive(Debug, Clone)]
160pub struct MMSparseMatrix<T> {
161 pub header: MMHeader,
163 pub rows: usize,
165 pub cols: usize,
167 pub nnz: usize,
169 pub entries: Vec<SparseEntry<T>>,
171}
172
173#[derive(Debug, Clone)]
175pub struct MMDenseMatrix<T> {
176 pub header: MMHeader,
178 pub rows: usize,
180 pub cols: usize,
182 pub data: Array2<T>,
184}
185
186#[derive(Debug, Clone)]
188pub struct ParallelConfig {
189 pub num_threads: usize,
191 pub chunk_size: usize,
193 pub buffer_size: usize,
195 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, buffer_size: 64 * 1024 * 1024, use_memory_mapping: false,
208 }
209 }
210}
211
212#[derive(Debug, Clone, Default)]
214pub struct IOStats {
215 pub io_time_ms: f64,
217 pub entries_processed: usize,
219 pub throughput_eps: f64,
221 pub memory_usage_bytes: usize,
223}
224
225impl MMHeader {
226 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 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#[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 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 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 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 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 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; let col = parts[1]
362 .parse::<usize>()
363 .map_err(|_| IoError::FormatError("Invalid column index".to_string()))?
364 - 1; let value = if header.data_type == MMDataType::Pattern {
367 1.0 } 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#[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 writeln!(writer, "{}", matrix.header.to_header_line())
440 .map_err(|e| IoError::FileError(e.to_string()))?;
441
442 for comment in &matrix.header.comments {
444 writeln!(writer, "%{}", comment).map_err(|e| IoError::FileError(e.to_string()))?;
445 }
446
447 writeln!(writer, "{} {} {}", matrix.rows, matrix.cols, matrix.nnz)
449 .map_err(|e| IoError::FileError(e.to_string()))?;
450
451 for entry in &matrix.entries {
453 if matrix.header.data_type == MMDataType::Pattern {
454 writeln!(writer, "{} {}", entry.row + 1, entry.col + 1)
456 .map_err(|e| IoError::FileError(e.to_string()))?;
457 } else {
458 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#[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 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 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 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 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 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#[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 writeln!(writer, "{}", matrix.header.to_header_line())
592 .map_err(|e| IoError::FileError(e.to_string()))?;
593
594 for comment in &matrix.header.comments {
596 writeln!(writer, "%{}", comment).map_err(|e| IoError::FileError(e.to_string()))?;
597 }
598
599 writeln!(writer, "{} {}", matrix.rows, matrix.cols)
601 .map_err(|e| IoError::FileError(e.to_string()))?;
602
603 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#[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#[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#[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 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 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 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 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 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 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 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 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#[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; let col = parts[1]
890 .parse::<usize>()
891 .map_err(|_| IoError::FormatError("Invalid column index".to_string()))?
892 - 1; let value = if header.data_type == MMDataType::Pattern {
895 1.0 } 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#[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 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 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 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 for handle in handles {
1010 handle
1011 .join()
1012 .map_err(|_| IoError::FormatError("Thread join failed".to_string()))??;
1013 }
1014
1015 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#[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#[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#[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 if nnz < 10_000 {
1098 config.num_threads = 1;
1100 config.chunk_size = nnz;
1101 } else if nnz < 1_000_000 {
1102 config.chunk_size = 50_000;
1104 } else {
1105 config.chunk_size = 200_000;
1107 config.use_memory_mapping = true;
1108 }
1109
1110 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); 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 let config = create_optimal_parallel_config(5000, None);
1252 assert_eq!(config.num_threads, 1);
1253 assert_eq!(config.chunk_size, 5000);
1254
1255 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 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 let config = create_optimal_parallel_config(1_000_000, Some(100_000)); 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 let entry = parse_matrix_entry(&format!("1 2 {}", std::f64::consts::PI), &header)
1284 .expect("Operation failed");
1285 assert_eq!(entry.row, 0); assert_eq!(entry.col, 1); assert!((entry.value - std::f64::consts::PI).abs() < 1e-10);
1288
1289 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"); 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 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); }
1338}