use super::{CooMatrix, MMsym, Sym};
use crate::{ComplexCooMatrix, StrError};
use russell_lab::{cpx, Complex64};
use std::ffi::OsStr;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
struct MatrixMarketData {
complex: bool,
symmetric: bool,
m: i32, n: i32, nnz: i32,
i: i32, j: i32, aij: f64, bij: f64, pos: i32, }
impl MatrixMarketData {
fn new() -> Self {
MatrixMarketData {
complex: false,
symmetric: false,
m: 0,
n: 0,
nnz: 0,
i: 0,
j: 0,
aij: 0.0,
bij: 0.0,
pos: 0,
}
}
#[inline]
fn parse_header(&mut self, line: &str) -> Result<(), StrError> {
let mut data = line.trim_start().trim_end_matches("\n").split_whitespace();
match data.next() {
Some(v) => {
if v != "%%MatrixMarket" {
return Err("the header (first line) must start with %%MatrixMarket");
}
}
None => return Err("cannot find the keyword %%MatrixMarket on the first line"),
}
match data.next() {
Some(v) => {
if v != "matrix" {
return Err("after %%MatrixMarket, the first option must be \"matrix\"");
}
}
None => return Err("cannot find the first option in the header line"),
}
match data.next() {
Some(v) => {
if v != "coordinate" {
return Err("after %%MatrixMarket, the second option must be \"coordinate\"");
}
}
None => return Err("cannot find the second option in the header line"),
}
match data.next() {
Some(v) => match v {
"real" => self.complex = false,
"complex" => self.complex = true,
_ => return Err("after %%MatrixMarket, the third option must be \"real\" or \"complex\""),
},
None => return Err("cannot find the third option in the header line"),
}
match data.next() {
Some(v) => match v {
"general" => self.symmetric = false,
"symmetric" => self.symmetric = true,
_ => return Err("after %%MatrixMarket, the fourth option must be either \"general\" or \"symmetric\""),
},
None => return Err("cannot find the fourth option in the header line"),
}
Ok(())
}
#[inline]
fn parse_dimensions(&mut self, line: &str) -> Result<bool, StrError> {
let maybe_data = line.trim_start().trim_end_matches("\n");
if maybe_data.starts_with("%") || maybe_data == "" {
return Ok(false); }
let mut data = maybe_data.split_whitespace();
self.m = data
.next()
.unwrap() .parse()
.map_err(|_| "cannot parse number of rows")?;
match data.next() {
Some(v) => self.n = v.parse().map_err(|_| "cannot parse number of columns")?,
None => return Err("cannot read number of columns"),
};
match data.next() {
Some(v) => self.nnz = v.parse().map_err(|_| "cannot parse number of non-zeros")?,
None => return Err("cannot read number of non-zeros"),
};
if self.m < 1 || self.n < 1 || self.nnz < 1 {
return Err("found invalid (zero or negative) dimensions");
}
Ok(true) }
#[inline]
fn parse_values(&mut self, line: &str) -> Result<bool, StrError> {
let maybe_data = line.trim_start().trim_end_matches("\n");
if maybe_data.starts_with("%") || maybe_data == "" {
return Ok(false); }
if self.pos == self.nnz {
return Err("there are more values than specified");
}
let mut data = maybe_data.split_whitespace();
self.i = data
.next()
.unwrap() .parse()
.map_err(|_| "cannot parse i")?;
match data.next() {
Some(v) => self.j = v.parse().map_err(|_| "cannot parse j")?,
None => return Err("cannot read j"),
};
match data.next() {
Some(v) => self.aij = v.parse().map_err(|_| "cannot parse aij")?,
None => return Err("cannot read aij"),
};
if self.complex {
match data.next() {
Some(v) => self.bij = v.parse().map_err(|_| "cannot parse bij")?,
None => return Err("cannot read bij"),
};
}
self.i -= 1; self.j -= 1;
if self.i < 0 || self.i >= self.m || self.j < 0 || self.j >= self.n {
return Err("found an invalid index");
}
self.pos += 1;
Ok(true) }
}
pub fn read_matrix_market<P>(
full_path: &P,
symmetric_handling: MMsym,
) -> Result<(Option<CooMatrix>, Option<ComplexCooMatrix>), StrError>
where
P: AsRef<OsStr> + ?Sized,
{
let path = Path::new(full_path).to_path_buf();
let input = File::open(path).map_err(|_| "cannot open file")?;
let buffered = BufReader::new(input);
let mut lines_iter = buffered.lines();
let mut data = MatrixMarketData::new();
let header = match lines_iter.next() {
Some(v) => v.unwrap(), None => return Err("the file is empty"),
};
data.parse_header(&header)?;
loop {
let line = lines_iter.next().unwrap().unwrap(); if data.parse_dimensions(&line)? {
break;
}
}
let sym = if data.symmetric {
if data.m != data.n {
return Err("MatrixMarket data is invalid: the number of rows must equal the number of columns for symmetric matrices");
}
match symmetric_handling {
MMsym::LeaveAsLower => Sym::YesLower,
MMsym::SwapToUpper => Sym::YesUpper,
MMsym::MakeItFull => Sym::YesFull,
}
} else {
Sym::No
};
let mut max = data.nnz;
if data.symmetric && symmetric_handling == MMsym::MakeItFull {
max = 2 * data.nnz;
}
if data.complex {
let mut coo = ComplexCooMatrix::new(data.m as usize, data.n as usize, max as usize, sym).unwrap();
loop {
match lines_iter.next() {
Some(v) => {
let line = v.unwrap(); if data.parse_values(&line)? {
if data.symmetric {
match symmetric_handling {
MMsym::LeaveAsLower => {
coo.put(data.i as usize, data.j as usize, cpx!(data.aij, data.bij))
.unwrap();
}
MMsym::SwapToUpper => {
coo.put(data.j as usize, data.i as usize, cpx!(data.aij, data.bij))
.unwrap();
}
MMsym::MakeItFull => {
coo.put(data.i as usize, data.j as usize, cpx!(data.aij, data.bij))
.unwrap();
if data.i != data.j {
coo.put(data.j as usize, data.i as usize, cpx!(data.aij, data.bij))
.unwrap();
}
}
}
} else {
coo.put(data.i as usize, data.j as usize, cpx!(data.aij, data.bij))
.unwrap();
};
}
}
None => break,
}
}
if data.pos != data.nnz {
return Err("not all values have been found");
}
Ok((None, Some(coo)))
} else {
let mut coo = CooMatrix::new(data.m as usize, data.n as usize, max as usize, sym).unwrap();
loop {
match lines_iter.next() {
Some(v) => {
let line = v.unwrap(); if data.parse_values(&line)? {
if data.symmetric {
match symmetric_handling {
MMsym::LeaveAsLower => {
coo.put(data.i as usize, data.j as usize, data.aij).unwrap();
}
MMsym::SwapToUpper => {
coo.put(data.j as usize, data.i as usize, data.aij).unwrap();
}
MMsym::MakeItFull => {
coo.put(data.i as usize, data.j as usize, data.aij).unwrap();
if data.i != data.j {
coo.put(data.j as usize, data.i as usize, data.aij).unwrap();
}
}
}
} else {
coo.put(data.i as usize, data.j as usize, data.aij).unwrap();
};
}
}
None => break,
}
}
if data.pos != data.nnz {
return Err("not all values have been found");
}
Ok((Some(coo), None))
}
}
#[cfg(test)]
mod tests {
use super::{read_matrix_market, MatrixMarketData};
use crate::{MMsym, Sym};
use russell_lab::{cpx, Complex64, Matrix};
#[test]
fn parse_header_captures_errors() {
let mut data = MatrixMarketData::new();
assert_eq!(
data.parse_header(" \n"),
Err("cannot find the keyword %%MatrixMarket on the first line")
);
assert_eq!(
data.parse_header("MatrixMarket "),
Err("the header (first line) must start with %%MatrixMarket"),
);
assert_eq!(
data.parse_header(" %%MatrixMarket"),
Err("cannot find the first option in the header line"),
);
assert_eq!(
data.parse_header("%%MatrixMarket wrong"),
Err("after %%MatrixMarket, the first option must be \"matrix\""),
);
assert_eq!(
data.parse_header("%%MatrixMarket matrix "),
Err("cannot find the second option in the header line"),
);
assert_eq!(
data.parse_header("%%MatrixMarket matrix wrong"),
Err("after %%MatrixMarket, the second option must be \"coordinate\""),
);
assert_eq!(
data.parse_header("%%MatrixMarket matrix coordinate"),
Err("cannot find the third option in the header line"),
);
assert_eq!(
data.parse_header("%%MatrixMarket matrix coordinate wrong"),
Err("after %%MatrixMarket, the third option must be \"real\" or \"complex\""),
);
assert_eq!(
data.parse_header("%%MatrixMarket matrix coordinate real"),
Err("cannot find the fourth option in the header line"),
);
assert_eq!(
data.parse_header(" %%MatrixMarket matrix coordinate real wrong"),
Err("after %%MatrixMarket, the fourth option must be either \"general\" or \"symmetric\""),
);
}
#[test]
fn parse_dimensions_captures_errors() {
let mut data = MatrixMarketData::new();
assert_eq!(
data.parse_dimensions(" wrong \n").err(),
Some("cannot parse number of rows")
);
assert_eq!(
data.parse_dimensions(" 1 \n").err(),
Some("cannot read number of columns")
);
assert_eq!(
data.parse_dimensions(" 1 wrong").err(),
Some("cannot parse number of columns")
);
assert_eq!(
data.parse_dimensions(" 1 1 \n").err(),
Some("cannot read number of non-zeros")
);
assert_eq!(
data.parse_dimensions(" 1 1 wrong").err(),
Some("cannot parse number of non-zeros")
);
assert_eq!(
data.parse_dimensions(" 0 1 1").err(),
Some("found invalid (zero or negative) dimensions")
);
assert_eq!(
data.parse_dimensions(" 1 0 1").err(),
Some("found invalid (zero or negative) dimensions")
);
assert_eq!(
data.parse_dimensions(" 1 1 0").err(),
Some("found invalid (zero or negative) dimensions")
);
}
#[test]
fn parse_values_captures_errors() {
let mut data = MatrixMarketData::new();
data.m = 2;
data.n = 2;
data.nnz = 1;
assert_eq!(data.parse_values(" wrong \n").err(), Some("cannot parse i"));
assert_eq!(data.parse_values(" 1 \n").err(), Some("cannot read j"));
assert_eq!(data.parse_values(" 1 wrong").err(), Some("cannot parse j"));
assert_eq!(data.parse_values(" 1 1 \n").err(), Some("cannot read aij"));
assert_eq!(data.parse_values(" 1 1 wrong").err(), Some("cannot parse aij"));
assert_eq!(data.parse_values(" 0 1 1").err(), Some("found an invalid index"));
assert_eq!(data.parse_values(" 3 1 1").err(), Some("found an invalid index"));
assert_eq!(data.parse_values(" 1 0 1").err(), Some("found an invalid index"));
assert_eq!(data.parse_values(" 1 3 1").err(), Some("found an invalid index"));
let mut data = MatrixMarketData::new();
data.complex = true;
data.m = 2;
data.n = 2;
data.nnz = 1;
assert_eq!(data.parse_values(" 1 1 1").err(), Some("cannot read bij"));
assert_eq!(data.parse_values(" 1 1 1 wrong").err(), Some("cannot parse bij"));
}
#[test]
fn read_matrix_market_handle_wrong_files() {
let h = MMsym::LeaveAsLower;
assert_eq!(read_matrix_market("__wrong__", h).err(), Some("cannot open file"));
assert_eq!(
read_matrix_market("./data/matrix_market/bad_empty_file.mtx", h).err(),
Some("the file is empty")
);
assert_eq!(
read_matrix_market("./data/matrix_market/bad_wrong_header.mtx", h).err(),
Some("after %%MatrixMarket, the first option must be \"matrix\"")
);
assert_eq!(
read_matrix_market("./data/matrix_market/bad_wrong_dims.mtx", h).err(),
Some("found invalid (zero or negative) dimensions")
);
assert_eq!(
read_matrix_market("./data/matrix_market/bad_wrong_dims_complex.mtx", h).err(),
Some("found invalid (zero or negative) dimensions")
);
assert_eq!(
read_matrix_market("./data/matrix_market/bad_missing_data.mtx", h).err(),
Some("not all values have been found")
);
assert_eq!(
read_matrix_market("./data/matrix_market/bad_missing_data_complex.mtx", h).err(),
Some("not all values have been found")
);
assert_eq!(
read_matrix_market("./data/matrix_market/bad_many_lines.mtx", h).err(),
Some("there are more values than specified")
);
assert_eq!(
read_matrix_market("./data/matrix_market/bad_many_lines_complex.mtx", h).err(),
Some("there are more values than specified")
);
assert_eq!(
read_matrix_market("./data/matrix_market/bad_symmetric_rectangular.mtx", h).err(),
Some("MatrixMarket data is invalid: the number of rows must equal the number of columns for symmetric matrices")
);
assert_eq!(
read_matrix_market("./data/matrix_market/bad_symmetric_rectangular_complex.mtx", h).err(),
Some("MatrixMarket data is invalid: the number of rows must equal the number of columns for symmetric matrices")
);
}
#[test]
fn read_matrix_market_works() {
let h = MMsym::LeaveAsLower;
let filepath = "./data/matrix_market/ok_general.mtx".to_string();
let (coo_real, coo_cpx) = read_matrix_market(&filepath, h).unwrap();
assert!(coo_cpx.is_none());
let coo = coo_real.unwrap();
assert_eq!(coo.symmetric, Sym::No);
assert_eq!((coo.nrow, coo.ncol, coo.nnz, coo.max_nnz), (5, 5, 12, 12));
assert_eq!(coo.indices_i, &[0, 1, 0, 2, 4, 1, 2, 3, 4, 2, 1, 4]);
assert_eq!(coo.indices_j, &[0, 0, 1, 1, 1, 2, 2, 2, 2, 3, 4, 4]);
assert_eq!(
coo.values,
&[2.0, 3.0, 3.0, -1.0, 4.0, 4.0, -3.0, 1.0, 2.0, 2.0, 6.0, 1.0]
);
}
#[test]
fn read_matrix_market_complex_works() {
let h = MMsym::LeaveAsLower;
let filepath = "./data/matrix_market/ok_complex_general.mtx".to_string();
let (coo_real, coo_cpx) = read_matrix_market(&filepath, h).unwrap();
assert!(coo_real.is_none());
let coo = coo_cpx.unwrap();
assert_eq!(coo.symmetric, Sym::No);
assert_eq!((coo.nrow, coo.ncol, coo.nnz, coo.max_nnz), (5, 5, 12, 12));
assert_eq!(coo.indices_i, &[0, 1, 0, 2, 4, 1, 2, 3, 4, 2, 1, 4]);
assert_eq!(coo.indices_j, &[0, 0, 1, 1, 1, 2, 2, 2, 2, 3, 4, 4]);
assert_eq!(
coo.values,
&[
cpx!(2.0, -1.0),
cpx!(3.0, -8.0),
cpx!(3.0, 80.0),
cpx!(-1.0, 30.0),
cpx!(4.0, 33.0),
cpx!(4.0, 60.0),
cpx!(-3.0, 6.0),
cpx!(1.0, 8.0),
cpx!(2.0, 3.0),
cpx!(2.0, 1.0),
cpx!(6.0, 9.0),
cpx!(1.0, -2.0)
]
);
}
#[test]
fn read_matrix_market_symmetric_lower_works() {
let h = MMsym::LeaveAsLower;
let filepath = "./data/matrix_market/ok_symmetric.mtx".to_string();
let (coo_real, coo_cpx) = read_matrix_market(&filepath, h).unwrap();
assert!(coo_cpx.is_none());
let coo = coo_real.unwrap();
assert_eq!(coo.symmetric, Sym::YesLower);
assert_eq!((coo.nrow, coo.ncol, coo.nnz, coo.max_nnz), (5, 5, 15, 15));
assert_eq!(coo.indices_i, &[0, 1, 2, 3, 4, 1, 2, 3, 4, 2, 3, 4, 3, 4, 4]);
assert_eq!(coo.indices_j, &[0, 1, 2, 3, 4, 0, 0, 0, 0, 1, 1, 1, 2, 2, 3]);
assert_eq!(
coo.values,
&[2.0, 2.0, 9.0, 7.0, 8.0, 1.0, 1.0, 3.0, 2.0, 2.0, 1.0, 1.0, 1.0, 5.0, 1.0],
);
}
#[test]
fn read_matrix_market_complex_symmetric_lower_works() {
let h = MMsym::LeaveAsLower;
let filepath = "./data/matrix_market/ok_complex_symmetric_small.mtx".to_string();
let (coo_real, coo_cpx) = read_matrix_market(&filepath, h).unwrap();
assert!(coo_real.is_none());
let coo = coo_cpx.unwrap();
assert_eq!(coo.symmetric, Sym::YesLower);
assert_eq!((coo.nrow, coo.ncol, coo.nnz, coo.max_nnz), (5, 5, 7, 7));
assert_eq!(coo.indices_i, &[0, 1, 2, 3, 3, 4, 4]);
assert_eq!(coo.indices_j, &[0, 0, 1, 2, 3, 1, 4]);
assert_eq!(
coo.values,
&[
cpx!(2.0, 1.0),
cpx!(3.0, 2.0),
cpx!(-1.0, 3.0),
cpx!(2.0, 4.0),
cpx!(3.0, 5.0),
cpx!(6.0, 6.0),
cpx!(1.0, 7.0),
]
);
}
#[test]
fn read_matrix_market_symmetric_upper_works() {
let h = MMsym::SwapToUpper;
let filepath = "./data/matrix_market/ok_symmetric.mtx".to_string();
let (maybe_coo, _) = read_matrix_market(&filepath, h).unwrap();
let coo = maybe_coo.unwrap();
assert_eq!(coo.symmetric, Sym::YesUpper);
assert_eq!((coo.nrow, coo.ncol, coo.nnz, coo.max_nnz), (5, 5, 15, 15));
assert_eq!(coo.indices_i, &[0, 1, 2, 3, 4, 0, 0, 0, 0, 1, 1, 1, 2, 2, 3]);
assert_eq!(coo.indices_j, &[0, 1, 2, 3, 4, 1, 2, 3, 4, 2, 3, 4, 3, 4, 4]);
assert_eq!(
coo.values,
&[2.0, 2.0, 9.0, 7.0, 8.0, 1.0, 1.0, 3.0, 2.0, 2.0, 1.0, 1.0, 1.0, 5.0, 1.0],
);
}
#[test]
fn read_matrix_market_complex_symmetric_upper_works() {
let h = MMsym::SwapToUpper;
let filepath = "./data/matrix_market/ok_complex_symmetric_small.mtx".to_string();
let (coo_real, coo_cpx) = read_matrix_market(&filepath, h).unwrap();
assert!(coo_real.is_none());
let coo = coo_cpx.unwrap();
assert_eq!(coo.symmetric, Sym::YesUpper);
assert_eq!((coo.nrow, coo.ncol, coo.nnz, coo.max_nnz), (5, 5, 7, 7));
assert_eq!(coo.indices_i, &[0, 0, 1, 2, 3, 1, 4]);
assert_eq!(coo.indices_j, &[0, 1, 2, 3, 3, 4, 4]);
assert_eq!(
coo.values,
&[
cpx!(2.0, 1.0),
cpx!(3.0, 2.0),
cpx!(-1.0, 3.0),
cpx!(2.0, 4.0),
cpx!(3.0, 5.0),
cpx!(6.0, 6.0),
cpx!(1.0, 7.0),
]
);
}
#[test]
fn read_matrix_market_symmetric_to_full_works() {
let h = MMsym::MakeItFull;
let filepath = "./data/matrix_market/ok_symmetric_small.mtx".to_string();
let (maybe_coo, _) = read_matrix_market(&filepath, h).unwrap();
let coo = maybe_coo.unwrap();
assert_eq!(coo.symmetric, Sym::YesFull);
assert_eq!((coo.nrow, coo.ncol, coo.nnz, coo.max_nnz), (5, 5, 11, 14));
assert_eq!(coo.indices_i, &[0, 1, 0, 2, 1, 3, 2, 3, 4, 1, 4, 0, 0, 0]);
assert_eq!(coo.indices_j, &[0, 0, 1, 1, 2, 2, 3, 3, 1, 4, 4, 0, 0, 0]);
assert_eq!(
coo.values,
&[2.0, 3.0, 3.0, -1.0, -1.0, 2.0, 2.0, 3.0, 6.0, 6.0, 1.0, 0.0, 0.0, 0.0]
);
let mut a = Matrix::new(5, 5);
coo.to_dense(&mut a).unwrap();
let correct = "┌ ┐\n\
│ 2 3 0 0 0 │\n\
│ 3 0 -1 0 6 │\n\
│ 0 -1 0 2 0 │\n\
│ 0 0 2 3 0 │\n\
│ 0 6 0 0 1 │\n\
└ ┘";
assert_eq!(format!("{}", a), correct);
}
#[test]
fn read_matrix_market_complex_symmetric_to_full_works() {
let h = MMsym::MakeItFull;
let filepath = "./data/matrix_market/ok_complex_symmetric_small.mtx".to_string();
let (coo_real, coo_cpx) = read_matrix_market(&filepath, h).unwrap();
assert!(coo_real.is_none());
let coo = coo_cpx.unwrap();
assert_eq!(coo.symmetric, Sym::YesFull);
assert_eq!((coo.nrow, coo.ncol, coo.nnz, coo.max_nnz), (5, 5, 11, 14));
assert_eq!(coo.indices_i, &[0, 1, 0, 2, 1, 3, 2, 3, 4, 1, 4, 0, 0, 0]);
assert_eq!(coo.indices_j, &[0, 0, 1, 1, 2, 2, 3, 3, 1, 4, 4, 0, 0, 0]);
assert_eq!(
coo.values,
&[
cpx!(2.0, 1.0),
cpx!(3.0, 2.0),
cpx!(3.0, 2.0),
cpx!(-1.0, 3.0),
cpx!(-1.0, 3.0),
cpx!(2.0, 4.0),
cpx!(2.0, 4.0),
cpx!(3.0, 5.0),
cpx!(6.0, 6.0),
cpx!(6.0, 6.0),
cpx!(1.0, 7.0),
cpx!(0.0, 0.0),
cpx!(0.0, 0.0),
cpx!(0.0, 0.0),
]
);
}
}