Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Implement Serde Serialize/Deserialize for nalgebra-sparse types (Again) #1020

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion nalgebra-sparse/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ keywords = [ "linear", "algebra", "matrix", "vector", "math" ]
license = "Apache-2.0"

[features]
default = ["serde-serialize"]
proptest-support = ["proptest", "nalgebra/proptest-support"]
compare = [ "matrixcompare-core" ]
# Sparse uses std::collection::Vec, so no-std is a non-starter for now
# serde-serialize-no-std = [ "serde" ]
serde-serialize = [ "serde", "serde/std", "serde_test"]

# Enable to enable running some tests that take a lot of time to run
slow-tests = []
Expand All @@ -24,12 +28,15 @@ nalgebra = { version="0.29", path = "../" }
num-traits = { version = "0.2", default-features = false }
proptest = { version = "1.0", optional = true }
matrixcompare-core = { version = "0.1.0", optional = true }
serde = { version = "1.0", default-features = false, features = [ "derive" ], optional = true }
serde_test = { version = "1.0", optional = true }

[dev-dependencies]
itertools = "0.10"
matrixcompare = { version = "0.3.0", features = [ "proptest-support" ] }
nalgebra = { version="0.29", path = "../", features = ["compare"] }
serde_json = "1.0"

[package.metadata.docs.rs]
# Enable certain features when building docs for docs.rs
features = [ "proptest-support", "compare" ]
features = [ "proptest-support", "compare" ]
34 changes: 34 additions & 0 deletions nalgebra-sparse/src/coo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

use crate::SparseFormatError;

#[cfg(feature = "serde-serialize")]
use serde::{Deserialize, Deserializer, Serialize, Serializer};

/// A COO representation of a sparse matrix.
///
/// A COO matrix stores entries in coordinate-form, that is triplets `(i, j, v)`, where `i` and `j`
Expand Down Expand Up @@ -37,6 +40,11 @@ use crate::SparseFormatError;
/// let csc = CscMatrix::from(&coo);
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
feature = "serde-serialize",
derive(Serialize, Deserialize),
serde(remote = "Self")
)]
pub struct CooMatrix<T> {
nrows: usize,
ncols: usize,
Expand Down Expand Up @@ -273,3 +281,29 @@ impl<T> CooMatrix<T> {
(self.row_indices, self.col_indices, self.values)
}
}

impl<T: Serialize> Serialize for CooMatrix<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
Self::serialize(self, serializer)
}
}

impl<'de, T: Deserialize<'de>> Deserialize<'de> for CooMatrix<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let unchecked = Self::deserialize(deserializer)?;
Self::try_from_triplets(
unchecked.nrows,
unchecked.ncols,
unchecked.row_indices,
unchecked.col_indices,
unchecked.values,
)
.map_err(|e| serde::de::Error::custom(e.to_string()))
}
}
35 changes: 35 additions & 0 deletions nalgebra-sparse/src/cs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ use nalgebra::Scalar;
use crate::pattern::SparsityPattern;
use crate::{SparseEntry, SparseEntryMut};

#[cfg(feature = "serde-serialize")]
use serde::{Deserialize, Deserializer, Serialize, Serializer};

/// An abstract compressed matrix.
///
/// For the time being, this is only used internally to share implementation between
Expand All @@ -16,6 +19,11 @@ use crate::{SparseEntry, SparseEntryMut};
/// A CSR matrix is obtained by associating rows with the major dimension, while a CSC matrix
/// is obtained by associating columns with the major dimension.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
feature = "serde-serialize",
derive(Serialize, Deserialize),
serde(remote = "Self")
)]
pub struct CsMatrix<T> {
sparsity_pattern: SparsityPattern,
values: Vec<T>,
Expand Down Expand Up @@ -543,3 +551,30 @@ pub fn convert_counts_to_offsets(counts: &mut [usize]) {
offset += count;
}
}

impl<T: Serialize> Serialize for CsMatrix<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
Self::serialize(self, serializer)
}
}

impl<'de, T: Deserialize<'de>> Deserialize<'de> for CsMatrix<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let unchecked = Self::deserialize(deserializer)?;
if unchecked.sparsity_pattern.nnz() != unchecked.values.len() {
return Err(serde::de::Error::custom(
"Pattern non-zero count doesn't match number of values",
));
}
Ok(Self::from_pattern_and_values(
unchecked.sparsity_pattern,
unchecked.values,
))
}
}
4 changes: 4 additions & 0 deletions nalgebra-sparse/src/csc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ use nalgebra::Scalar;
use num_traits::One;
use std::slice::{Iter, IterMut};

#[cfg(feature = "serde-serialize")]
use serde::{Deserialize, Serialize};

/// A CSC representation of a sparse matrix.
///
/// The Compressed Sparse Column (CSC) format is well-suited as a general-purpose storage format
Expand Down Expand Up @@ -121,6 +124,7 @@ use std::slice::{Iter, IterMut};
///
/// [Wikipedia article]: https://en.wikipedia.org/wiki/Sparse_matrix#Compressed_sparse_column_(CSC_or_CCS)
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
pub struct CscMatrix<T> {
// Cols are major, rows are minor in the sparsity pattern
pub(crate) cs: CsMatrix<T>,
Expand Down
4 changes: 4 additions & 0 deletions nalgebra-sparse/src/csr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ use num_traits::One;
use std::iter::FromIterator;
use std::slice::{Iter, IterMut};

#[cfg(feature = "serde-serialize")]
use serde::{Deserialize, Serialize};

/// A CSR representation of a sparse matrix.
///
/// The Compressed Sparse Row (CSR) format is well-suited as a general-purpose storage format
Expand Down Expand Up @@ -122,6 +125,7 @@ use std::slice::{Iter, IterMut};
///
/// [Wikipedia article]: https://en.wikipedia.org/wiki/Sparse_matrix#Compressed_sparse_row_(CSR,_CRS_or_Yale_format)
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
pub struct CsrMatrix<T> {
// Rows are major, cols are minor in the sparsity pattern
pub(crate) cs: CsMatrix<T>,
Expand Down
126 changes: 126 additions & 0 deletions nalgebra-sparse/src/pattern.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ use crate::SparseFormatError;
use std::error::Error;
use std::fmt;

#[cfg(feature = "serde-serialize")]
use serde::{Deserialize, Deserializer, Serialize, Serializer};

/// A representation of the sparsity pattern of a CSR or CSC matrix.
///
/// CSR and CSC matrices store matrices in a very similar fashion. In fact, in a certain sense,
Expand Down Expand Up @@ -40,6 +43,11 @@ use std::fmt;
/// as for `row_offsets` and `col_indices` in the [CSR](`crate::csr::CsrMatrix`) format
/// specification.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
feature = "serde-serialize",
derive(Serialize, Deserialize),
serde(remote = "Self")
)]
// TODO: Make SparsityPattern parametrized by index type
// (need a solid abstraction for index types though)
pub struct SparsityPattern {
Expand Down Expand Up @@ -400,3 +408,121 @@ impl<'a> Iterator for SparsityPatternIter<'a> {
}
}
}

impl Serialize for SparsityPattern {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
Self::serialize(self, serializer)
}
}

impl<'de> Deserialize<'de> for SparsityPattern {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let unchecked = SparsityPattern::deserialize(deserializer)?;
if unchecked.major_offsets.len() == 0 {
return Err(serde::de::Error::custom(
"Deserializing from pattern with no major offset.
There should always be at least one major offset",
));
}
let major_dim = unchecked.major_offsets.len() - 1;
let minor_dim = unchecked.minor_dim();
let major_offsets = unchecked.major_offsets;
let minor_indices = unchecked.minor_indices;
SparsityPattern::try_from_offsets_and_indices(
major_dim,
minor_dim,
major_offsets,
minor_indices,
)
.map_err(|e| serde::de::Error::custom(e.to_string()))
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn pattern_serde() {
// Round trip
{
// 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();

let serialized = serde_json::to_string(&pattern).unwrap();
let deserialized = serde_json::from_str::<SparsityPattern>(&serialized).unwrap();
assert_eq!(pattern, deserialized);
}

// Empty Major Offsets
{
let sp = SparsityPattern {
major_offsets: vec![0; 0],
minor_indices: vec![0; 0],
minor_dim: 0,
};
let serialized = serde_json::to_string(&sp).unwrap();
let deserialized = serde_json::from_str::<SparsityPattern>(&serialized);
match deserialized {
Ok(_) => assert!(false, "Should error if major offsets is empty"),
Err(_e) => assert!(true, "asdf"), // TODO check message
}
}
// InvalidOffsetArrayLength
{
let sp = SparsityPattern {
major_offsets: vec![0, 1, 2, 3],
minor_indices: vec![0; 0],
minor_dim: 3,
};
let serialized = serde_json::to_string(&sp).unwrap();
let deserialized = serde_json::from_str::<SparsityPattern>(&serialized);
match deserialized {
Ok(_) => assert!(false, "Should error if major offsets is empty"),
Err(_e) => assert!(true, "asdf"), // TODO check message
}
}
// Nonmonotonic Minor Indiices
{
let sp = SparsityPattern {
major_offsets: vec![0, 1, 2, 3],
minor_indices: vec![3, 2, 1, 0],
minor_dim: 3,
};
let serialized = serde_json::to_string(&sp).unwrap();
let deserialized = serde_json::from_str::<SparsityPattern>(&serialized);
match deserialized {
Ok(_) => assert!(false, "Should error if major offsets is empty"),
Err(_e) => assert!(true, "asdf"), // TODO check message
}
}
// Duplicate Entries
{
let sp = SparsityPattern {
major_offsets: vec![0, 1, 2, 3],
minor_indices: vec![0, 1, 2, 2],
minor_dim: 3,
};
let serialized = serde_json::to_string(&sp).unwrap();
let deserialized = serde_json::from_str::<SparsityPattern>(&serialized);
match deserialized {
Ok(_) => assert!(false, "Should error if major offsets is empty"),
Err(_e) => assert!(true, "asdf"), // TODO check message
}
}
}
}
13 changes: 13 additions & 0 deletions nalgebra-sparse/tests/unit_tests/coo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,3 +344,16 @@ fn coo_push_matrix_out_of_bounds_entries() {
assert_panics!(CooMatrix::new(3, 3).push_matrix(2, 2, &inserted));
}
}

#[test]
fn coo_serde() {
// Arbitrary matrix, with duplicates
let i = vec![0, 1, 0, 0, 0, 0, 2, 1];
let j = vec![0, 2, 0, 1, 0, 3, 3, 2];
let v = vec![2, 3, 4, 7, 1, 3, 1, 5];
let coo = CooMatrix::<i32>::try_from_triplets(3, 5, i.clone(), j.clone(), v.clone()).unwrap();

let serialized = serde_json::to_string(&coo).unwrap();
let deserialized = serde_json::from_str::<CooMatrix<i32>>(&serialized).unwrap();
assert_eq!(coo, deserialized);
}
Loading