diff --git a/.github/workflows/nalgebra-ci-build.yml b/.github/workflows/nalgebra-ci-build.yml index fd3ec273..b553deaf 100644 --- a/.github/workflows/nalgebra-ci-build.yml +++ b/.github/workflows/nalgebra-ci-build.yml @@ -65,10 +65,10 @@ jobs: - name: test nalgebra-sparse # Manifest-path is necessary because cargo otherwise won't correctly forward features # We increase number of proptest cases to hopefully catch more potential bugs - run: PROPTEST_CASES=10000 cargo test --manifest-path=nalgebra-sparse/Cargo.toml --features compare,proptest-support + run: PROPTEST_CASES=10000 cargo test --manifest-path=nalgebra-sparse/Cargo.toml --features compare,proptest-support,io - name: test nalgebra-sparse (slow tests) # Unfortunately, the "slow-tests" take so much time that we need to run them with --release - run: PROPTEST_CASES=10000 cargo test --release --manifest-path=nalgebra-sparse/Cargo.toml --features compare,proptest-support,slow-tests slow + run: PROPTEST_CASES=10000 cargo test --release --manifest-path=nalgebra-sparse/Cargo.toml --features compare,proptest-support,io,slow-tests slow test-nalgebra-macros: runs-on: ubuntu-latest steps: diff --git a/nalgebra-sparse/Cargo.toml b/nalgebra-sparse/Cargo.toml index b85fea2c..7692984e 100644 --- a/nalgebra-sparse/Cargo.toml +++ b/nalgebra-sparse/Cargo.toml @@ -15,6 +15,8 @@ license = "Apache-2.0" [features] proptest-support = ["proptest", "nalgebra/proptest-support"] compare = [ "matrixcompare-core" ] + +# Enable matrix market I/O io = [ "pest", "pest_derive" ] # Enable to enable running some tests that take a lot of time to run diff --git a/nalgebra-sparse/src/io/matrix_market.rs b/nalgebra-sparse/src/io/matrix_market.rs index 6b008517..1429d814 100644 --- a/nalgebra-sparse/src/io/matrix_market.rs +++ b/nalgebra-sparse/src/io/matrix_market.rs @@ -4,7 +4,6 @@ use crate::coo::CooMatrix; use crate::SparseFormatError; use crate::SparseFormatErrorKind; -use nalgebra::base::Scalar; use nalgebra::Complex; use pest::iterators::Pairs; use pest::Parser; @@ -27,11 +26,12 @@ pub struct MatrixMarketError { } /// Errors produced by functions that expect well-formed matrix market format data. -/// > _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 MatrixMarketErrorKind { - /// Indicates that some word is not known to MM format + /// Parsing failure. + /// + /// Indicates that the parser failed, for example due to an unexpected string. /// /// Examples /// -------- @@ -39,17 +39,17 @@ pub enum MatrixMarketErrorKind { /// # use nalgebra_sparse::io::load_coo_from_matrix_market_str; /// # use nalgebra_sparse::io::MatrixMarketErrorKind; /// let str = r#" - /// %%MatrixMarket whatever whatever whatever whatever + /// %%MatrixMarket invalid invalid invalid invalid /// 1 1 1 /// 1 1 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::ParsingError); + /// let matrix_result = load_coo_from_matrix_market_str::(str); + /// assert_eq!(matrix_result.is_err(), true); + /// assert_eq!(matrix_result.unwrap_err().kind(), MatrixMarketErrorKind::ParsingError); /// ``` ParsingError, - /// Indicates that header is not valid + /// Indicates that the matrix market header is invalid. /// /// Examples /// -------- @@ -62,13 +62,13 @@ pub enum MatrixMarketErrorKind { /// 1 1 1 /// 1 1 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::InvalidHeader); + /// let matrix_result = load_coo_from_matrix_market_str::(str); + /// assert_eq!(matrix_result.is_err(), true); + /// assert_eq!(matrix_result.unwrap_err().kind(),MatrixMarketErrorKind::InvalidHeader); /// ``` InvalidHeader, - /// Indicates that the data entries in .mtx file are more or less than entries specified in .mtx file + /// Indicates that the number of data entries in the matrix market file does not match the header. /// /// Examples /// -------- @@ -82,13 +82,14 @@ pub enum MatrixMarketErrorKind { /// 2 2 2 /// 2 3 2 /// "#; - /// 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::EntryNumUnmatched); + /// let matrix_result = load_coo_from_matrix_market_str::(str); + /// assert_eq!(matrix_result.is_err(), true); + /// assert_eq!(matrix_result.unwrap_err().kind(),MatrixMarketErrorKind::EntryMismatch); /// ``` - EntryNumUnmatched, + EntryMismatch, - /// Indicates that the type T is not matched with the function it called. + /// Indicates that the scalar type requested is not compatible with the scalar type stored + /// in the matrix market file. /// /// Examples /// -------- @@ -97,18 +98,20 @@ pub enum MatrixMarketErrorKind { /// # use nalgebra_sparse::io::MatrixMarketErrorKind; /// let str = r#" /// %%matrixmarket matrix coordinate real general - /// % it should be called by load_coo_from_matrix_market_str::(str), or f32; + /// % it should be loaded with 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_matrix_market_str::(str); - /// assert_eq!(matrix_error.is_err(), true); - /// assert_eq!(matrix_error.unwrap_err().kind(),MatrixMarketErrorKind::TypeUnmatched); + /// let matrix_result = load_coo_from_matrix_market_str::(str); + /// assert_eq!(matrix_result.is_err(), true); + /// assert_eq!(matrix_result.unwrap_err().kind(),MatrixMarketErrorKind::TypeMismatch); /// ``` - TypeUnmatched, + TypeMismatch, - /// Indicates that zero has been used as an index in the data, or the shape of the matrix, which is not allowed. + /// Indicates that zero has been used as an index in the data. + /// + /// **Note**: The matrix market format uses 1-based indexing. /// /// Examples /// -------- @@ -120,13 +123,14 @@ pub enum MatrixMarketErrorKind { /// 1 1 1 /// 0 0 10 /// "#; - /// 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::ZeroError); + /// let matrix_result = load_coo_from_matrix_market_str::(str); + /// assert_eq!(matrix_result.is_err(), true); + /// assert_eq!(matrix_result.unwrap_err().kind(),MatrixMarketErrorKind::ZeroError); /// ``` ZeroError, - /// Indicates [SparseFormatError], while creating the sparse matrix. + /// Indicates [SparseFormatError] while creating the sparse matrix. + /// /// /// Examples /// -------- @@ -139,13 +143,14 @@ pub enum MatrixMarketErrorKind { /// 1 1 1 /// 4 2 10 /// "#; - /// 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::SparseFormatError(SparseFormatErrorKind::IndexOutOfBounds)); + /// let matrix_result = load_coo_from_matrix_market_str::(str); + /// assert_eq!(matrix_result.is_err(), true); + /// assert_eq!(matrix_result.unwrap_err().kind(), + /// MatrixMarketErrorKind::SparseFormatError(SparseFormatErrorKind::IndexOutOfBounds)); /// ``` SparseFormatError(SparseFormatErrorKind), - /// Indicates that a wrong diagonal element has been provieded to the matrix + /// Indicates that a wrong diagonal element has been provided to the matrix. /// /// Examples /// -------- @@ -160,9 +165,9 @@ pub enum MatrixMarketErrorKind { /// 1 1 10 /// 2 1 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::DiagonalError); + /// let matrix_result = load_coo_from_matrix_market_str::(str); + /// assert_eq!(matrix_result.is_err(), true); + /// assert_eq!(matrix_result.unwrap_err().kind(),MatrixMarketErrorKind::DiagonalError); /// /// let str = r#" /// %%matrixmarket matrix coordinate complex hermitian @@ -171,28 +176,27 @@ pub enum MatrixMarketErrorKind { /// 1 1 10 2 /// 2 1 5 2 /// "#; - /// 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::DiagonalError); + /// let matrix_result = load_coo_from_matrix_market_str::>(str); + /// assert_eq!(matrix_result.is_err(), true); + /// assert_eq!(matrix_result.unwrap_err().kind(),MatrixMarketErrorKind::DiagonalError); /// ``` - /// Here the skew matrix shouldn't have an element on the diagonal + /// Here the skew matrix shouldn't have an element on the diagonal. DiagonalError, - /// Indicates [io error](`std::io::Error`), while reading the data from file. + /// Indicates an [IO error](`std::io::Error`) while reading the data from file. /// /// Examples /// -------- /// ```rust /// # 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_matrix_market_file::(file_name); - /// assert_eq!(matrix_error.is_err(), true); - /// assert_eq!(matrix_error.unwrap_err().kind(),MatrixMarketErrorKind::IOError(std::io::ErrorKind::NotFound)); + /// let matrix_result = load_coo_from_matrix_market_file::("matrix.mtx"); + /// assert_eq!(matrix_result.is_err(), true); + /// assert_eq!(matrix_result.unwrap_err().kind(),MatrixMarketErrorKind::IOError(std::io::ErrorKind::NotFound)); /// ``` IOError(std::io::ErrorKind), - /// Indicates (skew-)symmetric (or hermitian) matrix is not lower triangle matrix. + /// Indicates that a (skew-)symmetric (or hermitian) matrix is not a lower triangular matrix. /// /// Examples /// -------- @@ -205,13 +209,13 @@ pub enum MatrixMarketErrorKind { /// 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::NotLowerTriangle); + /// let matrix_result = load_coo_from_matrix_market_str::(str); + /// assert_eq!(matrix_result.is_err(), true); + /// assert_eq!(matrix_result.unwrap_err().kind(),MatrixMarketErrorKind::NotLowerTriangle); /// ``` NotLowerTriangle, - /// Indicates (skew-)symmetric (or hermitian) matrix is not square matrix. + /// Indicates that a (skew-)symmetric (or hermitian) matrix is not a square matrix. /// /// Examples /// -------- @@ -224,11 +228,11 @@ pub enum MatrixMarketErrorKind { /// 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); + /// let matrix_result = load_coo_from_matrix_market_str::(str); + /// assert_eq!(matrix_result.is_err(), true); + /// assert_eq!(matrix_result.unwrap_err().kind(),MatrixMarketErrorKind::NonSquare); /// ``` - NotSquareMatrix, + NonSquare, } impl MatrixMarketError { @@ -239,7 +243,7 @@ impl MatrixMarketError { } } - /// The operation error kind. + /// The matrix market error kind. #[must_use] pub fn kind(&self) -> MatrixMarketErrorKind { self.error_kind @@ -254,7 +258,7 @@ impl MatrixMarketError { impl fmt::Display for MatrixMarketError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "Matrix Market load error: ")?; + write!(f, "Matrix Market error: ")?; match self.kind() { MatrixMarketErrorKind::ParsingError => { write!(f, "ParsingError,")?; @@ -262,11 +266,11 @@ impl fmt::Display for MatrixMarketError { MatrixMarketErrorKind::InvalidHeader => { write!(f, "InvalidHeader,")?; } - MatrixMarketErrorKind::EntryNumUnmatched => { + MatrixMarketErrorKind::EntryMismatch => { write!(f, "EntryNumUnmatched,")?; } - MatrixMarketErrorKind::TypeUnmatched => { - write!(f, "TypeUnmatched,")?; + MatrixMarketErrorKind::TypeMismatch => { + write!(f, "TypeMismatch,")?; } MatrixMarketErrorKind::SparseFormatError(_) => { write!(f, "SparseFormatError,")?; @@ -283,23 +287,24 @@ impl fmt::Display for MatrixMarketError { MatrixMarketErrorKind::NotLowerTriangle => { write!(f, "NotLowerTriangle,")?; } - MatrixMarketErrorKind::NotSquareMatrix => { + MatrixMarketErrorKind::NonSquare => { write!(f, "NotSquareMatrix,")?; } } - write!(f, " Message: {}", self.message) + write!(f, " message: {}", self.message) } } impl std::error::Error for MatrixMarketError {} -impl From> - for MatrixMarketError -{ - fn from(err: pest::error::Error) -> Self { +impl MatrixMarketError { + fn from_pest_error(error: pest::error::Error) -> Self + where + T: fmt::Debug + std::hash::Hash + std::marker::Copy + Ord + { Self::from_kind_and_message( MatrixMarketErrorKind::ParsingError, - format!("Can't parse the data.\n Error: {}", err), + format!("Can't parse the data.\n Error: {}", error), ) } } @@ -334,9 +339,9 @@ impl From for MatrixMarketError { impl From for MatrixMarketError { fn from(err: TryFromIntError) -> Self { Self::from_kind_and_message( - MatrixMarketErrorKind::TypeUnmatched, + MatrixMarketErrorKind::TypeMismatch, format!( - "Please consider using a larger integery type. Error message: {}", + "Please consider using a larger integer type. Error message: {}", &err ), ) @@ -348,7 +353,7 @@ impl From for MatrixMarketError { impl From for MatrixMarketError { fn from(_err: Infallible) -> Self { Self::from_kind_and_message( - MatrixMarketErrorKind::TypeUnmatched, + MatrixMarketErrorKind::TypeMismatch, format!("This won't happen"), ) } @@ -431,7 +436,11 @@ 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. +/// 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<(), MatrixMarketError> { match tc { Typecode { @@ -479,25 +488,39 @@ fn typecode_precheck(tc: &Typecode) -> Result<(), MatrixMarketError> { } } -/// Base trait for matrix market types. -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; - /// When matrix is a Real matrix, it will convert a [f64] number to this type. - 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; - /// When matrix is a Pattern matrix, it will convert a unit type [unit] to this type. - fn from_pattern(p: ()) -> Result; - /// When matrix is a Skew-symmetric matrix, it will convert itself to its negative. - fn negative(self) -> Result; - /// When matrix is a Hermitian matrix, it will convert itself to its conjugate. - fn conjugate(self) -> Result; +/// Scalar types supported by the matrix market parser. +mod internal { + use crate::io::MatrixMarketError; + use na::{Complex, Scalar}; + + pub trait SupportedMatrixMarketScalar: Scalar { + /// When the matrix is an integer matrix, it will convert a [i128] number to this type. + 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; + /// When matrix is a Complex matrix, it will convert a [Complex] number to this type. + 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; + /// When matrix is a Skew-symmetric matrix, it will convert itself to its negative. + fn negative(self) -> Result; + /// When matrix is a Hermitian matrix, it will convert itself to its conjugate. + fn conjugate(self) -> Result; + } } + +/// A marker trait for supported matrix market scalars. +/// +/// This is a sealed trait; it cannot be implemented by external crates. This is done in order to prevent leaking +/// some of the implementation details we currently rely on. We may relax this restriction in the future. +pub trait MatrixMarketScalar: internal::SupportedMatrixMarketScalar {} + /// Implement MatrixMarketScalar for primitive integer types. macro_rules! mm_int_impl { ($T:ty) => { - impl MatrixMarketScalar for $T { + impl MatrixMarketScalar for $T {} + + impl internal::SupportedMatrixMarketScalar for $T { #[inline] fn from_i128(i: i128) -> Result { Ok(Self::try_from(i)?) @@ -505,28 +528,28 @@ macro_rules! mm_int_impl { #[inline] fn from_f64(_f: f64) -> Result { Err(MatrixMarketError::from_kind_and_message( - MatrixMarketErrorKind::TypeUnmatched, + MatrixMarketErrorKind::TypeMismatch, format!("Int type can't be parsed from f64"), )) } #[inline] fn from_c64(_c: Complex) -> Result { Err(MatrixMarketError::from_kind_and_message( - MatrixMarketErrorKind::TypeUnmatched, + MatrixMarketErrorKind::TypeMismatch, format!("Int type can't be parsed from Complex"), )) } #[inline] fn from_pattern(_p: ()) -> Result { Err(MatrixMarketError::from_kind_and_message( - MatrixMarketErrorKind::TypeUnmatched, + MatrixMarketErrorKind::TypeMismatch, format!("Int type can't be parsed from ()"), )) } #[inline] fn conjugate(self) -> Result { Err(MatrixMarketError::from_kind_and_message( - MatrixMarketErrorKind::TypeUnmatched, + MatrixMarketErrorKind::TypeMismatch, format!("Int type has no conjugate"), )) } @@ -540,11 +563,13 @@ macro_rules! mm_int_impl { /// Implement MatrixMarketScalar for primitive real types. macro_rules! mm_real_impl { ($T:ty) => { - impl MatrixMarketScalar for $T { + impl MatrixMarketScalar for $T {} + + impl internal::SupportedMatrixMarketScalar for $T { #[inline] fn from_i128(_i: i128) -> Result { Err(MatrixMarketError::from_kind_and_message( - MatrixMarketErrorKind::TypeUnmatched, + MatrixMarketErrorKind::TypeMismatch, format!("real type can't be parsed from i128"), )) } @@ -555,21 +580,21 @@ macro_rules! mm_real_impl { #[inline] fn from_c64(_c: Complex) -> Result { Err(MatrixMarketError::from_kind_and_message( - MatrixMarketErrorKind::TypeUnmatched, + MatrixMarketErrorKind::TypeMismatch, format!("real type can't be parsed from Complex"), )) } #[inline] fn from_pattern(_p: ()) -> Result { Err(MatrixMarketError::from_kind_and_message( - MatrixMarketErrorKind::TypeUnmatched, + MatrixMarketErrorKind::TypeMismatch, format!("real type can't be parsed from ()"), )) } #[inline] fn conjugate(self) -> Result { Err(MatrixMarketError::from_kind_and_message( - MatrixMarketErrorKind::TypeUnmatched, + MatrixMarketErrorKind::TypeMismatch, format!("real type has no conjugate"), )) } @@ -580,21 +605,24 @@ macro_rules! mm_real_impl { } }; } + /// Implement MatrixMarketScalar for primitive complex types. macro_rules! mm_complex_impl { ($T:ty) => { - impl MatrixMarketScalar for Complex<$T> { + impl MatrixMarketScalar for Complex<$T> {} + + impl internal::SupportedMatrixMarketScalar for Complex<$T> { #[inline] fn from_i128(_i: i128) -> Result { Err(MatrixMarketError::from_kind_and_message( - MatrixMarketErrorKind::TypeUnmatched, + MatrixMarketErrorKind::TypeMismatch, format!("Complex type can't be parsed from i128"), )) } #[inline] fn from_f64(_f: f64) -> Result { Err(MatrixMarketError::from_kind_and_message( - MatrixMarketErrorKind::TypeUnmatched, + MatrixMarketErrorKind::TypeMismatch, format!("Complex type can't be parsed from f64"), )) } @@ -608,7 +636,7 @@ macro_rules! mm_complex_impl { #[inline] fn from_pattern(_p: ()) -> Result { Err(MatrixMarketError::from_kind_and_message( - MatrixMarketErrorKind::TypeUnmatched, + MatrixMarketErrorKind::TypeMismatch, format!("Complex type can't be parsed from ()"), )) } @@ -626,25 +654,27 @@ macro_rules! mm_complex_impl { /// Implement MatrixMarketScalar for primitive unit types. macro_rules! mm_pattern_impl { ($T:ty) => { - impl MatrixMarketScalar for $T { + impl MatrixMarketScalar for $T {} + + impl internal::SupportedMatrixMarketScalar for $T { #[inline] fn from_i128(_i: i128) -> Result { Err(MatrixMarketError::from_kind_and_message( - MatrixMarketErrorKind::TypeUnmatched, + MatrixMarketErrorKind::TypeMismatch, format!("Pattern type can't be parsed from i128"), )) } #[inline] fn from_f64(_f: f64) -> Result { Err(MatrixMarketError::from_kind_and_message( - MatrixMarketErrorKind::TypeUnmatched, + MatrixMarketErrorKind::TypeMismatch, format!("Pattern type can't be parsed from f64"), )) } #[inline] fn from_c64(_c: Complex) -> Result { Err(MatrixMarketError::from_kind_and_message( - MatrixMarketErrorKind::TypeUnmatched, + MatrixMarketErrorKind::TypeMismatch, format!("Pattern type can't be parsed from Complex"), )) } @@ -656,14 +686,14 @@ macro_rules! mm_pattern_impl { #[inline] fn conjugate(self) -> Result { Err(MatrixMarketError::from_kind_and_message( - MatrixMarketErrorKind::TypeUnmatched, + MatrixMarketErrorKind::TypeMismatch, format!("Pattern type has no conjugate"), )) } #[inline] fn negative(self) -> Result { Err(MatrixMarketError::from_kind_and_message( - MatrixMarketErrorKind::TypeUnmatched, + MatrixMarketErrorKind::TypeMismatch, format!("Pattern type has no negative"), )) } @@ -687,16 +717,31 @@ mm_pattern_impl!(()); #[derive(Parser)] #[grammar = "io/matrix_market.pest"] -struct MMParser; +struct MatrixMarketParser; -/// Parses a Matrix Market file at the given path, and returns the corresponding sparse matrix as CooMatrix format. +/// Parses a Matrix Market file at the given path as a `CooMatrix`. +/// +/// The matrix market format specification does not clarify whether duplicate entries are allowed. Our importer +/// assumes that this is permitted and produces a `CooMatrix` with possibly duplicate entries. +/// +/// **Note**: A current restriction of the importer is that you must use a compatible scalar type when importing. +/// For example, in order to import a matrix stored as `integer` in the matrix market format, you must +/// import it as an integer matrix, otherwise a [TypeMismatch](MatrixMarketErrorKind::TypeMismatch) error +/// will be returned. This restriction may be lifted in the future, and is +/// tracked by issue [#1038](https://github.com/dimforge/nalgebra/issues/1038). /// /// 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. +/// Examples +/// -------- +/// ``` +/// use nalgebra_sparse::io::load_coo_from_matrix_market_file; +/// // Use e.g. `f64` for floating-point matrices +/// let matrix = load_coo_from_matrix_market_file::("path/to/matrix.mtx")?; +/// ``` pub fn load_coo_from_matrix_market_file>( path: P, ) -> Result, MatrixMarketError> @@ -707,21 +752,36 @@ where load_coo_from_matrix_market_str(&file) } -/// Parses a Matrix Market file described by the given string, and returns the corresponding as CooMatrix format. +/// Parses a Matrix Market file described by the given string as a `CooMatrix`. +/// +/// See [load_coo_from_matrix_market_file] for more information. /// /// 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. - +/// Examples +/// -------- +/// ``` +/// # 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 = load_coo_from_matrix_market_str::(str)?; +/// ``` pub fn load_coo_from_matrix_market_str(data: &str) -> Result, MatrixMarketError> where T: MatrixMarketScalar, { // unwrap() in this function are guaranteed by parsing the data - let file = MMParser::parse(Rule::Document, data)?.next().unwrap(); + let file = MatrixMarketParser::parse(Rule::Document, data) + .map_err(MatrixMarketError::from_pest_error)? + .next().unwrap(); let mut rows: Vec = Vec::new(); let mut cols: Vec = Vec::new(); @@ -746,16 +806,16 @@ where // used when constructing dense matrix. // If it's sparse matrix, it has no effect. - let mut current_dense_coordiante: (usize, usize) = (0, 0); + let mut current_dense_coordinate: (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); + current_dense_coordinate = (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, + MatrixMarketErrorKind::EntryMismatch, format!( "{} entries required for the matrix, but {} was provided", shape.2, count, @@ -800,12 +860,12 @@ where .. } => { entry = ( - current_dense_coordiante.0, - current_dense_coordiante.1, + current_dense_coordinate.0, + current_dense_coordinate.1, parse_dense_complex::(&mut data_line.into_inner())?, ); - next_dense_coordiante( - &mut current_dense_coordiante, + next_dense_coordinate( + &mut current_dense_coordinate, shape, &header_type.storagescheme, ); @@ -816,13 +876,13 @@ where .. } => { entry = ( - current_dense_coordiante.0, - current_dense_coordiante.1, + current_dense_coordinate.0, + current_dense_coordinate.1, parse_dense_real::(&mut data_line.into_inner())?, ); - next_dense_coordiante( - &mut current_dense_coordiante, + next_dense_coordinate( + &mut current_dense_coordinate, shape, &header_type.storagescheme, ); @@ -833,12 +893,12 @@ where .. } => { entry = ( - current_dense_coordiante.0, - current_dense_coordiante.1, + current_dense_coordinate.0, + current_dense_coordinate.1, parse_dense_int::(&mut data_line.into_inner())?, ); - next_dense_coordiante( - &mut current_dense_coordiante, + next_dense_coordinate( + &mut current_dense_coordinate, shape, &header_type.storagescheme, ); @@ -963,7 +1023,7 @@ fn parse_header(inner: &mut Pairs<'_, Rule>) -> Typecode { // Parse shape starts here------------------------------------------------- -/// Parse a pest structure to sparse shape information, including 3 int, which are number of rols, cols and non-zeros. +/// Parse a pest structure to sparse shape information, including 3 int, which are number of rows, cols and non-zeros. fn parse_sparse_shape( inner: &mut Pairs<'_, Rule>, storagescheme: &StorageScheme, @@ -994,15 +1054,15 @@ fn parse_sparse_shape( )); } - // check for square matirx, when it's not a general matrix + // check for square matrix, 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))); + return Err(MatrixMarketError::from_kind_and_message(MatrixMarketErrorKind::NonSquare, 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. +/// Parse a pest structure to dense shape information, including 2 int, which are number of rows, cols. fn parse_dense_shape( inner: &mut Pairs<'_, Rule>, storagescheme: &StorageScheme, @@ -1030,9 +1090,9 @@ fn parse_dense_shape( )); } - // check for square matirx, when it's not a general matrix + // check for square matrix, 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))); + return Err(MatrixMarketError::from_kind_and_message(MatrixMarketErrorKind::NonSquare, format!("(Skew-)Symmetric or hermitian matrix should be square matrix, but it has dimension {} and {}", r, c))); } let n: usize; @@ -1061,7 +1121,7 @@ fn parse_dense_shape( // 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 +/// Parse a pest structure to sparse real entry, including 2 int, which are number of rows, cols, and a real number as data fn parse_sparse_real(inner: &mut Pairs<'_, Rule>) -> Result<(usize, usize, T), MatrixMarketError> where T: MatrixMarketScalar, @@ -1070,17 +1130,17 @@ where 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. + Spare real matrix requires 2 int number as coordinates 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 (r, c) = parse_sparse_coordinate(&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 +/// Parse a pest structure to sparse integer entry, including 2 int, which are number of rows, cols, and a int number as data fn parse_sparse_int(inner: &mut Pairs<'_, Rule>) -> Result<(usize, usize, T), MatrixMarketError> where T: MatrixMarketScalar, @@ -1093,20 +1153,20 @@ where MatrixMarketErrorKind::ParsingError, format!( " - Spare real matrix requires 3 int number as coordiantes and data, but line {} was provided. + Spare real matrix requires 3 int number as coordinates 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)?; + let (r, c) = parse_sparse_coordinate(&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 +/// Parse a pest structure to sparse pattern entry, including 2 int, which are number of rows, cols fn parse_sparse_pattern( inner: &mut Pairs<'_, Rule>, ) -> Result<(usize, usize, T), MatrixMarketError> @@ -1120,18 +1180,18 @@ where MatrixMarketErrorKind::ParsingError, format!( " - Spare real matrix requires 2 int number as coordiantes, but line {} was provided. + Spare real matrix requires 2 int number as coordinates, but line {} was provided. ", entry_inner.as_str() ), )); } let mut inner = entry_inner.into_inner(); - let (r, c) = parse_sparse_coordiante(&mut inner)?; + let (r, c) = parse_sparse_coordinate(&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 +/// Parse a pest structure to sparse complex entry, including 2 int, which are number of rows, cols, and 2 real number as complex data fn parse_sparse_complex( inner: &mut Pairs<'_, Rule>, ) -> Result<(usize, usize, T), MatrixMarketError> @@ -1142,11 +1202,11 @@ where 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. + Spare real matrix requires 2 int number as coordinates 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 (r, c) = parse_sparse_coordinate(&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); @@ -1232,8 +1292,8 @@ where // Parse entry ends here------------------------------------------------- -/// Parse the coordiantes information used for sparse matrix -fn parse_sparse_coordiante( +/// Parse the coordinates information used for sparse matrix +fn parse_sparse_coordinate( inner: &mut Pairs<'_, Rule>, ) -> Result<(usize, usize), MatrixMarketError> { // unwrap() in this function are guaranteed by parsing the data @@ -1242,51 +1302,51 @@ fn parse_sparse_coordiante( if r * c == 0 { return Err(MatrixMarketError::from_kind_and_message( MatrixMarketErrorKind::ZeroError, - String::from("The data has to be one-indixed"), + String::from("The data has to be one-indexed"), )); } - // The coordiantes in matrix market is one-based, but in CooMatrix is zero-based. + // The coordinates 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), +/// Calculate the next coordinates used for dense matrix +fn next_dense_coordinate( + current_dense_coordinate: &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) + // current_dense_coordinate is (row, column) match storagescheme { StorageScheme::General => { - if current_dense_coordiante.0 < shape.0 - 1 { - current_dense_coordiante.0 += 1 + if current_dense_coordinate.0 < shape.0 - 1 { + current_dense_coordinate.0 += 1 } else { // jump to next column, reset row to 1, column add 1 - current_dense_coordiante.0 = 0; - current_dense_coordiante.1 += 1; + current_dense_coordinate.0 = 0; + current_dense_coordinate.1 += 1; } } StorageScheme::Symmetric | StorageScheme::Hermitian => { - if current_dense_coordiante.0 < shape.0 - 1 { - current_dense_coordiante.0 += 1 + if current_dense_coordinate.0 < shape.0 - 1 { + current_dense_coordinate.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; + current_dense_coordinate.1 += 1; + current_dense_coordinate.0 = current_dense_coordinate.1; } } StorageScheme::Skew => { - if current_dense_coordiante.0 < shape.0 - 1 { - current_dense_coordiante.0 += 1; + if current_dense_coordinate.0 < shape.0 - 1 { + current_dense_coordinate.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; + current_dense_coordinate.1 += 1; + current_dense_coordinate.0 = current_dense_coordinate.1 + 1; } } } diff --git a/nalgebra-sparse/src/io/mod.rs b/nalgebra-sparse/src/io/mod.rs index 2d74a35c..89b21ffb 100644 --- a/nalgebra-sparse/src/io/mod.rs +++ b/nalgebra-sparse/src/io/mod.rs @@ -1,7 +1,35 @@ -//! Parsers for various matrix formats. +//! Functionality for importing and exporting sparse matrices to and from files. //! -//! ## 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. +//! **Available only when the `io` feature is enabled.** +//! +//! The following formats are currently supported: +//! +//! | Format | Import | Export | +//! | ------------------------------------------------|------------|------------| +//! | [Matrix market](#matrix-market-format) | Yes | No | +//! +//! [Matrix market]: https://math.nist.gov/MatrixMarket/formats.html +//! +//! ## Matrix Market format +//! +//! The Matrix Market format is a simple ASCII-based file format for sparse matrices, and was initially developed for +//! the [NIST Matrix Market](https://math.nist.gov/MatrixMarket/), a repository of example sparse matrices. +//! In later years it has largely been superseded by the +//! [SuiteSparse Matrix Collection](https://sparse.tamu.edu/) (formerly University of Florida Sparse Matrix Collection), +//! which also uses the Matrix Market file format. +//! +//! We currently offer functionality for importing a Matrix market file to an instance of a +//! [CooMatrix](crate::CooMatrix) through the function [load_coo_from_matrix_market_file]. It is also possible to load +//! a matrix stored in the matrix market format with the function [load_coo_from_matrix_market_str]. +//! +//! Export is currently not implemented, but [planned](https://github.com/dimforge/nalgebra/issues/1037). +//! +//! Our implementation is based on the [format description](https://math.nist.gov/MatrixMarket/formats.html) +//! on the Matrix Market website and the +//! [following NIST whitepaper](https://math.nist.gov/MatrixMarket/reports/MMformat.ps): +//! +//! > Boisvert, Ronald F., Roldan Pozo, and Karin A. Remington.
+//! > "*The Matrix Market Exchange Formats: Initial Design.*" (1996). pub use self::matrix_market::{ load_coo_from_matrix_market_file, load_coo_from_matrix_market_str, MatrixMarketError, diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index a588787f..edbf83bd 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -19,6 +19,7 @@ //! - Sparsity patterns in CSR and CSC matrices are explicitly represented by the //! [SparsityPattern](pattern::SparsityPattern) type, which encodes the invariants of the //! associated index data structures. +//! - [Matrix market format support](`io`) when the `io` feature is enabled. //! - [proptest strategies](`proptest`) for sparse matrices when the feature //! `proptest-support` is enabled. //! - [matrixcompare support](https://crates.io/crates/matrixcompare) for effortless diff --git a/nalgebra-sparse/tests/unit.rs b/nalgebra-sparse/tests/unit.rs index 73a95cd7..1e61467c 100644 --- a/nalgebra-sparse/tests/unit.rs +++ b/nalgebra-sparse/tests/unit.rs @@ -1,6 +1,13 @@ //! Unit tests -#[cfg(any(not(feature = "proptest-support"), not(feature = "compare")))] -compile_error!("Tests must be run with features `proptest-support` and `compare`"); +#[cfg(not(all( +feature = "proptest-support", +feature = "compare", +feature = "io", +)))] +compile_error!( + "Please enable the `proptest-support`, `compare` and `io` features in order to compile and run the tests. + Example: `cargo test -p nalgebra-sparse --features proptest-support,compare,io`" +); mod unit_tests; diff --git a/nalgebra-sparse/tests/unit_tests/matrix_market.rs b/nalgebra-sparse/tests/unit_tests/matrix_market.rs index 316ac02f..f0a9d217 100644 --- a/nalgebra-sparse/tests/unit_tests/matrix_market.rs +++ b/nalgebra-sparse/tests/unit_tests/matrix_market.rs @@ -4,6 +4,24 @@ use nalgebra::Complex; use nalgebra_sparse::io::load_coo_from_matrix_market_str; use nalgebra_sparse::CooMatrix; +#[test] +#[rustfmt::skip] +fn test_matrixmarket_sparse_real_general_empty() { + // Test several valid zero-shapes of a matrix + let shapes = vec![ (0, 0), (1, 0), (0, 1) ]; + let strings: Vec = shapes + .into_iter() + .map(|(m, n)| format!("%%MatrixMarket matrix coordinate real general\n {} {} 0", m, n)) + .collect(); + + for string in &strings { + let sparse_mat = load_coo_from_matrix_market_str::(string).unwrap(); + assert_eq!(sparse_mat.nrows(), 0); + assert_eq!(sparse_mat.ncols(), 0); + assert_eq!(sparse_mat.nnz(), 0); + } +} + #[test] #[rustfmt::skip] fn test_matrixmarket_sparse_real_general() { diff --git a/nalgebra-sparse/tests/unit_tests/mod.rs b/nalgebra-sparse/tests/unit_tests/mod.rs index e97731e1..7090a493 100644 --- a/nalgebra-sparse/tests/unit_tests/mod.rs +++ b/nalgebra-sparse/tests/unit_tests/mod.rs @@ -3,7 +3,6 @@ mod convert_serial; mod coo; mod csc; mod csr; -#[cfg(feature = "io")] mod matrix_market; mod ops; mod pattern;