1use std::io::{BufWriter, Write};
10use std::path::Path;
11
12use sprs::CsMat;
13
14use crate::{Error, Result};
15
16pub fn write_mtx(matrix: &CsMat<f64>, path: impl AsRef<Path>) -> Result<()> {
17 let path = path.as_ref();
18 if is_exactly_symmetric(matrix) {
19 write_symmetric_mtx(matrix, path)
20 } else {
21 sprs::io::write_matrix_market(path, matrix.view()).map_err(|e| Error::Mtx(e.to_string()))
22 }
23}
24
25fn write_symmetric_mtx(matrix: &CsMat<f64>, path: &Path) -> Result<()> {
26 let f = std::fs::File::create(path)?;
27 let mut w = BufWriter::new(f);
28 writeln!(w, "%%MatrixMarket matrix coordinate real symmetric")?;
29 writeln!(w, "% written by powerio")?;
30
31 let nnz = matrix
33 .iter()
34 .filter(|&(_, (i, j))| i >= j)
35 .filter(|&(&v, _)| v != 0.0)
36 .count();
37 writeln!(w, "{} {} {}", matrix.rows(), matrix.cols(), nnz)?;
38
39 for (&v, (i, j)) in matrix {
40 if i < j || v == 0.0 {
41 continue;
42 }
43 writeln!(w, "{} {} {:.16e}", i + 1, j + 1, v)?;
44 }
45 Ok(())
46}
47
48pub fn read_mtx(path: impl AsRef<Path>) -> Result<CsMat<f64>> {
50 let tri: sprs::TriMat<f64> =
51 sprs::io::read_matrix_market(path).map_err(|e| Error::Mtx(e.to_string()))?;
52 Ok(tri.to_csr())
53}
54
55pub fn read_vector_mtx(path: impl AsRef<Path>) -> Result<Vec<f64>> {
58 let text = std::fs::read_to_string(path)?;
59 let mut lines = text.lines().filter(|l| !l.starts_with('%'));
60 let header = lines
61 .next()
62 .ok_or_else(|| Error::Mtx("empty vector file".into()))?;
63 let len: usize = header
64 .split_whitespace()
65 .next()
66 .and_then(|t| t.parse().ok())
67 .ok_or_else(|| Error::Mtx(format!("bad vector dimensions line: {header:?}")))?;
68 let values = lines
69 .take(len)
70 .map(|l| {
71 l.trim()
72 .parse::<f64>()
73 .map_err(|_| Error::Mtx(format!("bad vector entry: {l:?}")))
74 })
75 .collect::<Result<Vec<_>>>()?;
76 if values.len() != len {
77 return Err(Error::Mtx(format!(
78 "expected {len} entries, got {}",
79 values.len()
80 )));
81 }
82 Ok(values)
83}
84
85pub fn write_vector_mtx(values: &[f64], path: impl AsRef<Path>) -> Result<()> {
87 let f = std::fs::File::create(path)?;
88 let mut w = BufWriter::new(f);
89 writeln!(w, "%%MatrixMarket matrix array real general")?;
90 writeln!(w, "% written by powerio")?;
91 writeln!(w, "{} 1", values.len())?;
92 for v in values {
93 writeln!(w, "{v:.16e}")?;
94 }
95 Ok(())
96}
97
98fn is_exactly_symmetric(a: &CsMat<f64>) -> bool {
105 if a.rows() != a.cols() {
106 return false;
107 }
108 for (i, row) in a.outer_iterator().enumerate() {
109 for (j, &v) in row.iter() {
110 match a.get(j, i) {
113 Some(&mirror) if mirror.to_bits() == v.to_bits() => {}
114 _ => return false,
115 }
116 }
117 }
118 true
119}
120
121#[cfg(test)]
122mod tests {
123 use sprs::TriMat;
124
125 use super::write_mtx;
126
127 #[test]
128 fn value_asymmetric_matrix_writes_general_mtx() {
129 let mut tri = TriMat::new((2, 2));
130 tri.add_triplet(0, 0, 2.0);
131 tri.add_triplet(0, 1, -1.0);
132 tri.add_triplet(1, 0, -2.0);
133 tri.add_triplet(1, 1, 2.0);
134 let matrix = tri.to_csr();
135
136 let path = temp_path("value-asymmetric");
137 write_mtx(&matrix, &path).unwrap();
138 let text = std::fs::read_to_string(&path).unwrap();
139 let _ = std::fs::remove_file(&path);
140
141 assert!(
142 text.lines().next().unwrap().ends_with("general"),
143 "value-asymmetric matrices must not be written with a symmetric header"
144 );
145 }
146
147 #[test]
148 fn a_matrix_asymmetric_below_the_old_tolerance_writes_general() {
149 let mut tri = TriMat::new((2, 2));
154 tri.add_triplet(0, 0, 5.0);
155 tri.add_triplet(0, 1, 3.000_000_000_000_001);
156 tri.add_triplet(1, 0, 3.0);
157 tri.add_triplet(1, 1, 5.0);
158 let matrix = tri.to_csr();
159
160 let path = temp_path("near-symmetric");
161 write_mtx(&matrix, &path).unwrap();
162 let text = std::fs::read_to_string(&path).unwrap();
163 let _ = std::fs::remove_file(&path);
164
165 assert!(
166 text.lines().next().unwrap().ends_with("general"),
167 "a matrix that is only nearly symmetric must carry both triangles:\n{text}"
168 );
169 }
170
171 #[test]
172 fn a_structurally_asymmetric_matrix_writes_general() {
173 let mut tri = TriMat::new((2, 2));
176 tri.add_triplet(0, 0, 5.0);
177 tri.add_triplet(0, 1, 0.0);
178 tri.add_triplet(1, 1, 5.0);
179 let matrix = tri.to_csr();
180
181 let path = temp_path("structurally-asymmetric");
182 write_mtx(&matrix, &path).unwrap();
183 let text = std::fs::read_to_string(&path).unwrap();
184 let _ = std::fs::remove_file(&path);
185
186 assert!(
187 text.lines().next().unwrap().ends_with("general"),
188 "an unmirrored stored entry must not claim a symmetric header:\n{text}"
189 );
190 }
191
192 #[test]
193 fn an_exactly_symmetric_matrix_still_writes_symmetric() {
194 let mut tri = TriMat::new((2, 2));
195 tri.add_triplet(0, 0, 5.0);
196 tri.add_triplet(0, 1, -3.0);
197 tri.add_triplet(1, 0, -3.0);
198 tri.add_triplet(1, 1, 5.0);
199 let matrix = tri.to_csr();
200
201 let path = temp_path("exactly-symmetric");
202 write_mtx(&matrix, &path).unwrap();
203 let text = std::fs::read_to_string(&path).unwrap();
204 let _ = std::fs::remove_file(&path);
205
206 assert!(
207 text.lines().next().unwrap().ends_with("symmetric"),
208 "an exactly symmetric matrix keeps the compact form:\n{text}"
209 );
210 }
211
212 fn temp_path(stem: &str) -> std::path::PathBuf {
213 let mut path = std::env::temp_dir();
214 let nanos = std::time::SystemTime::now()
215 .duration_since(std::time::UNIX_EPOCH)
216 .map_or(0, |d| d.as_nanos());
217 path.push(format!("powerio-{stem}-{nanos}.mtx"));
218 path
219 }
220}