From 332fe8c0e49f0b413762d245946aeab52adbda62 Mon Sep 17 00:00:00 2001 From: Hantao Hui Date: Wed, 17 Nov 2021 16:32:57 +0100 Subject: [PATCH] update loading matrix market, including: 1. Several renamings 2. removed unnecessary traits 3. added support for dense matrix 4. removed unnecessary comparison when reading data line by line. (e.g. only read header line once) --- nalgebra-sparse/src/io/matrix_market.pest | 31 +- nalgebra-sparse/src/io/matrix_market.rs | 1232 +++++++++++------ nalgebra-sparse/src/io/mod.rs | 6 +- .../tests/unit_tests/matrix_market.rs | 206 ++- 4 files changed, 1031 insertions(+), 444 deletions(-) diff --git a/nalgebra-sparse/src/io/matrix_market.pest b/nalgebra-sparse/src/io/matrix_market.pest index 73809166..aeeba406 100644 --- a/nalgebra-sparse/src/io/matrix_market.pest +++ b/nalgebra-sparse/src/io/matrix_market.pest @@ -1,9 +1,8 @@ -WHITESPACE = _{ " " } +WHITESPACE = _{ " "|"\t" } // -// dense matrix(array) not supported here -Sparsity = {^"coordinate"} +Sparsity = {^"coordinate" | ^"array"} DataType = {^"real" | ^"complex" | ^"pattern" | ^"integer" } StorageScheme = {^"symmetric" | ^"general" | ^"skew-symmetric" | ^"hermitian"} // Only consider matrices here. @@ -16,7 +15,9 @@ Comments = _{ "%" ~ (!NEWLINE ~ ANY)* } // Dimension = @{ ASCII_DIGIT+ } -Shape = { Dimension ~ Dimension ~ Dimension } +SparseShape = { Dimension ~ Dimension ~ Dimension} +DenseShape = { Dimension ~ Dimension} +Shape = {SparseShape | DenseShape } // @@ -25,14 +26,21 @@ Shape = { Dimension ~ Dimension ~ Dimension } Sign = {("+" | "-")} Exp = @{ ^"e" ~ Sign? ~ ASCII_DIGIT+} Number = @{ ((ASCII_DIGIT+ ~ "." ~ ASCII_DIGIT*) | (ASCII_DIGIT* ~ "." ~ASCII_DIGIT+) | ASCII_DIGIT+ ) ~ Exp? } -Value = @{ Sign? ~ ("inf" | "NaN" | Number) } +Real = @{ Sign? ~ ("inf" | "NaN" | Number) } -// zero value: pattern -// one Value: real or int number -// two values: complex number -Entry = { Dimension ~ Dimension ~ Value? ~ Value? } -// +SparseReal = {Dimension~ Dimension~ Real } +SparseComplex = {Dimension ~ Dimension ~ Real ~ Real} +SparsePattern = {Dimension ~ Dimension} + +DenseReal = {Real} +DenseComplex = {Real ~ Real} + + +Entry = { SparseComplex | SparseReal | SparsePattern | DenseComplex | DenseReal } + +// end of file, a silent way, see https://github.com/pest-parser/pest/issues/304#issuecomment-427198507 +eoi = _{ !ANY } Document = { SOI ~ @@ -40,5 +48,6 @@ Document = { Header ~ (NEWLINE ~ Comments)* ~ (NEWLINE ~ Shape) ~ - (NEWLINE ~ Entry?)* + (NEWLINE ~ Entry?)* ~ + eoi } \ No newline at end of file diff --git a/nalgebra-sparse/src/io/matrix_market.rs b/nalgebra-sparse/src/io/matrix_market.rs index 9ff4abec..3be7a8ac 100644 --- a/nalgebra-sparse/src/io/matrix_market.rs +++ b/nalgebra-sparse/src/io/matrix_market.rs @@ -1,48 +1,51 @@ -//! use mm(or MM) to represent martrix market +//! Implementation of matrix market io code. +//! +//! See the [website](https://math.nist.gov/MatrixMarket/formats.html) or the [paper](https://www.researchgate.net/publication/2630533_The_Matrix_Market_Exchange_Formats_Initial_Design) for more details about matrix market. use crate::coo::CooMatrix; use crate::SparseFormatError; use crate::SparseFormatErrorKind; +use nalgebra::base::Scalar; use nalgebra::Complex; use pest::iterators::Pairs; use pest::Parser; use std::cmp::PartialEq; +use std::convert::Infallible; +use std::convert::TryFrom; use std::fmt; use std::fmt::Formatter; use std::fs; use std::num::ParseIntError; -use std::ops::Neg; +use std::num::TryFromIntError; use std::path::Path; use std::str::FromStr; /// A description of the error that occurred during importing a matrix from a matrix market format data. #[derive(Debug)] -pub struct MMError { - error_kind: MMErrorKind, +pub struct MatrixMarketError { + error_kind: MatrixMarketErrorKind, message: String, } /// Errors produced by functions that expect well-formed matrix market format data. -/// > _NOTE1:_ Since the matrix market design didn't mention if duplicate entries are allowed or not, so, it's allowed here. -/// -/// > _NOTE2:_ Dense matrices are not supported here. For example, `%%matrixmarket matrix array real general` will give `MMErrorKind::ParsingError`. +/// > _NOTE:_ Since the matrix market design didn't mention if multiple sparse entries with the same coordiantes are allowed or not, so, it's allowed here. #[non_exhaustive] #[derive(Copy, Clone, Debug, PartialEq)] -pub enum MMErrorKind { +pub enum MatrixMarketErrorKind { /// Indicates that some word is not known to MM format /// /// Examples /// -------- /// ```rust - /// # use nalgebra_sparse::io::load_coo_from_mm_str; - /// # use nalgebra_sparse::io::MMErrorKind; + /// # use nalgebra_sparse::io::load_coo_from_matrix_market_str; + /// # use nalgebra_sparse::io::MatrixMarketErrorKind; /// let str = r#" /// %%MatrixMarket whatever whatever whatever whatever /// 1 1 1 /// 1 1 5 /// "#; - /// let matrix_error = load_coo_from_mm_str::(str); + /// let matrix_error = load_coo_from_matrix_market_str::(str); /// assert_eq!(matrix_error.is_err(), true); - /// assert_eq!(*matrix_error.unwrap_err().kind(),MMErrorKind::ParsingError); + /// assert_eq!(matrix_error.unwrap_err().kind(),MatrixMarketErrorKind::ParsingError); /// ``` ParsingError, @@ -51,17 +54,17 @@ pub enum MMErrorKind { /// Examples /// -------- /// ```rust - /// # use nalgebra_sparse::io::load_coo_from_mm_str; - /// # use nalgebra_sparse::io::MMErrorKind; + /// # use nalgebra_sparse::io::load_coo_from_matrix_market_str; + /// # use nalgebra_sparse::io::MatrixMarketErrorKind; /// let str = r#" /// %%MatrixMarket matrix coordinate real hermitian /// % a real matrix can't be hermitian /// 1 1 1 /// 1 1 5 /// "#; - /// let matrix_error = load_coo_from_mm_str::(str); + /// let matrix_error = load_coo_from_matrix_market_str::(str); /// assert_eq!(matrix_error.is_err(), true); - /// assert_eq!(*matrix_error.unwrap_err().kind(),MMErrorKind::InvalidHeader); + /// assert_eq!(matrix_error.unwrap_err().kind(),MatrixMarketErrorKind::InvalidHeader); /// ``` InvalidHeader, @@ -70,8 +73,8 @@ pub enum MMErrorKind { /// Examples /// -------- /// ```rust - /// # use nalgebra_sparse::io::load_coo_from_mm_str; - /// # use nalgebra_sparse::io::MMErrorKind; + /// # use nalgebra_sparse::io::load_coo_from_matrix_market_str; + /// # use nalgebra_sparse::io::MatrixMarketErrorKind; /// let str = r#" /// %%matrixmarket matrix coordinate real general /// % it has one more data entry than specified. @@ -79,9 +82,9 @@ pub enum MMErrorKind { /// 2 2 2 /// 2 3 2 /// "#; - /// let matrix_error = load_coo_from_mm_str::(str); + /// let matrix_error = load_coo_from_matrix_market_str::(str); /// assert_eq!(matrix_error.is_err(), true); - /// assert_eq!(*matrix_error.unwrap_err().kind(),MMErrorKind::EntryNumUnmatched); + /// assert_eq!(matrix_error.unwrap_err().kind(),MatrixMarketErrorKind::EntryNumUnmatched); /// ``` EntryNumUnmatched, @@ -90,55 +93,55 @@ pub enum MMErrorKind { /// Examples /// -------- /// ```rust - /// # use nalgebra_sparse::io::load_coo_from_mm_str; - /// # use nalgebra_sparse::io::MMErrorKind; + /// # use nalgebra_sparse::io::load_coo_from_matrix_market_str; + /// # use nalgebra_sparse::io::MatrixMarketErrorKind; /// let str = r#" - /// %%matrixmarket matrix coordinate integer general - /// % it should be called by load_coo_from_mm_str::(str), or any other integer type; - /// 3 3 1 - /// 2 2 2 - /// 2 3 2 + /// %%matrixmarket matrix coordinate real general + /// % it should be called by load_coo_from_matrix_market_str::(str), or f32; + /// 3 3 2 + /// 2 2 2.22 + /// 2 3 2.22 /// "#; - /// let matrix_error = load_coo_from_mm_str::(str); + /// let matrix_error = load_coo_from_matrix_market_str::(str); /// assert_eq!(matrix_error.is_err(), true); - /// assert_eq!(*matrix_error.unwrap_err().kind(),MMErrorKind::TypeUnmatched); + /// assert_eq!(matrix_error.unwrap_err().kind(),MatrixMarketErrorKind::TypeUnmatched); /// ``` TypeUnmatched, - /// Indicates that zero has been used as an index in the data, which is not allowed. + /// Indicates that zero has been used as an index in the data, or the shape of the matrix, which is not allowed. /// /// Examples /// -------- /// ```rust - /// # use nalgebra_sparse::io::load_coo_from_mm_str; - /// # use nalgebra_sparse::io::MMErrorKind; + /// # use nalgebra_sparse::io::load_coo_from_matrix_market_str; + /// # use nalgebra_sparse::io::MatrixMarketErrorKind; /// let str = r#" /// %%matrixmarket matrix coordinate real general /// 1 1 1 /// 0 0 10 /// "#; - /// let matrix_error = load_coo_from_mm_str::(str); + /// let matrix_error = load_coo_from_matrix_market_str::(str); /// assert_eq!(matrix_error.is_err(), true); - /// assert_eq!(*matrix_error.unwrap_err().kind(),MMErrorKind::ZeroIndexed); + /// assert_eq!(matrix_error.unwrap_err().kind(),MatrixMarketErrorKind::ZeroError); /// ``` - ZeroIndexed, + ZeroError, /// Indicates [SparseFormatError], while creating the sparse matrix. /// /// Examples /// -------- /// ```rust - /// # use nalgebra_sparse::io::load_coo_from_mm_str; - /// # use nalgebra_sparse::io::MMErrorKind; + /// # use nalgebra_sparse::io::load_coo_from_matrix_market_str; + /// # use nalgebra_sparse::io::MatrixMarketErrorKind; /// # use nalgebra_sparse::SparseFormatErrorKind; /// let str = r#" /// %%matrixmarket matrix coordinate real general /// 1 1 1 /// 4 2 10 /// "#; - /// let matrix_error = load_coo_from_mm_str::(str); + /// let matrix_error = load_coo_from_matrix_market_str::(str); /// assert_eq!(matrix_error.is_err(), true); - /// assert_eq!(*matrix_error.unwrap_err().kind(),MMErrorKind::SparseFormatError(SparseFormatErrorKind::IndexOutOfBounds)); + /// assert_eq!(matrix_error.unwrap_err().kind(),MatrixMarketErrorKind::SparseFormatError(SparseFormatErrorKind::IndexOutOfBounds)); /// ``` SparseFormatError(SparseFormatErrorKind), @@ -147,8 +150,8 @@ pub enum MMErrorKind { /// Examples /// -------- /// ```rust - /// # use nalgebra_sparse::io::load_coo_from_mm_str; - /// # use nalgebra_sparse::io::MMErrorKind; + /// # use nalgebra_sparse::io::load_coo_from_matrix_market_str; + /// # use nalgebra_sparse::io::MatrixMarketErrorKind; /// # use nalgebra::Complex; /// let str = r#" /// %%matrixmarket matrix coordinate real skew-symmetric @@ -157,9 +160,9 @@ pub enum MMErrorKind { /// 1 1 10 /// 2 1 5 /// "#; - /// let matrix_error = load_coo_from_mm_str::(str); + /// let matrix_error = load_coo_from_matrix_market_str::(str); /// assert_eq!(matrix_error.is_err(), true); - /// assert_eq!(*matrix_error.unwrap_err().kind(),MMErrorKind::DiagonalError); + /// assert_eq!(matrix_error.unwrap_err().kind(),MatrixMarketErrorKind::DiagonalError); /// /// let str = r#" /// %%matrixmarket matrix coordinate complex hermitian @@ -168,9 +171,9 @@ pub enum MMErrorKind { /// 1 1 10 2 /// 2 1 5 2 /// "#; - /// let matrix_error = load_coo_from_mm_str::>(str); + /// let matrix_error = load_coo_from_matrix_market_str::>(str); /// assert_eq!(matrix_error.is_err(), true); - /// assert_eq!(*matrix_error.unwrap_err().kind(),MMErrorKind::DiagonalError); + /// assert_eq!(matrix_error.unwrap_err().kind(),MatrixMarketErrorKind::DiagonalError); /// ``` /// Here the skew matrix shouldn't have an element on the diagonal DiagonalError, @@ -180,12 +183,12 @@ pub enum MMErrorKind { /// Examples /// -------- /// ```rust - /// # use nalgebra_sparse::io::load_coo_from_mm_file; - /// # use nalgebra_sparse::io::MMErrorKind; + /// # use nalgebra_sparse::io::load_coo_from_matrix_market_file; + /// # use nalgebra_sparse::io::MatrixMarketErrorKind; /// let file_name = "whatever.mtx"; - /// let matrix_error = load_coo_from_mm_file::(file_name); + /// let matrix_error = load_coo_from_matrix_market_file::(file_name); /// assert_eq!(matrix_error.is_err(), true); - /// assert_eq!(*matrix_error.unwrap_err().kind(),MMErrorKind::IOError(std::io::ErrorKind::NotFound)); + /// assert_eq!(matrix_error.unwrap_err().kind(),MatrixMarketErrorKind::IOError(std::io::ErrorKind::NotFound)); /// ``` IOError(std::io::ErrorKind), @@ -194,23 +197,42 @@ pub enum MMErrorKind { /// Examples /// -------- /// ```rust - /// # use nalgebra_sparse::io::load_coo_from_mm_str; - /// # use nalgebra_sparse::io::MMErrorKind; + /// # use nalgebra_sparse::io::load_coo_from_matrix_market_str; + /// # use nalgebra_sparse::io::MatrixMarketErrorKind; /// let str = r#" /// %%matrixmarket matrix coordinate integer symmetric /// 5 5 2 /// 1 1 10 /// 2 3 5 /// "#; - /// let matrix_error = load_coo_from_mm_str::(str); + /// let matrix_error = load_coo_from_matrix_market_str::(str); /// assert_eq!(matrix_error.is_err(), true); - /// assert_eq!(*matrix_error.unwrap_err().kind(),MMErrorKind::LowerTriangleError); + /// assert_eq!(matrix_error.unwrap_err().kind(),MatrixMarketErrorKind::NotLowerTriangle); /// ``` - LowerTriangleError, + NotLowerTriangle, + + /// Indicates (skew-)symmetric (or hermitian) matrix is not square matrix. + /// + /// Examples + /// -------- + /// ```rust + /// # use nalgebra_sparse::io::load_coo_from_matrix_market_str; + /// # use nalgebra_sparse::io::MatrixMarketErrorKind; + /// let str = r#" + /// %%matrixmarket matrix coordinate integer symmetric + /// 5 4 2 + /// 1 1 10 + /// 2 3 5 + /// "#; + /// let matrix_error = load_coo_from_matrix_market_str::(str); + /// assert_eq!(matrix_error.is_err(), true); + /// assert_eq!(matrix_error.unwrap_err().kind(),MatrixMarketErrorKind::NotSquareMatrix); + /// ``` + NotSquareMatrix, } -impl MMError { - fn from_kind_and_message(error_type: MMErrorKind, message: String) -> Self { +impl MatrixMarketError { + fn from_kind_and_message(error_type: MatrixMarketErrorKind, message: String) -> Self { Self { error_kind: error_type, message, @@ -219,8 +241,8 @@ impl MMError { /// The operation error kind. #[must_use] - pub fn kind(&self) -> &MMErrorKind { - &self.error_kind + pub fn kind(&self) -> MatrixMarketErrorKind { + self.error_kind } /// The underlying error message. @@ -230,82 +252,112 @@ impl MMError { } } -impl fmt::Display for MMError { +impl fmt::Display for MatrixMarketError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "Matrix Market load error: ")?; match self.kind() { - MMErrorKind::ParsingError => { + MatrixMarketErrorKind::ParsingError => { write!(f, "ParsingError,")?; } - MMErrorKind::InvalidHeader => { + MatrixMarketErrorKind::InvalidHeader => { write!(f, "InvalidHeader,")?; } - MMErrorKind::EntryNumUnmatched => { + MatrixMarketErrorKind::EntryNumUnmatched => { write!(f, "EntryNumUnmatched,")?; } - MMErrorKind::TypeUnmatched => { + MatrixMarketErrorKind::TypeUnmatched => { write!(f, "TypeUnmatched,")?; } - MMErrorKind::SparseFormatError(_) => { + MatrixMarketErrorKind::SparseFormatError(_) => { write!(f, "SparseFormatError,")?; } - MMErrorKind::ZeroIndexed => { - write!(f, "ZeroIndexed,")?; + MatrixMarketErrorKind::ZeroError => { + write!(f, "ZeroError,")?; } - MMErrorKind::IOError(_) => { + MatrixMarketErrorKind::IOError(_) => { write!(f, "IOError,")?; } - MMErrorKind::DiagonalError => { + MatrixMarketErrorKind::DiagonalError => { write!(f, "DiagonalError,")?; } - MMErrorKind::LowerTriangleError => { - write!(f, "LowerTriangleError,")?; + MatrixMarketErrorKind::NotLowerTriangle => { + write!(f, "NotLowerTriangle,")?; + } + MatrixMarketErrorKind::NotSquareMatrix => { + write!(f, "NotSquareMatrix,")?; } } write!(f, " Message: {}", self.message) } } -impl std::error::Error for MMError {} +impl std::error::Error for MatrixMarketError {} impl From> - for MMError + for MatrixMarketError { fn from(err: pest::error::Error) -> Self { Self::from_kind_and_message( - MMErrorKind::ParsingError, + MatrixMarketErrorKind::ParsingError, format!("Can't parse the data.\n Error: {}", err), ) } } -impl From for MMError { +impl From for MatrixMarketError { fn from(err: ParseIntError) -> Self { Self::from_kind_and_message( - MMErrorKind::ParsingError, + MatrixMarketErrorKind::ParsingError, format!("Can't parse data as i128.\n Error: {}", err), ) } } -impl From for MMError { +impl From for MatrixMarketError { fn from(err: SparseFormatError) -> Self { Self::from_kind_and_message( - MMErrorKind::SparseFormatError(*err.kind()), + MatrixMarketErrorKind::SparseFormatError(*err.kind()), format!("{}", &err), ) } } -impl From for MMError { +impl From for MatrixMarketError { fn from(err: std::io::Error) -> Self { - Self::from_kind_and_message(MMErrorKind::IOError(err.kind()), format!("{}", &err)) + Self::from_kind_and_message( + MatrixMarketErrorKind::IOError(err.kind()), + format!("{}", &err), + ) + } +} + +impl From for MatrixMarketError { + fn from(err: TryFromIntError) -> Self { + Self::from_kind_and_message( + MatrixMarketErrorKind::TypeUnmatched, + format!( + "Please consider using a larger integery type. Error message: {}", + &err + ), + ) + } +} + +// This is needed when calling `i128::try_from(i: i128)` +// but it won't happen +impl From for MatrixMarketError { + fn from(_err: Infallible) -> Self { + Self::from_kind_and_message( + MatrixMarketErrorKind::TypeUnmatched, + format!("This won't happen"), + ) } } #[derive(Debug, PartialEq)] enum Sparsity { Sparse, + Dense, } #[derive(Debug, PartialEq)] enum DataType { @@ -323,19 +375,20 @@ enum StorageScheme { } #[derive(Debug, PartialEq)] struct Typecode { - sp: Sparsity, - dt: DataType, - ss: StorageScheme, + sparsity: Sparsity, + datatype: DataType, + storagescheme: StorageScheme, } impl FromStr for Sparsity { - type Err = MMError; + type Err = MatrixMarketError; /// Assumes that `word` is already lower case. fn from_str(word: &str) -> Result { match word { "coordinate" => Ok(Sparsity::Sparse), - _ => Err(MMError::from_kind_and_message( - MMErrorKind::ParsingError, + "array" => Ok(Sparsity::Dense), + _ => Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::ParsingError, format!("keyword {} is unknown", word), )), } @@ -343,7 +396,7 @@ impl FromStr for Sparsity { } impl FromStr for DataType { - type Err = MMError; + type Err = MatrixMarketError; /// Assumes that `word` is already lower case. fn from_str(word: &str) -> Result { match word { @@ -351,8 +404,8 @@ impl FromStr for DataType { "complex" => Ok(DataType::Complex), "integer" => Ok(DataType::Integer), "pattern" => Ok(DataType::Pattern), - _ => Err(MMError::from_kind_and_message( - MMErrorKind::ParsingError, + _ => Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::ParsingError, format!("keyword {} is unknown", word), )), } @@ -360,7 +413,7 @@ impl FromStr for DataType { } impl FromStr for StorageScheme { - type Err = MMError; + type Err = MatrixMarketError; /// Assumes that `word` is already lower case. fn from_str(word: &str) -> Result { match word { @@ -368,8 +421,8 @@ impl FromStr for StorageScheme { "general" => Ok(StorageScheme::General), "symmetric" => Ok(StorageScheme::Symmetric), "hermitian" => Ok(StorageScheme::Hermitian), - _ => Err(MMError::from_kind_and_message( - MMErrorKind::ParsingError, + _ => Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::ParsingError, format!("keyword {} is unknown", word), )), } @@ -379,239 +432,238 @@ impl FromStr for StorageScheme { /// Precheck if it's a valid header. /// /// For more details, please check Boisvert, Ronald F., Roldan Pozo, and Karin A. Remington. The matrix market formats: Initial design. Technical report, Applied and Computational Mathematics Division, NIST, 1996. Section 3. -fn typecode_precheck(tc: &Typecode) -> Result<(), MMError> { +fn typecode_precheck(tc: &Typecode) -> Result<(), MatrixMarketError> { match tc { Typecode { - dt: DataType::Real, - ss: StorageScheme::Hermitian, + datatype: DataType::Real, + storagescheme: StorageScheme::Hermitian, .. - } => Err(MMError::from_kind_and_message( - MMErrorKind::InvalidHeader, + } => Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::InvalidHeader, String::from("Real matrix can't be hermitian."), )), Typecode { - dt: DataType::Integer, - ss: StorageScheme::Hermitian, + datatype: DataType::Integer, + storagescheme: StorageScheme::Hermitian, .. - } => Err(MMError::from_kind_and_message( - MMErrorKind::InvalidHeader, + } => Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::InvalidHeader, String::from("Integer matrix can't be hermitian."), )), Typecode { - dt: DataType::Pattern, - ss: StorageScheme::Hermitian, + datatype: DataType::Pattern, + storagescheme: StorageScheme::Hermitian, .. - } => Err(MMError::from_kind_and_message( - MMErrorKind::InvalidHeader, + } => Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::InvalidHeader, String::from("Pattern matrix can't be hermitian."), )), Typecode { - dt: DataType::Pattern, - ss: StorageScheme::Skew, + datatype: DataType::Pattern, + storagescheme: StorageScheme::Skew, .. - } => Err(MMError::from_kind_and_message( - MMErrorKind::InvalidHeader, + } => Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::InvalidHeader, String::from("Pattern matrix can't be skew-symmetric."), )), + Typecode { + datatype: DataType::Pattern, + sparsity: Sparsity::Dense, + .. + } => Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::InvalidHeader, + String::from("Dense matrix can't be pattern matrix."), + )), // precheck success _ => Ok(()), } } /// Base trait for matrix market types. -pub trait MMType: Sized + Clone { +pub trait MatrixMarketScalar: Scalar { /// When matrix is an Integer matrix, it will convert a [i128] number to this type. - fn from_i128(i: i128) -> Result; + fn from_i128(i: i128) -> Result; /// When matrix is a Real matrix, it will convert a [f64] number to this type. - fn from_f64(f: f64) -> Result; + fn from_f64(f: f64) -> Result; /// When matrix is a Complx matrix, it will convert a [Complex] number to this type. - fn from_c64(c: Complex) -> Result; + fn from_c64(c: Complex) -> Result; /// When matrix is a Pattern matrix, it will convert a unit type [unit] to this type. - fn from_pattern(p: ()) -> Result; + fn from_pattern(p: ()) -> Result; /// When matrix is a Skew-symmetric matrix, it will convert itself to its negative. - fn negative(self) -> Result; + fn negative(self) -> Result; /// When matrix is a Hermitian matrix, it will convert itself to its conjugate. - fn conjugate(self) -> Result; + fn conjugate(self) -> Result; } -/// Implement MMType for primitive integer types. +/// Implement MatrixMarketScalar for primitive integer types. macro_rules! mm_int_impl { ($T:ty) => { - impl MMType for $T { + impl MatrixMarketScalar for $T { #[inline] - fn from_i128(i: i128) -> Result { - Ok(i as Self) + fn from_i128(i: i128) -> Result { + Ok(Self::try_from(i)?) } #[inline] - fn from_f64(_f: f64) -> Result { - Err(MMError::from_kind_and_message( - MMErrorKind::TypeUnmatched, - format!("Int type can't parse from f64"), + fn from_f64(_f: f64) -> Result { + Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::TypeUnmatched, + format!("Int type can't be parsed from f64"), )) } #[inline] - fn from_c64(_c: Complex) -> Result { - Err(MMError::from_kind_and_message( - MMErrorKind::TypeUnmatched, - format!("Int type can't parse from Complex"), + fn from_c64(_c: Complex) -> Result { + Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::TypeUnmatched, + format!("Int type can't be parsed from Complex"), )) } #[inline] - fn from_pattern(_p: ()) -> Result { - Err(MMError::from_kind_and_message( - MMErrorKind::TypeUnmatched, - format!("Int type can't parse from ()"), + fn from_pattern(_p: ()) -> Result { + Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::TypeUnmatched, + format!("Int type can't be parsed from ()"), )) } #[inline] - fn conjugate(self) -> Result { - Err(MMError::from_kind_and_message( - MMErrorKind::TypeUnmatched, + fn conjugate(self) -> Result { + Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::TypeUnmatched, format!("Int type has no conjugate"), )) } #[inline] - fn negative(self) -> Result - where - Self: Neg, - { + fn negative(self) -> Result { Ok(-self) } } }; } -/// Implement MMType for primitive float types. -macro_rules! mm_float_impl { +/// Implement MatrixMarketScalar for primitive real types. +macro_rules! mm_real_impl { ($T:ty) => { - impl MMType for $T { + impl MatrixMarketScalar for $T { #[inline] - fn from_i128(_i: i128) -> Result { - Err(MMError::from_kind_and_message( - MMErrorKind::TypeUnmatched, - format!("Float type can't parse from i128"), + fn from_i128(_i: i128) -> Result { + Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::TypeUnmatched, + format!("real type can't be parsed from i128"), )) } #[inline] - fn from_f64(f: f64) -> Result { + fn from_f64(f: f64) -> Result { Ok(f as Self) } #[inline] - fn from_c64(_c: Complex) -> Result { - Err(MMError::from_kind_and_message( - MMErrorKind::TypeUnmatched, - format!("Float type can't parse from Complex"), + fn from_c64(_c: Complex) -> Result { + Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::TypeUnmatched, + format!("real type can't be parsed from Complex"), )) } #[inline] - fn from_pattern(_p: ()) -> Result { - Err(MMError::from_kind_and_message( - MMErrorKind::TypeUnmatched, - format!("Float type can't parse from ()"), + fn from_pattern(_p: ()) -> Result { + Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::TypeUnmatched, + format!("real type can't be parsed from ()"), )) } #[inline] - fn conjugate(self) -> Result { - Err(MMError::from_kind_and_message( - MMErrorKind::TypeUnmatched, - format!("Float type has no conjugate"), + fn conjugate(self) -> Result { + Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::TypeUnmatched, + format!("real type has no conjugate"), )) } #[inline] - fn negative(self) -> Result - where - Self: Neg, - { + fn negative(self) -> Result { Ok(-self) } } }; } -/// Implement MMType for primitive complex types. +/// Implement MatrixMarketScalar for primitive complex types. macro_rules! mm_complex_impl { ($T:ty) => { - impl MMType for Complex<$T> { + impl MatrixMarketScalar for Complex<$T> { #[inline] - fn from_i128(_i: i128) -> Result { - Err(MMError::from_kind_and_message( - MMErrorKind::TypeUnmatched, - format!("Complex type can't parse from i128"), + fn from_i128(_i: i128) -> Result { + Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::TypeUnmatched, + format!("Complex type can't be parsed from i128"), )) } #[inline] - fn from_f64(_f: f64) -> Result { - Err(MMError::from_kind_and_message( - MMErrorKind::TypeUnmatched, - format!("Complex type can't parse from f64"), + fn from_f64(_f: f64) -> Result { + Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::TypeUnmatched, + format!("Complex type can't be parsed from f64"), )) } #[inline] - fn from_c64(c: Complex) -> Result { + fn from_c64(c: Complex) -> Result { Ok(Self { re: c.re as $T, im: c.im as $T, }) } #[inline] - fn from_pattern(_p: ()) -> Result { - Err(MMError::from_kind_and_message( - MMErrorKind::TypeUnmatched, - format!("Complex type can't parse from ()"), + fn from_pattern(_p: ()) -> Result { + Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::TypeUnmatched, + format!("Complex type can't be parsed from ()"), )) } #[inline] - fn conjugate(self) -> Result { + fn conjugate(self) -> Result { Ok(self.conj()) } #[inline] - fn negative(self) -> Result - where - Self: Neg, - { + fn negative(self) -> Result { Ok(-self) } } }; } -/// Implement MMType for primitive unit types. +/// Implement MatrixMarketScalar for primitive unit types. macro_rules! mm_pattern_impl { ($T:ty) => { - impl MMType for $T { + impl MatrixMarketScalar for $T { #[inline] - fn from_i128(_i: i128) -> Result { - Err(MMError::from_kind_and_message( - MMErrorKind::TypeUnmatched, - format!("Pattern type can't parse from i128"), + fn from_i128(_i: i128) -> Result { + Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::TypeUnmatched, + format!("Pattern type can't be parsed from i128"), )) } #[inline] - fn from_f64(_f: f64) -> Result { - Err(MMError::from_kind_and_message( - MMErrorKind::TypeUnmatched, - format!("Pattern type can't parse from f64"), + fn from_f64(_f: f64) -> Result { + Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::TypeUnmatched, + format!("Pattern type can't be parsed from f64"), )) } #[inline] - fn from_c64(_c: Complex) -> Result { - Err(MMError::from_kind_and_message( - MMErrorKind::TypeUnmatched, - format!("Pattern type can't parse from Complex"), + fn from_c64(_c: Complex) -> Result { + Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::TypeUnmatched, + format!("Pattern type can't be parsed from Complex"), )) } #[inline] - fn from_pattern(p: ()) -> Result { + fn from_pattern(p: ()) -> Result { Ok(p) } #[inline] - fn conjugate(self) -> Result { - Err(MMError::from_kind_and_message( - MMErrorKind::TypeUnmatched, + fn conjugate(self) -> Result { + Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::TypeUnmatched, format!("Pattern type has no conjugate"), )) } #[inline] - fn negative(self) -> Result { - Err(MMError::from_kind_and_message( - MMErrorKind::TypeUnmatched, + fn negative(self) -> Result { + Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::TypeUnmatched, format!("Pattern type has no negative"), )) } @@ -625,8 +677,8 @@ mm_int_impl!(i32); mm_int_impl!(i64); mm_int_impl!(i128); -mm_float_impl!(f32); -mm_float_impl!(f64); +mm_real_impl!(f32); +mm_real_impl!(f64); mm_complex_impl!(f32); mm_complex_impl!(f64); @@ -637,61 +689,240 @@ mm_pattern_impl!(()); #[grammar = "io/matrix_market.pest"] struct MMParser; -/// Parsing a pest structure to a Typecode of the matrix. -fn parsing_header(inner: &mut Pairs<'_, Rule>, header_type: &mut Typecode) -> Result<(), MMError> { - // once the data can be parsed, all unwrap() in this function here will not panic. - header_type.sp = inner - .next() - .unwrap() - .as_str() - .to_ascii_lowercase() - .parse::() - .unwrap(); - header_type.dt = inner - .next() - .unwrap() - .as_str() - .to_ascii_lowercase() - .parse::() - .unwrap(); - header_type.ss = inner - .next() - .unwrap() - .as_str() - .to_ascii_lowercase() - .parse::() - .unwrap(); - typecode_precheck(&header_type) +/// Parses a Matrix Market file at the given path, and returns the corresponding sparse matrix as CooMatrix format. +/// +/// Errors +/// -------- +/// +/// See [MatrixMarketErrorKind] for a list of possible error conditions. +/// +/// > _NOTE:_ Here uses strong type requirements, which means if the matrix is an integer matrix, e.g. `%%matrixmarket matrix cooridnate integer general`, then you have to load it by `load_coo_from_matrix_market_file`, where T is an integer type. Trying `load_coo_from_matrix_market_file` will give [TypeUnmatched](MatrixMarketErrorKind::TypeUnmatched) Error. After loading it, you can cast it into a `f64` matrix, by calling [cast](`nalgebra::base::Matrix::cast()`), but be aware of accuracy lose. +pub fn load_coo_from_matrix_market_file>( + path: P, +) -> Result, MatrixMarketError> +where + T: MatrixMarketScalar, +{ + let file = fs::read_to_string(path)?; + load_coo_from_matrix_market_str(&file) } -/// Parsing a pest structure to 3 int, which are number of rols, cols and non-zeros. -fn parsing_shape(inner: &mut Pairs<'_, Rule>, shape: &mut (usize, usize, usize)) { - // once the file can be parsed, all unwrap() in this function here will not panic. - shape.0 = inner.next().unwrap().as_str().parse::().unwrap(); - shape.1 = inner.next().unwrap().as_str().parse::().unwrap(); - shape.2 = inner.next().unwrap().as_str().parse::().unwrap(); -} +/// Parses a Matrix Market file described by the given string, and returns the corresponding as CooMatrix format. +/// +/// Errors +/// -------- +/// +/// See [MatrixMarketErrorKind] for a list of possible error conditions. +/// +/// > _NOTE:_ Here uses strong type requirements, which means if the matrix is an integer matrix, e.g. `%%matrixmarket matrix cooridnate integer general`, then you have to load it by `load_coo_from_matrix_market_str`, where T is an integer type. Trying `load_coo_from_matrix_market_str` will give [TypeUnmatched](MatrixMarketErrorKind::TypeUnmatched) Error. After loading it, you can cast it into a `f64` matrix, by calling [cast](`nalgebra::base::Matrix::cast()`),but be aware of accuracy lose. -#[inline] -/// Do a precheck of remaing data. Because in matrix_market.pest file, either 0, or 1 or 2 float number(s) as data is valid, however, for example, for Real matrix, only 1 float is valid. And 2 float for Complex matrix. -fn check_value_length(inner: &Pairs<'_, Rule>, l: usize) -> Result<(), MMError> { - let copy = inner.clone(); - let c = copy.count(); - if l != c { - return Err(MMError::from_kind_and_message( - MMErrorKind::ParsingError, - format!("{} data required, but {} provided.", l, c), +pub fn load_coo_from_matrix_market_str(data: &str) -> Result, MatrixMarketError> +where + T: MatrixMarketScalar, +{ + // unwrap() in this function are guaranteed by pasing the data + let file = MMParser::parse(Rule::Document, data)?.next().unwrap(); + + let mut rows: Vec = Vec::new(); + let mut cols: Vec = Vec::new(); + let mut data: Vec = Vec::new(); + let mut lines = file.into_inner(); + + let header_line = lines.next().unwrap(); + let header_type = parse_header(&mut header_line.into_inner()); + typecode_precheck(&header_type)?; + + let shape_line = lines.next().unwrap(); + // shape here is number of rows, columns, non-zeros + let shape: (usize, usize, usize); + match header_type.sparsity { + Sparsity::Sparse => { + shape = parse_sparse_shape(&mut shape_line.into_inner(), &header_type.storagescheme)?; + } + Sparsity::Dense => { + shape = parse_dense_shape(&mut shape_line.into_inner(), &header_type.storagescheme)?; + } + } + + // used when constructing dense matrix. + // If it's sparse matrix, it has no effect. + let mut current_dense_coordiante: (usize, usize) = (0, 0); + if header_type.storagescheme == StorageScheme::Skew { + // for skew dense matrix, the first element starts from (1,0) + current_dense_coordiante = (1, 0); + } + // count how many entries in the matrix data + let count = lines.clone().count(); + if count != shape.2 { + return Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::EntryNumUnmatched, + format!( + "{} entries required for the matrix, but {} was provided", + shape.2, count, + ), )); } - Ok(()) + + for data_line in lines { + let entry: (usize, usize, T); + match header_type { + Typecode { + sparsity: Sparsity::Sparse, + datatype: DataType::Real, + .. + } => { + entry = parse_sparse_real::(&mut data_line.into_inner())?; + } + Typecode { + sparsity: Sparsity::Sparse, + datatype: DataType::Integer, + .. + } => { + entry = parse_sparse_int::(&mut data_line.into_inner())?; + } + Typecode { + sparsity: Sparsity::Sparse, + datatype: DataType::Pattern, + .. + } => { + entry = parse_sparse_pattern::(&mut data_line.into_inner())?; + } + Typecode { + sparsity: Sparsity::Sparse, + datatype: DataType::Complex, + .. + } => { + entry = parse_sparse_complex::(&mut data_line.into_inner())?; + } + Typecode { + sparsity: Sparsity::Dense, + datatype: DataType::Complex, + .. + } => { + entry = ( + current_dense_coordiante.0, + current_dense_coordiante.1, + parse_dense_complex::(&mut data_line.into_inner())?, + ); + next_dense_coordiante( + &mut current_dense_coordiante, + shape, + &header_type.storagescheme, + ); + } + Typecode { + sparsity: Sparsity::Dense, + datatype: DataType::Real, + .. + } => { + entry = ( + current_dense_coordiante.0, + current_dense_coordiante.1, + parse_dense_real::(&mut data_line.into_inner())?, + ); + + next_dense_coordiante( + &mut current_dense_coordiante, + shape, + &header_type.storagescheme, + ); + } + Typecode { + sparsity: Sparsity::Dense, + datatype: DataType::Integer, + .. + } => { + entry = ( + current_dense_coordiante.0, + current_dense_coordiante.1, + parse_dense_int::(&mut data_line.into_inner())?, + ); + next_dense_coordiante( + &mut current_dense_coordiante, + shape, + &header_type.storagescheme, + ); + } + _ => { + // it shouldn't happen here, because dense matrix can't be pattern. And it will give InvalidHeader error beforehand. + entry = (1, 1, T::from_i128(1)?) + } + } + + let (r, c, d) = entry; + + match header_type.storagescheme { + StorageScheme::General => { + rows.push(r); + cols.push(c); + data.push(d); + } + StorageScheme::Symmetric => { + check_lower_triangle(r, c)?; + rows.push(r); + cols.push(c); + data.push(d.clone()); + // don't need to add twice if the element in on diagonal + if r != c { + rows.push(c); + cols.push(r); + data.push(d); + } + } + StorageScheme::Skew => { + check_lower_triangle(r, c)?; + rows.push(r); + cols.push(c); + data.push(d.clone()); + // skew-symmetric matrix shouldn't have diagonal element + if r == c { + return Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::DiagonalError, + format!( + "There is a diagonal element in skew matrix, in row(and column) {}", + r + 1 + ), + )); + } + rows.push(c); + cols.push(r); + data.push(d.negative()?); + } + StorageScheme::Hermitian => { + check_lower_triangle(r, c)?; + rows.push(r); + cols.push(c); + data.push(d.clone()); + + if r == c && d != d.clone().conjugate()? { + return Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::DiagonalError, + format!( + "There is a diagonal element in hermitian matrix, which is not a real number, in row(and column) {}", + r + 1 + ), + )); + } + // don't need to add twice if the element in on diagonal + if r != c { + rows.push(c); + cols.push(r); + data.push(d.conjugate()?); + } + } + } + } + Ok(CooMatrix::try_from_triplets( + shape.0, shape.1, rows, cols, data, + )?) } #[inline] -// do a quick check it the entry is in the lower triangle part of the matrix -fn check_lower_triangle(r: usize, c: usize) -> Result<(), MMError> { +/// do a quick check it the entry is in the lower triangle part of the matrix +fn check_lower_triangle(r: usize, c: usize) -> Result<(), MatrixMarketError> { if c > r { - return Err(MMError::from_kind_and_message( - MMErrorKind::LowerTriangleError, + return Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::NotLowerTriangle, format!( "Entry: row {} col {} should be put into lower triangle", r, c @@ -701,161 +932,362 @@ fn check_lower_triangle(r: usize, c: usize) -> Result<(), MMError> { Ok(()) } -/// Parses a Matrix Market file at the given path, and returns the corresponding sparse matrix as CooMatrix format. -pub fn load_coo_from_mm_file>(path: P) -> Result, MMError> -where - T: MMType, -{ - let file = fs::read_to_string(path)?; - load_coo_from_mm_str(&file) +#[inline] +/// Parse a pest structure to a Typecode of the matrix. +fn parse_header(inner: &mut Pairs<'_, Rule>) -> Typecode { + // unwrap() in this function are guaranteed by pasing the data + Typecode { + sparsity: inner + .next() + .unwrap() + .as_str() + .to_ascii_lowercase() + .parse::() + .unwrap(), + datatype: inner + .next() + .unwrap() + .as_str() + .to_ascii_lowercase() + .parse::() + .unwrap(), + storagescheme: inner + .next() + .unwrap() + .as_str() + .to_ascii_lowercase() + .parse::() + .unwrap(), + } } -/// Parses a Matrix Market file described by the given string, and returns the corresponding as CooMatrix format. -pub fn load_coo_from_mm_str(data: &str) -> Result, MMError> -where - T: MMType, -{ - // unwrap() here guaranteed when data can be parsed - let file = MMParser::parse(Rule::Document, data)?.next().unwrap(); - // Default typecode - let mut header_type = Typecode { - sp: Sparsity::Sparse, - dt: DataType::Real, - ss: StorageScheme::General, - }; - let mut shape = (0, 0, 0); - let mut rows: Vec = Vec::new(); - let mut cols: Vec = Vec::new(); - let mut data: Vec = Vec::new(); +// Parse shape starts here------------------------------------------------- - let mut count = 0; - for line in file.into_inner() { - match line.as_rule() { - Rule::Header => { - let mut inner = line.into_inner(); - parsing_header(&mut inner, &mut header_type)?; - } - Rule::Shape => { - let mut inner = line.into_inner(); - parsing_shape(&mut inner, &mut shape); - } - Rule::Entry => { - count += 1; - let mut inner = line.into_inner(); - // NOTE: indices are 1-based. - // unwrap() here guaranteed when data can be parsed - let r = inner.next().unwrap().as_str().parse::().unwrap(); - if r == 0 { - return Err(MMError::from_kind_and_message( - MMErrorKind::ZeroIndexed, - String::from("The data has to be one-indixed"), - )); - } - let r = r - 1; - // unwrap() here guaranteed when data can be parsed - let c = inner.next().unwrap().as_str().parse::().unwrap(); - if c == 0 { - return Err(MMError::from_kind_and_message( - MMErrorKind::ZeroIndexed, - String::from("The data has to be one-indixed"), - )); - } - let c = c - 1; - let d: T; - match header_type.dt { - DataType::Integer => { - check_value_length(&inner, 1)?; - // unwrap() here guaranteed by check_value_length - let i = inner.next().unwrap().as_str().parse::()?; - d = T::from_i128(i)?; - } - DataType::Real => { - check_value_length(&inner, 1)?; - // first unwrap() here guaranteed by check_value_length - // second unwrap() here guaranteed by parsing the data - let i = inner.next().unwrap().as_str().parse::().unwrap(); - d = T::from_f64(i)?; - } - DataType::Complex => { - check_value_length(&inner, 2)?; - // first unwrap() here guaranteed by check_value_length - // second unwrap() here guaranteed by parsing the data - let real = inner.next().unwrap().as_str().parse::().unwrap(); - // first unwrap() here guaranteed by check_value_length - // second unwrap() here guaranteed by parsing the data - let imag = inner.next().unwrap().as_str().parse::().unwrap(); - // only complex could be hermitian, and check diagonal element is a real number - if header_type.ss == StorageScheme::Hermitian && r == c && imag != 0.0 { - return Err(MMError::from_kind_and_message(MMErrorKind::DiagonalError,format!("There is a diagonal element in hermitian matrix, in row(and column) {}, but imaginary part is not zero",r))); - } - d = T::from_c64(Complex::::new(real, imag))?; - } - DataType::Pattern => { - check_value_length(&inner, 0)?; - d = T::from_pattern(())?; - } - } - - match header_type.ss { - StorageScheme::General => { - rows.push(r); - cols.push(c); - data.push(d); - } - StorageScheme::Symmetric => { - check_lower_triangle(r, c)?; - rows.push(r); - cols.push(c); - data.push(d.clone()); - // don't need to add twice if the element in on diagonal - if r != c { - rows.push(c); - cols.push(r); - data.push(d); - } - } - StorageScheme::Skew => { - check_lower_triangle(r, c)?; - rows.push(r); - cols.push(c); - data.push(d.clone()); - // skew-symmetric matrix shouldn't have diagonal element - if r == c { - return Err(MMError::from_kind_and_message(MMErrorKind::DiagonalError,format!("There is a diagonal element in skew matrix, in row(and column) {}",r))); - } - rows.push(c); - cols.push(r); - data.push(d.negative()?); - } - StorageScheme::Hermitian => { - check_lower_triangle(r, c)?; - rows.push(r); - cols.push(c); - data.push(d.clone()); - // don't need to add twice if the element in on diagonal - // diagonal element has been checked before to make sure the imaginary part is zero - if r != c { - rows.push(c); - cols.push(r); - data.push(d.conjugate()?); - } - } - } - } - _ => {} - } +/// Parse a pest structure to sparse shape information, including 3 int, which are number of rols, cols and non-zeros. +fn parse_sparse_shape( + inner: &mut Pairs<'_, Rule>, + storagescheme: &StorageScheme, +) -> Result<(usize, usize, usize), MatrixMarketError> { + // unwrap() in this function are guaranteed by pasing the data + let shape_inner = inner.next().unwrap(); + if shape_inner.as_rule() != Rule::SparseShape { + return Err(MatrixMarketError::from_kind_and_message(MatrixMarketErrorKind::ParsingError,format!(" + Shape shape line requires 3 int numbers as number of rows, columns and non-zeros, but line {} was provided here. + ",shape_inner.as_str()))); } - if count != shape.2 { - return Err(MMError::from_kind_and_message( - MMErrorKind::EntryNumUnmatched, - format!( - "expected {} entries in matrix market file, but found {}", - shape.2, count + + let mut inner = shape_inner.into_inner(); + + let r = inner.next().unwrap().as_str().parse::().unwrap(); + let c = inner.next().unwrap().as_str().parse::().unwrap(); + let nnz = inner.next().unwrap().as_str().parse::().unwrap(); + + // shape information can't use 0 as dimension + if r * c * nnz == 0 { + return Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::ZeroError, + String::from( + " + Matrix can't have 0 as shape dimensions. + ", ), )); } - Ok(CooMatrix::try_from_triplets( - shape.0, shape.1, rows, cols, data, - )?) + // check for square matirx, when it's not a general matrix + if *storagescheme != StorageScheme::General && r != c { + return Err(MatrixMarketError::from_kind_and_message(MatrixMarketErrorKind::NotSquareMatrix,format!("(Skew-)Symmetric or hermitian matrix should be square matrix, but it has dimension {} and {}",r,c))); + } + + Ok((r, c, nnz)) +} + +/// Parse a pest structure to dense shape information, including 2 int, which are number of rols, cols. +fn parse_dense_shape( + inner: &mut Pairs<'_, Rule>, + storagescheme: &StorageScheme, +) -> Result<(usize, usize, usize), MatrixMarketError> { + // unwrap() in this function are guaranteed by pasing the data + let shape_inner = inner.next().unwrap(); + if shape_inner.as_rule() != Rule::DenseShape { + return Err(MatrixMarketError::from_kind_and_message(MatrixMarketErrorKind::ParsingError,format!(" + Shape shape line requires 2 int numbers as number of rows, columns, but line {} was provided here. + ",shape_inner.as_str()))); + } + + let mut inner = shape_inner.into_inner(); + let r = inner.next().unwrap().as_str().parse::().unwrap(); + let c = inner.next().unwrap().as_str().parse::().unwrap(); + // shape information can't use 0 as dimension + if r * c == 0 { + return Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::ZeroError, + String::from( + " + Matrix can't have 0 as shape dimensions. + ", + ), + )); + } + + // check for square matirx, when it's not a general matrix + if *storagescheme != StorageScheme::General && r != c { + return Err(MatrixMarketError::from_kind_and_message(MatrixMarketErrorKind::NotSquareMatrix,format!("(Skew-)Symmetric or hermitian matrix should be square matrix, but it has dimension {} and {}",r,c))); + } + + let n: usize; + // Calculate the number of entries in the dense matrix + match storagescheme { + StorageScheme::General => { + // general matrix should contain r*c entries + n = r * c; + } + StorageScheme::Symmetric | StorageScheme::Hermitian => { + // it must be square matrix, so r==c is true here + // Symmetric or Hermitian should contain 1+2...+r = r*(r+1)/2 entries + n = r * (r + 1) / 2; + } + StorageScheme::Skew => { + // it must be square matrix, so r==c is true here + // Skew-Symmetric should contain 1+2...+r-1 = r*(r-1)/2 entries + n = r * (r - 1) / 2; + } + } + + Ok((r, c, n)) +} + +// Parse shape ends here------------------------------------------------- + +// Parse entry starts here------------------------------------------------- + +/// Parse a pest structure to sparse real entry, including 2 int, which are number of rols, cols, and a real number as data +fn parse_sparse_real(inner: &mut Pairs<'_, Rule>) -> Result<(usize, usize, T), MatrixMarketError> +where + T: MatrixMarketScalar, +{ + // unwrap() in this function are guaranteed by pasing the data + let entry_inner = inner.next().unwrap(); + if entry_inner.as_rule() != Rule::SparseReal { + return Err(MatrixMarketError::from_kind_and_message(MatrixMarketErrorKind::ParsingError,format!(" + Spare real matrix requires 2 int number as coordiantes and 1 real number as data, but line {} was provided. + ",entry_inner.as_str() ))); + } + + let mut inner = entry_inner.into_inner(); + let (r, c) = parse_sparse_coordiante(&mut inner)?; + let d = inner.next().unwrap().as_str().parse::().unwrap(); + Ok((r, c, T::from_f64(d)?)) +} + +/// Parse a pest structure to sparse integer entry, including 2 int, which are number of rols, cols, and a int number as data +fn parse_sparse_int(inner: &mut Pairs<'_, Rule>) -> Result<(usize, usize, T), MatrixMarketError> +where + T: MatrixMarketScalar, +{ + // unwrap() in this function are guaranteed by pasing the data + let entry_inner = inner.next().unwrap(); + // Because integer numbers can also be parsed as float numbers, it will be checked again in `parse::()?` + if entry_inner.as_rule() != Rule::SparseReal { + return Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::ParsingError, + format!( + " + Spare real matrix requires 3 int number as coordiantes and data, but line {} was provided. + ", + entry_inner.as_str() + ), + )); + } + let mut inner = entry_inner.into_inner(); + let (r, c) = parse_sparse_coordiante(&mut inner)?; + // Here to guarantee it is an integer number + let d = inner.next().unwrap().as_str().parse::()?; + Ok((r, c, T::from_i128(d)?)) +} + +/// Parse a pest structure to sparse pattern entry, including 2 int, which are number of rols, cols +fn parse_sparse_pattern( + inner: &mut Pairs<'_, Rule>, +) -> Result<(usize, usize, T), MatrixMarketError> +where + T: MatrixMarketScalar, +{ + // unwrap() in this function are guaranteed by pasing the data + let entry_inner = inner.next().unwrap(); + if entry_inner.as_rule() != Rule::SparsePattern { + return Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::ParsingError, + format!( + " + Spare real matrix requires 2 int number as coordiantes, but line {} was provided. + ", + entry_inner.as_str() + ), + )); + } + let mut inner = entry_inner.into_inner(); + let (r, c) = parse_sparse_coordiante(&mut inner)?; + Ok((r, c, T::from_pattern(())?)) +} + +/// Parse a pest structure to sparse complex entry, including 2 int, which are number of rols, cols, and 2 real number as complex data +fn parse_sparse_complex( + inner: &mut Pairs<'_, Rule>, +) -> Result<(usize, usize, T), MatrixMarketError> +where + T: MatrixMarketScalar, +{ + // unwrap() in this function are guaranteed by pasing the data + let entry_inner = inner.next().unwrap(); + if entry_inner.as_rule() != Rule::SparseComplex { + return Err(MatrixMarketError::from_kind_and_message(MatrixMarketErrorKind::ParsingError,format!(" + Spare real matrix requires 2 int number as coordiantes and 2 real number as complex data, but line {} was provided. + ",entry_inner.as_str() ))); + } + let mut inner = entry_inner.into_inner(); + let (r, c) = parse_sparse_coordiante(&mut inner)?; + let real = inner.next().unwrap().as_str().parse::().unwrap(); + let imag = inner.next().unwrap().as_str().parse::().unwrap(); + let complex = Complex::::new(real, imag); + Ok((r, c, T::from_c64(complex)?)) +} + +/// Parse a pest structure to dense real entry, including a real number as data +fn parse_dense_real(inner: &mut Pairs<'_, Rule>) -> Result +where + T: MatrixMarketScalar, +{ + // unwrap() in this function are guaranteed by pasing the data + let entry_inner = inner.next().unwrap(); + if entry_inner.as_rule() != Rule::DenseReal { + return Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::ParsingError, + format!( + " + Dense real matrix requires 1 real number as data, but line {} was provided. + ", + entry_inner.as_str() + ), + )); + } + let mut inner = entry_inner.into_inner(); + let d = inner.next().unwrap().as_str().parse::().unwrap(); + Ok(T::from_f64(d)?) +} + +/// Parse a pest structure to dense integer entry, including a integer number as data +fn parse_dense_int(inner: &mut Pairs<'_, Rule>) -> Result +where + T: MatrixMarketScalar, +{ + // unwrap() in this function are guaranteed by pasing the data + let entry_inner = inner.next().unwrap(); + // Because integer numbers can also be parsed as float numbers, it will be checked again in `parse::()?` + if entry_inner.as_rule() != Rule::DenseReal { + return Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::ParsingError, + format!( + " + Dense real matrix requires 1 int number as data, but line {} was provided. + ", + entry_inner.as_str() + ), + )); + } + let mut inner = entry_inner.into_inner(); + // Here to guarantee it is an integer number + let d = inner.next().unwrap().as_str().parse::()?; + Ok(T::from_i128(d)?) +} + +/// Parse a pest structure to dense complex entry, including 2 real number as complex data +fn parse_dense_complex(inner: &mut Pairs<'_, Rule>) -> Result +where + T: MatrixMarketScalar, +{ + // unwrap() in this function are guaranteed by pasing the data + let entry_inner = inner.next().unwrap(); + // Note: theoretically, 2 positive integers could also become the complex number, + // but it would be parsed as SparsePattern, because SparsePattern has higher priority. + // But DenseComplex can't have higher priority, + // because, it's more often to deal with "normal" SparsePattern, rather than "unnormal" DenseComplex + if entry_inner.as_rule() != Rule::DenseComplex && entry_inner.as_rule() != Rule::SparsePattern { + return Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::ParsingError, + format!( + " + Dense real matrix requires 2 real number as complex data, but line {} was provided. + ", + entry_inner.as_str() + ), + )); + } + let mut inner = entry_inner.into_inner(); + let real = inner.next().unwrap().as_str().parse::().unwrap(); + let imag = inner.next().unwrap().as_str().parse::().unwrap(); + let complex = Complex::::new(real, imag); + Ok(T::from_c64(complex)?) +} + +// Parse entry ends here------------------------------------------------- + +/// Parse the coordiantes information used for sparse matrix +fn parse_sparse_coordiante( + inner: &mut Pairs<'_, Rule>, +) -> Result<(usize, usize), MatrixMarketError> { + // unwrap() in this function are guaranteed by pasing the data + let r = inner.next().unwrap().as_str().parse::().unwrap(); + let c = inner.next().unwrap().as_str().parse::().unwrap(); + if r * c == 0 { + return Err(MatrixMarketError::from_kind_and_message( + MatrixMarketErrorKind::ZeroError, + String::from("The data has to be one-indixed"), + )); + } + // The coordiantes in matrix market is one-based, but in CooMatrix is zero-based. + Ok((r - 1, c - 1)) +} + +/// Calculate the next coordiantes used for dense matrix +fn next_dense_coordiante( + current_dense_coordiante: &mut (usize, usize), + shape: (usize, usize, usize), + storagescheme: &StorageScheme, +) { + // matrix market is column based format. + // so it follows the order (0,0) -> (1,0) -> ... -> (row, 0) -> (0,1) -> ... ->(row,col) + // current_dense_coordiante is (row, column) + match storagescheme { + StorageScheme::General => { + if current_dense_coordiante.0 < shape.0 - 1 { + current_dense_coordiante.0 += 1 + } else { + // jump to next column, reset row to 1, column add 1 + current_dense_coordiante.0 = 0; + current_dense_coordiante.1 += 1; + } + } + StorageScheme::Symmetric | StorageScheme::Hermitian => { + if current_dense_coordiante.0 < shape.0 - 1 { + current_dense_coordiante.0 += 1 + } else { + // jump to next column, column add 1, then set row equals to current column + // for example (0,0) -> (1,0) -> ... -> (row, 0) -> (1,1) -> ... + current_dense_coordiante.1 += 1; + current_dense_coordiante.0 = current_dense_coordiante.1; + } + } + StorageScheme::Skew => { + if current_dense_coordiante.0 < shape.0 - 1 { + current_dense_coordiante.0 += 1; + } else { + // jump to next column, set row equals to current column, then column add 1 + // skew matrix doesn't have element on diagonal + // for example (1,0) -> (2,0) -> ... -> (row, 0) -> (2,1) -> ... + current_dense_coordiante.1 += 1; + current_dense_coordiante.0 = current_dense_coordiante.1 + 1; + } + } + } } diff --git a/nalgebra-sparse/src/io/mod.rs b/nalgebra-sparse/src/io/mod.rs index 51b62873..2d74a35c 100644 --- a/nalgebra-sparse/src/io/mod.rs +++ b/nalgebra-sparse/src/io/mod.rs @@ -1,8 +1,10 @@ //! Parsers for various matrix formats. //! -//! Use mm(or MM) to represent matrix market. +//! ## Matrix Market +//! See the [website](https://math.nist.gov/MatrixMarket/formats.html) or the [paper](https://www.researchgate.net/publication/2630533_The_Matrix_Market_Exchange_Formats_Initial_Design) for more details about matrix market. pub use self::matrix_market::{ - load_coo_from_mm_file, load_coo_from_mm_str, MMError, MMErrorKind, MMType, + load_coo_from_matrix_market_file, load_coo_from_matrix_market_str, MatrixMarketError, + MatrixMarketErrorKind, MatrixMarketScalar, }; mod matrix_market; diff --git a/nalgebra-sparse/tests/unit_tests/matrix_market.rs b/nalgebra-sparse/tests/unit_tests/matrix_market.rs index 82ac6e2b..316ac02f 100644 --- a/nalgebra-sparse/tests/unit_tests/matrix_market.rs +++ b/nalgebra-sparse/tests/unit_tests/matrix_market.rs @@ -1,12 +1,12 @@ use matrixcompare::assert_matrix_eq; use nalgebra::dmatrix; use nalgebra::Complex; -use nalgebra_sparse::io::load_coo_from_mm_str; +use nalgebra_sparse::io::load_coo_from_matrix_market_str; use nalgebra_sparse::CooMatrix; #[test] #[rustfmt::skip] -fn test_mm_sparse_real_general() { +fn test_matrixmarket_sparse_real_general() { let file_str = r#" %%MatrixMarket matrix CoOrdinate real general % This is also an example of free-format features. @@ -32,16 +32,27 @@ fn test_mm_sparse_real_general() { % %================================================================================= 5 5 8 - 1 1 1 - 2 2 1.050e+01 - 3 3 1.500e-02 + 1 1 1 + + 2 2 1.050e+01 + 3 3 1.500e-02 + + + + + + + + 1 4 6.000e+00 + 4 2 2.505e+02 + 4 4 -2.800e+02 4 5 3.332e+01 5 5 1.200e+01 "#; - let sparse_mat = load_coo_from_mm_str::(file_str).unwrap(); + let sparse_mat = load_coo_from_matrix_market_str::(file_str).unwrap(); let expected = dmatrix![ 1.0, 0.0, 0.0, 6.0, 0.0; 0.0, 10.5, 0.0, 0.0, 0.0; @@ -54,22 +65,22 @@ fn test_mm_sparse_real_general() { #[test] #[rustfmt::skip] -fn test_mm_sparse_int_symmetric() { +fn test_matrixmarket_sparse_int_symmetric() { let file_str = r#" %%MatrixMarket matrix coordinate integer symmetric % 5 5 9 1 1 11 - 2 2 22 - 3 2 23 - 3 3 33 - 4 2 24 - 4 4 44 + 2 2 22 + 3 2 23 + 3 3 33 + 4 2 24 + 4 4 44 5 1 -15 5 3 35 - 5 5 55 + 5 5 55 "#; - let sparse_mat = load_coo_from_mm_str::(file_str).unwrap(); + let sparse_mat = load_coo_from_matrix_market_str::(file_str).unwrap(); let expected = dmatrix![ 11, 0, 0, 0, -15; 0, 22, 23, 24, 0; @@ -82,21 +93,21 @@ fn test_mm_sparse_int_symmetric() { #[test] #[rustfmt::skip] -fn test_mm_sparse_complex_hermitian() { +fn test_matrixmarket_sparse_complex_hermitian() { let file_str = r#" %%MatrixMarket matrix coordinate complex hermitian % 5 5 7 - 1 1 1.0 0.0 - 2 2 10.5 0.0 - 4 2 250.5 22.22 - 3 3 0.015 0.0 - 4 4 -2.8e2 0.0 - 5 5 12.0 0.0 + 1 1 1.0 0.0 + 2 2 10.5 0.0 + 4 2 250.5 22.22 + 3 3 0.015 0.0 + 4 4 -2.8e2 0.0 + 5 5 12.0 0.0 5 4 0.0 33.32 - + "#; - let sparse_mat = load_coo_from_mm_str::>(file_str).unwrap(); + let sparse_mat = load_coo_from_matrix_market_str::>(file_str).unwrap(); let expected = dmatrix![ Complex::{re:1.0,im:0.0}, Complex::{re:0.0,im:0.0}, Complex::{re:0.0,im:0.0}, Complex::{re:0.0,im:0.0},Complex::{re:0.0,im:0.0}; Complex::{re:0.0,im:0.0}, Complex::{re:10.5,im:0.0}, Complex::{re:0.0,im:0.0}, Complex::{re:250.5,im:-22.22},Complex::{re:0.0,im:0.0}; @@ -109,17 +120,17 @@ fn test_mm_sparse_complex_hermitian() { #[test] #[rustfmt::skip] -fn test_mm_sparse_real_skew() { +fn test_matrixmarket_sparse_real_skew() { let file_str = r#" %%MatrixMarket matrix coordinate real skew-symmetric % 5 5 4 - 3 2 -23.0 - 4 2 -24.0 + 3 2 -23.0 + 4 2 -24.0 5 1 -15.0 5 3 -35.0 "#; - let sparse_mat = load_coo_from_mm_str::(file_str).unwrap(); + let sparse_mat = load_coo_from_matrix_market_str::(file_str).unwrap(); let expected = dmatrix![ 0.0, 0.0, 0.0, 0.0, 15.0; 0.0, 0.0, 23.0, 24.0, 0.0; @@ -132,12 +143,12 @@ fn test_mm_sparse_real_skew() { #[test] #[rustfmt::skip] -fn test_mm_sparse_pattern_general() { +fn test_matrixmarket_sparse_pattern_general() { let file_str = r#" %%MatrixMarket matrix coordinate pattern general % 5 5 10 - 1 1 + 1 1 1 5 2 3 2 4 @@ -146,9 +157,9 @@ fn test_mm_sparse_pattern_general() { 4 1 5 2 5 4 - 5 5 + 5 5 "#; - let pattern_matrix = load_coo_from_mm_str::<()>(file_str).unwrap(); + let pattern_matrix = load_coo_from_matrix_market_str::<()>(file_str).unwrap(); let nrows = pattern_matrix.nrows(); let ncols = pattern_matrix.ncols(); let (row_idx, col_idx, val) = pattern_matrix.disassemble(); @@ -163,3 +174,136 @@ fn test_mm_sparse_pattern_general() { ]; assert_matrix_eq!(sparse_mat, expected); } + +#[test] +#[rustfmt::skip] +fn test_matrixmarket_dense_real_general() { + let file_str = r#" +%%MatrixMarket matrix array real general +% +4 3 +1.0 +2.0 +3.0 +4.0 +5.0 +6.0 +7.0 +8.0 +9.0 +10.0 +11.0 +12.0 + +"#; + let sparse_mat = load_coo_from_matrix_market_str::(file_str).unwrap(); + let expected = dmatrix![ + 1.0, 5.0, 9.0; + 2.0, 6.0, 10.0; + 3.0, 7.0, 11.0; + 4.0, 8.0, 12.0; + ]; + assert_matrix_eq!(sparse_mat, expected); +} + +#[test] +#[rustfmt::skip] +fn test_matrixmarket_dense_real_symmetric() { + let file_str = r#" +%%MatrixMarket matrix array real symmetric +% +4 4 +1.0 +2.0 +3.0 +4.0 +5.0 +6.0 +7.0 +8.0 +9.0 +10.0 + +"#; + let sparse_mat = load_coo_from_matrix_market_str::(file_str).unwrap(); + let expected = dmatrix![ + 1.0, 2.0, 3.0, 4.0; + 2.0, 5.0, 6.0, 7.0; + 3.0, 6.0, 8.0, 9.0; + 4.0, 7.0, 9.0, 10.0; + ]; + assert_matrix_eq!(sparse_mat, expected); +} + +#[test] +#[rustfmt::skip] +fn test_matrixmarket_dense_complex_hermitian() { + let file_str = r#" +%%MatrixMarket matrix array complex hermitian +% +4 4 +1.0 0.0 +2.0 2.0 +3.0 3.0 +4.0 4.0 +5.0 0.0 +6.0 6.0 +7.0 7.0 +8.0 0.0 +9.0 9.0 +10.0 0.0 + +"#; + let sparse_mat = load_coo_from_matrix_market_str::>(file_str).unwrap(); + let expected = dmatrix![ + Complex::{re:1.0,im:0.0}, Complex::{re:2.0,im:-2.0} ,Complex::{re:3.0,im:-3.0} ,Complex::{re:4.0,im:-4.0}; + Complex::{re:2.0,im:2.0}, Complex::{re:5.0,im:0.0} ,Complex::{re:6.0,im:-6.0} ,Complex::{re:7.0,im:-7.0}; + Complex::{re:3.0,im:3.0}, Complex::{re:6.0,im:6.0} ,Complex::{re:8.0,im:0.0} ,Complex::{re:9.0,im:-9.0}; + Complex::{re:4.0,im:4.0}, Complex::{re:7.0,im:7.0} ,Complex::{re:9.0,im:9.0} ,Complex::{re:10.0,im:0.0}; + ]; + assert_matrix_eq!(sparse_mat, expected); +} + +#[test] +#[rustfmt::skip] +fn test_matrixmarket_dense_int_skew() { + let file_str = r#" +%%MatrixMarket matrix array integer skew-symmetric +% +4 4 +1 +2 +3 +4 +5 +6 +"#; + let sparse_mat = load_coo_from_matrix_market_str::(file_str).unwrap(); + let expected = dmatrix![ + 0,-1,-2,-3; + 1, 0,-4,-5; + 2, 4, 0,-6; + 3, 5, 6, 0; + ]; + assert_matrix_eq!(sparse_mat, expected); +} + +#[test] +#[rustfmt::skip] +fn test_matrixmarket_dense_complex_general() { + let file_str = r#" +%%MatrixMarket matrix array complex general +% +2 2 +1 0 +1 0 +1 0 +1 0 +"#; + let sparse_mat = load_coo_from_matrix_market_str::>(file_str).unwrap(); + let expected = dmatrix![ + Complex::{re:1.0,im:0.0},Complex::{re:1.0,im:0.0}; + Complex::{re:1.0,im:0.0},Complex::{re:1.0,im:0.0}; + ]; + assert_matrix_eq!(sparse_mat, expected); +}