use std::io::{BufWriter, Write};
use std::path::Path;
use sprs::CsMat;
use crate::{Error, Result};
pub fn write_mtx(matrix: &CsMat<f64>, path: impl AsRef<Path>) -> Result<()> {
let path = path.as_ref();
if is_numerically_symmetric(matrix) {
write_symmetric_mtx(matrix, path)
} else {
sprs::io::write_matrix_market(path, matrix.view()).map_err(|e| Error::Mtx(e.to_string()))
}
}
fn write_symmetric_mtx(matrix: &CsMat<f64>, path: &Path) -> Result<()> {
let f = std::fs::File::create(path)?;
let mut w = BufWriter::new(f);
writeln!(w, "%%MatrixMarket matrix coordinate real symmetric")?;
writeln!(w, "% written by powerio")?;
let nnz = matrix
.iter()
.filter(|&(_, (i, j))| i >= j)
.filter(|&(&v, _)| v != 0.0)
.count();
writeln!(w, "{} {} {}", matrix.rows(), matrix.cols(), nnz)?;
for (&v, (i, j)) in matrix {
if i < j || v == 0.0 {
continue;
}
writeln!(w, "{} {} {:.16e}", i + 1, j + 1, v)?;
}
Ok(())
}
pub fn read_mtx(path: impl AsRef<Path>) -> Result<CsMat<f64>> {
let tri: sprs::TriMat<f64> =
sprs::io::read_matrix_market(path).map_err(|e| Error::Mtx(e.to_string()))?;
Ok(tri.to_csr())
}
pub fn read_vector_mtx(path: impl AsRef<Path>) -> Result<Vec<f64>> {
let text = std::fs::read_to_string(path)?;
let mut lines = text.lines().filter(|l| !l.starts_with('%'));
let header = lines
.next()
.ok_or_else(|| Error::Mtx("empty vector file".into()))?;
let len: usize = header
.split_whitespace()
.next()
.and_then(|t| t.parse().ok())
.ok_or_else(|| Error::Mtx(format!("bad vector dimensions line: {header:?}")))?;
let values = lines
.take(len)
.map(|l| {
l.trim()
.parse::<f64>()
.map_err(|_| Error::Mtx(format!("bad vector entry: {l:?}")))
})
.collect::<Result<Vec<_>>>()?;
if values.len() != len {
return Err(Error::Mtx(format!(
"expected {len} entries, got {}",
values.len()
)));
}
Ok(values)
}
pub fn write_vector_mtx(values: &[f64], path: impl AsRef<Path>) -> Result<()> {
let f = std::fs::File::create(path)?;
let mut w = BufWriter::new(f);
writeln!(w, "%%MatrixMarket matrix array real general")?;
writeln!(w, "% written by powerio")?;
writeln!(w, "{} 1", values.len())?;
for v in values {
writeln!(w, "{v:.16e}")?;
}
Ok(())
}
fn is_numerically_symmetric(a: &CsMat<f64>) -> bool {
if a.rows() != a.cols() {
return false;
}
for (i, row) in a.outer_iterator().enumerate() {
for (j, &v) in row.iter() {
let mirror = a.get(j, i).copied().unwrap_or(0.0);
let scale = v.abs().max(mirror.abs()).max(1.0);
if (v - mirror).abs() > 1e-12 * scale {
return false;
}
}
}
true
}
#[cfg(test)]
mod tests {
use sprs::TriMat;
use super::write_mtx;
#[test]
fn value_asymmetric_matrix_writes_general_mtx() {
let mut tri = TriMat::new((2, 2));
tri.add_triplet(0, 0, 2.0);
tri.add_triplet(0, 1, -1.0);
tri.add_triplet(1, 0, -2.0);
tri.add_triplet(1, 1, 2.0);
let matrix = tri.to_csr();
let path = temp_path("value-asymmetric");
write_mtx(&matrix, &path).unwrap();
let text = std::fs::read_to_string(&path).unwrap();
let _ = std::fs::remove_file(&path);
assert!(
text.lines().next().unwrap().ends_with("general"),
"value-asymmetric matrices must not be written with a symmetric header"
);
}
fn temp_path(stem: &str) -> std::path::PathBuf {
let mut path = std::env::temp_dir();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos());
path.push(format!("powerio-{stem}-{nanos}.mtx"));
path
}
}