diff --git a/.github/workflows/nalgebra-ci-build.yml b/.github/workflows/nalgebra-ci-build.yml index 3a56df13..b9df85da 100644 --- a/.github/workflows/nalgebra-ci-build.yml +++ b/.github/workflows/nalgebra-ci-build.yml @@ -38,8 +38,12 @@ jobs: run: cargo build --features serde-serialize - name: Build nalgebra-lapack run: cd nalgebra-lapack; cargo build; - - name: Build nalgebra-sparse + - name: Build nalgebra-sparse --no-default-features + run: cd nalgebra-sparse; cargo build --no-default-features; + - name: Build nalgebra-sparse (default features) run: cd nalgebra-sparse; cargo build; + - name: Build nalgebra-sparse --all-features + run: cd nalgebra-sparse; cargo build --all-features; # Run this on it’s own job because it alone takes a lot of time. # So it’s best to let it run in parallel to the other jobs. build-nalgebra-all-features: @@ -71,10 +75,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,io + run: PROPTEST_CASES=10000 cargo test --manifest-path=nalgebra-sparse/Cargo.toml --features compare,proptest-support,io,serde-serialize - 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,io,slow-tests slow + run: PROPTEST_CASES=10000 cargo test --release --manifest-path=nalgebra-sparse/Cargo.toml --features compare,proptest-support,io,serde-serialize,slow-tests slow test-nalgebra-macros: runs-on: ubuntu-latest steps: diff --git a/nalgebra-sparse/Cargo.toml b/nalgebra-sparse/Cargo.toml index 6f7a7b4a..eec7326d 100644 --- a/nalgebra-sparse/Cargo.toml +++ b/nalgebra-sparse/Cargo.toml @@ -15,6 +15,7 @@ license = "Apache-2.0" [features] proptest-support = ["proptest", "nalgebra/proptest-support"] compare = [ "matrixcompare-core" ] +serde-serialize = [ "serde/std" ] # Enable matrix market I/O io = [ "pest", "pest_derive" ] @@ -29,12 +30,14 @@ proptest = { version = "1.0", optional = true } matrixcompare-core = { version = "0.1.0", optional = true } pest = { version = "2", optional = true } pest_derive = { version = "2", optional = true } +serde = { version = "1.0", default-features = false, features = [ "derive" ], optional = true } [dev-dependencies] itertools = "0.10" matrixcompare = { version = "0.3.0", features = [ "proptest-support" ] } nalgebra = { version="0.30", path = "../", features = ["compare"] } +serde_json = "1.0" [package.metadata.docs.rs] # Enable certain features when building docs for docs.rs -features = [ "proptest-support", "compare" ] \ No newline at end of file +features = [ "proptest-support", "compare" ] diff --git a/nalgebra-sparse/src/coo.rs b/nalgebra-sparse/src/coo.rs index 34e5ceec..2b302e37 100644 --- a/nalgebra-sparse/src/coo.rs +++ b/nalgebra-sparse/src/coo.rs @@ -1,5 +1,8 @@ //! An implementation of the COO sparse matrix format. +#[cfg(feature = "serde-serialize")] +mod coo_serde; + use crate::SparseFormatError; /// A COO representation of a sparse matrix. diff --git a/nalgebra-sparse/src/coo/coo_serde.rs b/nalgebra-sparse/src/coo/coo_serde.rs new file mode 100644 index 00000000..7ffcdf4a --- /dev/null +++ b/nalgebra-sparse/src/coo/coo_serde.rs @@ -0,0 +1,65 @@ +use crate::coo::CooMatrix; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; + +/// This is an intermediate type for (de)serializing `CooMatrix`. +/// +/// Deserialization requires using a `try_from_*` function for validation. We could have used +/// the `remote = "Self"` trick (https://github.com/serde-rs/serde/issues/1220) which allows +/// to directly serialize/deserialize the original fields and combine it with validation. +/// However, this would lead to nested serialization of the `CsMatrix` and `SparsityPattern` +/// types. Instead, we decided that we want a more human-readable serialization format using +/// field names like `row_indices` and `col_indices`. The easiest way to achieve this is to +/// introduce an intermediate type. It also allows the serialization format to stay constant +/// even if the internal layout in `nalgebra` changes. +/// +/// We want to avoid unnecessary copies when serializing (i.e. cloning slices into owned +/// storage). Therefore, we use generic arguments to allow using slices during serialization and +/// owned storage (i.e. `Vec`) during deserialization. Without a major update of serde, slices +/// and `Vec`s should always (de)serialize identically. +#[derive(Serialize, Deserialize)] +struct CooMatrixSerializationData { + nrows: usize, + ncols: usize, + row_indices: Indices, + col_indices: Indices, + values: Values, +} + +impl Serialize for CooMatrix +where + T: Serialize + Clone, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + CooMatrixSerializationData::<&[usize], &[T]> { + nrows: self.nrows(), + ncols: self.ncols(), + row_indices: self.row_indices(), + col_indices: self.col_indices(), + values: self.values(), + } + .serialize(serializer) + } +} + +impl<'de, T> Deserialize<'de> for CooMatrix +where + T: Deserialize<'de> + Clone, +{ + fn deserialize(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let de = CooMatrixSerializationData::, Vec>::deserialize(deserializer)?; + CooMatrix::try_from_triplets( + de.nrows, + de.ncols, + de.row_indices, + de.col_indices, + de.values, + ) + .map_err(|e| de::Error::custom(e)) + } +} diff --git a/nalgebra-sparse/src/csc.rs b/nalgebra-sparse/src/csc.rs index 607cc0cf..a461658e 100644 --- a/nalgebra-sparse/src/csc.rs +++ b/nalgebra-sparse/src/csc.rs @@ -3,6 +3,9 @@ //! This is the module-level documentation. See [`CscMatrix`] for the main documentation of the //! CSC implementation. +#[cfg(feature = "serde-serialize")] +mod csc_serde; + use crate::cs::{CsLane, CsLaneIter, CsLaneIterMut, CsLaneMut, CsMatrix}; use crate::csr::CsrMatrix; use crate::pattern::{SparsityPattern, SparsityPatternFormatError, SparsityPatternIter}; diff --git a/nalgebra-sparse/src/csc/csc_serde.rs b/nalgebra-sparse/src/csc/csc_serde.rs new file mode 100644 index 00000000..aab12d47 --- /dev/null +++ b/nalgebra-sparse/src/csc/csc_serde.rs @@ -0,0 +1,65 @@ +use crate::CscMatrix; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; + +/// This is an intermediate type for (de)serializing `CscMatrix`. +/// +/// Deserialization requires using a `try_from_*` function for validation. We could have used +/// the `remote = "Self"` trick (https://github.com/serde-rs/serde/issues/1220) which allows +/// to directly serialize/deserialize the original fields and combine it with validation. +/// However, this would lead to nested serialization of the `CsMatrix` and `SparsityPattern` +/// types. Instead, we decided that we want a more human-readable serialization format using +/// field names like `col_offsets` and `row_indices`. The easiest way to achieve this is to +/// introduce an intermediate type. It also allows the serialization format to stay constant +/// even if the internal layout in `nalgebra` changes. +/// +/// We want to avoid unnecessary copies when serializing (i.e. cloning slices into owned +/// storage). Therefore, we use generic arguments to allow using slices during serialization and +/// owned storage (i.e. `Vec`) during deserialization. Without a major update of serde, slices +/// and `Vec`s should always (de)serialize identically. +#[derive(Serialize, Deserialize)] +struct CscMatrixSerializationData { + nrows: usize, + ncols: usize, + col_offsets: Indices, + row_indices: Indices, + values: Values, +} + +impl Serialize for CscMatrix +where + T: Serialize + Clone, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + CscMatrixSerializationData::<&[usize], &[T]> { + nrows: self.nrows(), + ncols: self.ncols(), + col_offsets: self.col_offsets(), + row_indices: self.row_indices(), + values: self.values(), + } + .serialize(serializer) + } +} + +impl<'de, T> Deserialize<'de> for CscMatrix +where + T: Deserialize<'de> + Clone, +{ + fn deserialize(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let de = CscMatrixSerializationData::, Vec>::deserialize(deserializer)?; + CscMatrix::try_from_csc_data( + de.nrows, + de.ncols, + de.col_offsets, + de.row_indices, + de.values, + ) + .map_err(|e| de::Error::custom(e)) + } +} diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index 4324d18d..bd35bf6b 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -2,6 +2,10 @@ //! //! This is the module-level documentation. See [`CsrMatrix`] for the main documentation of the //! CSC implementation. + +#[cfg(feature = "serde-serialize")] +mod csr_serde; + use crate::cs::{CsLane, CsLaneIter, CsLaneIterMut, CsLaneMut, CsMatrix}; use crate::csc::CscMatrix; use crate::pattern::{SparsityPattern, SparsityPatternFormatError, SparsityPatternIter}; @@ -9,7 +13,6 @@ use crate::{SparseEntry, SparseEntryMut, SparseFormatError, SparseFormatErrorKin use nalgebra::Scalar; use num_traits::One; - use std::iter::FromIterator; use std::slice::{Iter, IterMut}; diff --git a/nalgebra-sparse/src/csr/csr_serde.rs b/nalgebra-sparse/src/csr/csr_serde.rs new file mode 100644 index 00000000..1b33fda0 --- /dev/null +++ b/nalgebra-sparse/src/csr/csr_serde.rs @@ -0,0 +1,65 @@ +use crate::CsrMatrix; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; + +/// This is an intermediate type for (de)serializing `CsrMatrix`. +/// +/// Deserialization requires using a `try_from_*` function for validation. We could have used +/// the `remote = "Self"` trick (https://github.com/serde-rs/serde/issues/1220) which allows +/// to directly serialize/deserialize the original fields and combine it with validation. +/// However, this would lead to nested serialization of the `CsMatrix` and `SparsityPattern` +/// types. Instead, we decided that we want a more human-readable serialization format using +/// field names like `row_offsets` and `cal_indices`. The easiest way to achieve this is to +/// introduce an intermediate type. It also allows the serialization format to stay constant +/// even if the internal layout in `nalgebra` changes. +/// +/// We want to avoid unnecessary copies when serializing (i.e. cloning slices into owned +/// storage). Therefore, we use generic arguments to allow using slices during serialization and +/// owned storage (i.e. `Vec`) during deserialization. Without a major update of serde, slices +/// and `Vec`s should always (de)serialize identically. +#[derive(Serialize, Deserialize)] +struct CsrMatrixSerializationData { + nrows: usize, + ncols: usize, + row_offsets: Indices, + col_indices: Indices, + values: Values, +} + +impl Serialize for CsrMatrix +where + T: Serialize + Clone, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + CsrMatrixSerializationData::<&[usize], &[T]> { + nrows: self.nrows(), + ncols: self.ncols(), + row_offsets: self.row_offsets(), + col_indices: self.col_indices(), + values: self.values(), + } + .serialize(serializer) + } +} + +impl<'de, T> Deserialize<'de> for CsrMatrix +where + T: Deserialize<'de> + Clone, +{ + fn deserialize(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let de = CsrMatrixSerializationData::, Vec>::deserialize(deserializer)?; + CsrMatrix::try_from_csr_data( + de.nrows, + de.ncols, + de.row_offsets, + de.col_indices, + de.values, + ) + .map_err(|e| de::Error::custom(e)) + } +} diff --git a/nalgebra-sparse/src/pattern.rs b/nalgebra-sparse/src/pattern.rs index 85f6bc1a..50fae072 100644 --- a/nalgebra-sparse/src/pattern.rs +++ b/nalgebra-sparse/src/pattern.rs @@ -1,4 +1,8 @@ //! Sparsity patterns for CSR and CSC matrices. + +#[cfg(feature = "serde-serialize")] +mod pattern_serde; + use crate::cs::transpose_cs; use crate::SparseFormatError; use std::error::Error; diff --git a/nalgebra-sparse/src/pattern/pattern_serde.rs b/nalgebra-sparse/src/pattern/pattern_serde.rs new file mode 100644 index 00000000..e11a550a --- /dev/null +++ b/nalgebra-sparse/src/pattern/pattern_serde.rs @@ -0,0 +1,56 @@ +use crate::pattern::SparsityPattern; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; + +/// This is an intermediate type for (de)serializing `SparsityPattern`. +/// +/// Deserialization requires using a `try_from_*` function for validation. We could have used +/// the `remote = "Self"` trick (https://github.com/serde-rs/serde/issues/1220) which allows +/// to directly serialize/deserialize the original fields and combine it with validation. +/// However, this would lead to nested serialization of the `CsMatrix` and `SparsityPattern` +/// types. Instead, we decided that we want a more human-readable serialization format using +/// field names like `major_offsets` and `minor_indices`. The easiest way to achieve this is to +/// introduce an intermediate type. It also allows the serialization format to stay constant +/// even when the internal layout in `nalgebra` changes. +/// +/// We want to avoid unnecessary copies when serializing (i.e. cloning slices into owned +/// storage). Therefore, we use generic arguments to allow using slices during serialization and +/// owned storage (i.e. `Vec`) during deserialization. Without a major update of serde, slices +/// and `Vec`s should always (de)serialize identically. +#[derive(Serialize, Deserialize)] +struct SparsityPatternSerializationData { + major_dim: usize, + minor_dim: usize, + major_offsets: Indices, + minor_indices: Indices, +} + +impl Serialize for SparsityPattern { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + SparsityPatternSerializationData::<&[usize]> { + major_dim: self.major_dim(), + minor_dim: self.minor_dim(), + major_offsets: self.major_offsets(), + minor_indices: self.minor_indices(), + } + .serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for SparsityPattern { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let de = SparsityPatternSerializationData::>::deserialize(deserializer)?; + SparsityPattern::try_from_offsets_and_indices( + de.major_dim, + de.minor_dim, + de.major_offsets, + de.minor_indices, + ) + .map_err(|e| de::Error::custom(e)) + } +} diff --git a/nalgebra-sparse/tests/serde.rs b/nalgebra-sparse/tests/serde.rs new file mode 100644 index 00000000..1ce1953f --- /dev/null +++ b/nalgebra-sparse/tests/serde.rs @@ -0,0 +1,206 @@ +#![cfg(feature = "serde-serialize")] +//! Serialization tests +#[cfg(any(not(feature = "proptest-support"), not(feature = "compare")))] +compile_error!("Tests must be run with features `proptest-support` and `compare`"); + +#[macro_use] +pub mod common; + +use nalgebra_sparse::coo::CooMatrix; +use nalgebra_sparse::csc::CscMatrix; +use nalgebra_sparse::csr::CsrMatrix; +use nalgebra_sparse::pattern::SparsityPattern; + +use proptest::prelude::*; +use serde::{Deserialize, Serialize}; + +use crate::common::{csc_strategy, csr_strategy}; + +fn json_roundtrip Deserialize<'a>>(csr: &T) -> T { + let serialized = serde_json::to_string(csr).unwrap(); + let deserialized: T = serde_json::from_str(&serialized).unwrap(); + deserialized +} + +#[test] +fn pattern_roundtrip() { + { + // A pattern with zero explicitly stored entries + let pattern = + SparsityPattern::try_from_offsets_and_indices(3, 2, vec![0, 0, 0, 0], Vec::new()) + .unwrap(); + + assert_eq!(json_roundtrip(&pattern), pattern); + } + + { + // Arbitrary pattern + let offsets = vec![0, 2, 2, 5]; + let indices = vec![0, 5, 1, 2, 3]; + let pattern = + SparsityPattern::try_from_offsets_and_indices(3, 6, offsets.clone(), indices.clone()) + .unwrap(); + + assert_eq!(json_roundtrip(&pattern), pattern); + } +} + +#[test] +#[rustfmt::skip] +fn pattern_deserialize_invalid() { + assert!(serde_json::from_str::(r#"{"major_dim":3,"minor_dim":6,"major_offsets":[0,2,2,5],"minor_indices":[0,5,1,2,3]}"#).is_ok()); + assert!(serde_json::from_str::(r#"{"major_dim":0,"minor_dim":0,"major_offsets":[],"minor_indices":[]}"#).is_err()); + assert!(serde_json::from_str::(r#"{"major_dim":3,"minor_dim":6,"major_offsets":[0, 3, 5],"minor_indices":[0, 1, 2, 3, 5]}"#).is_err()); + assert!(serde_json::from_str::(r#"{"major_dim":3,"minor_dim":6,"major_offsets":[1, 2, 2, 5],"minor_indices":[0, 5, 1, 2, 3]}"#).is_err()); + assert!(serde_json::from_str::(r#"{"major_dim":3,"minor_dim":6,"major_offsets":[0, 2, 2, 4],"minor_indices":[0, 5, 1, 2, 3]}"#).is_err()); + assert!(serde_json::from_str::(r#"{"major_dim":3,"minor_dim":6,"major_offsets":[0, 2, 2],"minor_indices":[0, 5, 1, 2, 3]}"#).is_err()); + assert!(serde_json::from_str::(r#"{"major_dim":3,"minor_dim":6,"major_offsets":[0, 3, 2, 5],"minor_indices":[0, 1, 2, 3, 4]}"#).is_err()); + assert!(serde_json::from_str::(r#"{"major_dim":3,"minor_dim":6,"major_offsets":[0, 2, 2, 5],"minor_indices":[0, 2, 3, 1, 4]}"#).is_err()); + assert!(serde_json::from_str::(r#"{"major_dim":3,"minor_dim":6,"major_offsets":[0, 2, 2, 5],"minor_indices":[0, 6, 1, 2, 3]}"#).is_err()); + assert!(serde_json::from_str::(r#"{"major_dim":3,"minor_dim":6,"major_offsets":[0, 2, 2, 5],"minor_indices":[0, 5, 2, 2, 3]}"#).is_err()); +} + +#[test] +fn coo_roundtrip() { + { + // A COO matrix without entries + let matrix = + CooMatrix::::try_from_triplets(3, 2, Vec::new(), Vec::new(), Vec::new()).unwrap(); + + assert_eq!(json_roundtrip(&matrix), matrix); + } + + { + // Arbitrary COO matrix, no duplicates + let i = vec![0, 1, 0, 0, 2]; + let j = vec![0, 2, 1, 3, 3]; + let v = vec![2, 3, 7, 3, 1]; + let matrix = + CooMatrix::::try_from_triplets(3, 5, i.clone(), j.clone(), v.clone()).unwrap(); + + assert_eq!(json_roundtrip(&matrix), matrix); + } +} + +#[test] +fn coo_deserialize_invalid() { + // Valid matrix: {"nrows":3,"ncols":5,"row_indices":[0,1,0,0,2],"col_indices":[0,2,1,3,3],"values":[2,3,7,3,1]} + assert!(serde_json::from_str::>(r#"{"nrows":3,"ncols":5,"row_indices":[0,1,0,0,2],"col_indices":[0,2,1,3,3],"values":[2,3,7,3,1]}"#).is_ok()); + assert!(serde_json::from_str::>(r#"{"nrows":0,"ncols":0,"row_indices":[0,1,0,0,2],"col_indices":[0,2,1,3,3],"values":[2,3,7,3,4]}"#).is_err()); + assert!(serde_json::from_str::>(r#"{"nrows":-3,"ncols":5,"row_indices":[0,1,0,0,2],"col_indices":[0,2,1,3,3],"values":[2,3,7,3,4]}"#).is_err()); + assert!(serde_json::from_str::>(r#"{"nrows":3,"ncols":5,"row_indices":[0,1,0,0,2],"col_indices":[0,2,1,3,3],"values":[2,3,7,3]}"#).is_err()); + assert!(serde_json::from_str::>(r#"{"nrows":3,"ncols":5,"row_indices":[0,1,0,0,2],"col_indices":[0,2,1,3,3],"values":[2,3,7,3,4,5]}"#).is_err()); + assert!(serde_json::from_str::>(r#"{"nrows":3,"ncols":5,"row_indices":[0,1,0,0,2],"col_indices":[0,2,1,8,3],"values":[2,3,7,3,4]}"#).is_err()); + assert!(serde_json::from_str::>(r#"{"nrows":3,"ncols":5,"row_indices":[0,1,0,0],"col_indices":[0,2,1,8,3],"values":[2,3,7,3,4]}"#).is_err()); + assert!(serde_json::from_str::>(r#"{"nrows":3,"ncols":5,"row_indices":[0,10,0,0,2],"col_indices":[0,2,1,3,3],"values":[2,3,7,3,4]}"#).is_err()); + assert!(serde_json::from_str::>(r#"{"nrows":3,"ncols":5,"row_indices":[0,1,0,0,2],"col_indices":[0,2,1,30,3],"values":[2,3,7,3,4]}"#).is_err()); +} + +#[test] +fn coo_deserialize_duplicates() { + assert_eq!( + serde_json::from_str::>( + r#"{"nrows":3,"ncols":5,"row_indices":[0,1,0,0,2,0,1],"col_indices":[0,2,1,3,3,0,2],"values":[2,3,7,3,1,5,6]}"# + ).unwrap(), + CooMatrix::::try_from_triplets( + 3, + 5, + vec![0, 1, 0, 0, 2, 0, 1], + vec![0, 2, 1, 3, 3, 0, 2], + vec![2, 3, 7, 3, 1, 5, 6] + ) + .unwrap() + ); +} + +#[test] +fn csc_roundtrip() { + { + // A CSC matrix with zero explicitly stored entries + let offsets = vec![0, 0, 0, 0]; + let indices = vec![]; + let values = Vec::::new(); + let matrix = CscMatrix::try_from_csc_data(2, 3, offsets, indices, values).unwrap(); + + assert_eq!(json_roundtrip(&matrix), matrix); + } + + { + // An arbitrary CSC matrix + let offsets = vec![0, 2, 2, 5]; + let indices = vec![0, 5, 1, 2, 3]; + let values = vec![0, 1, 2, 3, 4]; + let matrix = + CscMatrix::try_from_csc_data(6, 3, offsets.clone(), indices.clone(), values.clone()) + .unwrap(); + + assert_eq!(json_roundtrip(&matrix), matrix); + } +} + +#[test] +fn csc_deserialize_invalid() { + // Valid matrix: {"nrows":6,"ncols":3,"col_offsets":[0,2,2,5],"row_indices":[0,5,1,2,3],"values":[0,1,2,3,4]} + assert!(serde_json::from_str::>(r#"{"nrows":6,"ncols":3,"col_offsets":[0,2,2,5],"row_indices":[0,5,1,2,3],"values":[0,1,2,3,4]}"#).is_ok()); + assert!(serde_json::from_str::>(r#"{"nrows":0,"ncols":0,"col_offsets":[0,2,2,5],"row_indices":[0,5,1,2,3],"values":[0,1,2,3,4]}"#).is_err()); + assert!(serde_json::from_str::>(r#"{"nrows":-6,"ncols":3,"col_offsets":[0,2,2,5],"row_indices":[0,5,1,2,3],"values":[0,1,2,3,4]}"#).is_err()); + assert!(serde_json::from_str::>(r#"{"nrows":6,"ncols":3,"col_offsets":[0,2,2,5],"row_indices":[0,5,1,2,3],"values":[0,1,2,3]}"#).is_err()); + assert!(serde_json::from_str::>(r#"{"nrows":6,"ncols":3,"col_offsets":[0,2,2,5],"row_indices":[0,5,1,2,3],"values":[0,1,2,3,4,5]}"#).is_err()); + assert!(serde_json::from_str::>(r#"{"nrows":6,"ncols":3,"col_offsets":[0,2,2,5],"row_indices":[0,5,1,8,3],"values":[0,1,2,3,4]}"#).is_err()); + assert!(serde_json::from_str::>(r#"{"nrows":6,"ncols":3,"col_offsets":[0,2,2,5],"row_indices":[0,5,1,2,3,1,1],"values":[0,1,2,3,4]}"#).is_err()); + // The following actually panics ('range end index 10 out of range for slice of length 5', nalgebra-sparse\src\pattern.rs:156:38) + //assert!(serde_json::from_str::>(r#"{"nrows":6,"ncols":3,"col_offsets":[0,10,2,5],"row_indices":[0,5,1,2,3],"values":[0,1,2,3,4]}"#).is_err()); + assert!(serde_json::from_str::>(r#"{"nrows":3,"ncols":6,"row_offsets":[0,2,2,5],"col_indices":[0,5,1,2,3],"values":[0,1,2,3,4]}"#).is_err()); +} + +#[test] +fn csr_roundtrip() { + { + // A CSR matrix with zero explicitly stored entries + let offsets = vec![0, 0, 0, 0]; + let indices = vec![]; + let values = Vec::::new(); + let matrix = CsrMatrix::try_from_csr_data(3, 2, offsets, indices, values).unwrap(); + + assert_eq!(json_roundtrip(&matrix), matrix); + } + + { + // An arbitrary CSR matrix + let offsets = vec![0, 2, 2, 5]; + let indices = vec![0, 5, 1, 2, 3]; + let values = vec![0, 1, 2, 3, 4]; + let matrix = + CsrMatrix::try_from_csr_data(3, 6, offsets.clone(), indices.clone(), values.clone()) + .unwrap(); + + assert_eq!(json_roundtrip(&matrix), matrix); + } +} + +#[test] +fn csr_deserialize_invalid() { + // Valid matrix: {"nrows":3,"ncols":6,"row_offsets":[0,2,2,5],"col_indices":[0,5,1,2,3],"values":[0,1,2,3,4]} + assert!(serde_json::from_str::>(r#"{"nrows":3,"ncols":6,"row_offsets":[0,2,2,5],"col_indices":[0,5,1,2,3],"values":[0,1,2,3,4]}"#).is_ok()); + assert!(serde_json::from_str::>(r#"{"nrows":0,"ncols":0,"row_offsets":[0,2,2,5],"col_indices":[0,5,1,2,3],"values":[0,1,2,3,4]}"#).is_err()); + assert!(serde_json::from_str::>(r#"{"nrows":-3,"ncols":6,"row_offsets":[0,2,2,5],"col_indices":[0,5,1,2,3],"values":[0,1,2,3,4]}"#).is_err()); + assert!(serde_json::from_str::>(r#"{"nrows":3,"ncols":6,"row_offsets":[0,2,2,5],"col_indices":[0,5,1,2,3],"values":[0,1,2,3]}"#).is_err()); + assert!(serde_json::from_str::>(r#"{"nrows":3,"ncols":6,"row_offsets":[0,2,2,5],"col_indices":[0,5,1,2,3],"values":[0,1,2,3,4,5]}"#).is_err()); + assert!(serde_json::from_str::>(r#"{"nrows":3,"ncols":6,"row_offsets":[0,2,2,5],"col_indices":[0,5,1,8,3],"values":[0,1,2,3,4]}"#).is_err()); + assert!(serde_json::from_str::>(r#"{"nrows":3,"ncols":6,"row_offsets":[0,2,2,5],"col_indices":[0,5,1,2,3,1,1],"values":[0,1,2,3,4]}"#).is_err()); + // The following actually panics ('range end index 10 out of range for slice of length 5', nalgebra-sparse\src\pattern.rs:156:38) + //assert!(serde_json::from_str::>(r#"{"nrows":3,"ncols":6,"row_offsets":[0,10,2,5],"col_indices":[0,5,1,2,3],"values":[0,1,2,3,4]}"#).is_err()); + assert!(serde_json::from_str::>(r#"{"nrows":6,"ncols":3,"col_offsets":[0,2,2,5],"row_indices":[0,5,1,2,3],"values":[0,1,2,3,4]}"#).is_err()); +} + +proptest! { + #[test] + fn csc_roundtrip_proptest(csc in csc_strategy()) { + prop_assert_eq!(json_roundtrip(&csc), csc); + } + + #[test] + fn csr_roundtrip_proptest(csr in csr_strategy()) { + prop_assert_eq!(json_roundtrip(&csr), csr); + } +}