From f9ea2b4471101fff9e53844a964a5ba5a58dbc75 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 10 Nov 2020 14:46:33 +0100 Subject: [PATCH 001/183] Initial proptest implementation for nalgebra This introduces functionality for creating strategies for matrices and vectors, as well as an implementation of Arbitrary. Strategies for the geometric types (Point3, Quaternion etc.) are not currently part of this contribution. The current strategy implementation for matrices has some limitations that lead to suboptimal shrinking behavior. This is documented in the module-level docs, with some additional comments in the code. --- .circleci/config.yml | 2 +- Cargo.toml | 10 + src/lib.rs | 2 + src/proptest/mod.rs | 469 ++++++++++++++++++++++++++++++++++++++++++ tests/lib.rs | 4 + tests/proptest/mod.rs | 184 +++++++++++++++++ 6 files changed, 670 insertions(+), 1 deletion(-) create mode 100644 src/proptest/mod.rs create mode 100644 tests/proptest/mod.rs diff --git a/.circleci/config.yml b/.circleci/config.yml index 446e1139..c947dbd7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -57,7 +57,7 @@ jobs: - checkout - run: name: test - command: cargo test --features arbitrary --features serde-serialize --features abomonation-serialize --features sparse --features debug --features io --features compare --features libm + command: cargo test --features arbitrary --features serde-serialize --features abomonation-serialize --features sparse --features debug --features io --features compare --features libm --features proptest - run: name: test nalgebra-glm command: cargo test -p nalgebra-glm --features arbitrary --features serde-serialize --features abomonation-serialize --features sparse --features debug --features io --features compare --features libm diff --git a/Cargo.toml b/Cargo.toml index 615942a8..9a92dedd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ quickcheck = { version = "0.9", optional = true } pest = { version = "2", optional = true } pest_derive = { version = "2", optional = true } matrixcompare-core = { version = "0.1", optional = true } +proptest = { version = "0.10", optional = true, default-features = false, features = ["std"] } [dev-dependencies] serde_json = "1.0" @@ -68,6 +69,11 @@ rand_isaac = "0.2" # For matrix comparison macro matrixcompare = "0.1.3" +# Make sure that we use a specific version of proptest for tests. The reason is that we use a deterministic +# RNG for certain tests. However, different versions of proptest may give different sequences of numbers, +# which may cause more brittle tests (although ideally they should take enough samples for it not to matter). +proptest = { version = "=0.10.1" } + [workspace] members = [ "nalgebra-lapack", "nalgebra-glm" ] @@ -78,3 +84,7 @@ path = "benches/lib.rs" [profile.bench] lto = true + +[package.metadata.docs.rs] +# Enable certain features when building docs for docs.rs +features = [ "proptest" ] diff --git a/src/lib.rs b/src/lib.rs index 12a329be..22f39ae7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -127,6 +127,8 @@ pub mod geometry; #[cfg(feature = "io")] pub mod io; pub mod linalg; +#[cfg(feature = "proptest")] +pub mod proptest; #[cfg(feature = "sparse")] pub mod sparse; diff --git a/src/proptest/mod.rs b/src/proptest/mod.rs new file mode 100644 index 00000000..093e1525 --- /dev/null +++ b/src/proptest/mod.rs @@ -0,0 +1,469 @@ +//! `proptest`-related features for `nalgebra` data structures. +//! +//! **This module is only available when the `proptest` feature is enabled in `nalgebra`**. +//! +//! `proptest` is a library for *property-based testing*. While similar to QuickCheck, +//! which may be more familiar to some users, it has a more sophisticated design that +//! provides users with automatic invariant-preserving shrinking. This means that when using +//! `proptest`, you rarely need to write your own shrinkers - which is usually very difficult - +//! and can instead get this "for free". Moreover, `proptest` does not rely on a canonical +//! `Arbitrary` trait implementation like QuickCheck, though it does also provide this. For +//! more information, check out the [proptest docs](https://docs.rs/proptest/0.10.1/proptest/) +//! and the [proptest book](https://altsysrq.github.io/proptest-book/intro.html). +//! +//! This module provides users of `nalgebra` with tools to work with `nalgebra` types in +//! `proptest` tests. At present, this integration is at an early stage, and only +//! provides tools for generating matrices and vectors, and not any of the geometry types. +//! There are essentially two ways of using this functionality: +//! +//! - Using the [matrix](fn.matrix.html) function to generate matrices with constraints +//! on dimensions and elements. +//! - Relying on the `Arbitrary` implementation of `MatrixMN`. +//! +//! The first variant is almost always preferred in practice. Read on to discover why. +//! +//! ### Using free function strategies +//! +//! In `proptest`, it is usually preferable to have free functions that generate *strategies*. +//! Currently, the [matrix](fn.matrix.html) function fills this role. The analogous function for +//! column vectors is [vector](fn.vector.html). Let's take a quick look at how it may be used: +//! ```rust +//! use nalgebra::proptest::matrix; +//! use proptest::prelude::*; +//! +//! proptest! { +//! # /* +//! #[test] +//! # */ +//! fn my_test(a in matrix(-5 ..= 5, 2 ..= 4, 1..=4)) { +//! // Generates matrices with elements in the range -5 ..= 5, rows in 2..=4 and +//! // columns in 1..=4. +//! } +//! } +//! +//! # fn main() { my_test(); } +//! ``` +//! +//! In the above example, we generate matrices with constraints on the elements, as well as the +//! on the allowed dimensions. When a failing example is found, the resulting shrinking process +//! will preserve these invariants. We can use this to compose more advanced strategies. +//! For example, let's consider a toy example where we need to generate pairs of matrices +//! with exactly 3 rows fixed at compile-time and the same number of columns, but we want the +//! number of columns to vary. One way to do this is to use `proptest` combinators in combination +//! with [matrix](fn.matrix.html) as follows: +//! +//! ```rust +//! use nalgebra::{Dynamic, MatrixMN, U3}; +//! use nalgebra::proptest::matrix; +//! use proptest::prelude::*; +//! +//! type MyMatrix = MatrixMN; +//! +//! /// Returns a strategy for pairs of matrices with `U3` rows and the same number of +//! /// columns. +//! fn matrix_pairs() -> impl Strategy { +//! matrix(-5 ..= 5, U3, 0 ..= 10) +//! // We first generate the initial matrix `a`, and then depending on the concrete +//! // instances of `a`, we pick a second matrix with the same number of columns +//! .prop_flat_map(|a| { +//! let b = matrix(-5 .. 5, U3, a.ncols()); +//! // This returns a new tuple strategy where we keep `a` fixed while +//! // the second item is a strategy that generates instances with the same +//! // dimensions as `a` +//! (Just(a), b) +//! }) +//! } +//! +//! proptest! { +//! # /* +//! #[test] +//! # */ +//! fn my_test((a, b) in matrix_pairs()) { +//! // Let's double-check that the two matrices do indeed have the same number of +//! // columns +//! prop_assert_eq!(a.ncols(), b.ncols()); +//! } +//! } +//! +//! # fn main() { my_test(); } +//! ``` +//! +//! ### The `Arbitrary` implementation +//! +//! If you don't care about the dimensions of matrices, you can write tests like these: +//! +//! ```rust +//! use nalgebra::{DMatrix, DVector, Dynamic, Matrix3, MatrixMN, Vector3, U3}; +//! use proptest::prelude::*; +//! +//! proptest! { +//! # /* +//! #[test] +//! # */ +//! fn test_dynamic(matrix: DMatrix) { +//! // This will generate arbitrary instances of `DMatrix` and also attempt +//! // to shrink/simplify them when test failures are encountered. +//! } +//! +//! # /* +//! #[test] +//! # */ +//! fn test_static_and_mixed(matrix: Matrix3, matrix2: MatrixMN) { +//! // Test some property involving these matrices +//! } +//! +//! # /* +//! #[test] +//! # */ +//! fn test_vectors(fixed_size_vector: Vector3, dyn_vector: DVector) { +//! // Test some property involving these vectors +//! } +//! } +//! +//! # fn main() { test_dynamic(); test_static_and_mixed(); test_vectors(); } +//! ``` +//! +//! While this may be convenient, the default strategies for built-in types in `proptest` can +//! generate *any* number, including integers large enough to easily lead to overflow when used in +//! matrix operations, or even infinity or NaN values for floating-point types. Therefore +//! `Arbitrary` is rarely the method of choice for writing property-based tests. +//! +//! ### Notes on shrinking +//! +//! Due to some limitations of the current implementation, shrinking takes place by first +//! shrinking the matrix elements before trying to shrink the dimensions of the matrix. +//! This unfortunately often leads to the fact that a large number of shrinking iterations +//! are necessary to find a (nearly) minimal failing test case. As a workaround for this, +//! you can increase the maximum number of shrinking iterations when debugging. To do this, +//! simply set the `PROPTEST_MAX_SHRINK_ITERS` variable to a high number. For example: +//! +//! ```text +//! PROPTEST_MAX_SHRINK_ITERS=100000 cargo test my_failing_test +//! ``` +use crate::allocator::Allocator; +use crate::{DefaultAllocator, Dim, DimName, Dynamic, MatrixMN, Scalar, U1}; +use proptest::arbitrary::Arbitrary; +use proptest::collection::vec; +use proptest::strategy::{BoxedStrategy, Just, NewTree, Strategy, ValueTree}; +use proptest::test_runner::TestRunner; + +use std::ops::RangeInclusive; + +/// Parameters for arbitrary matrix generation. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct MatrixParameters { + /// The range of rows that may be generated. + pub rows: DimRange, + /// The range of columns that may be generated. + pub cols: DimRange, + /// Parameters for the `Arbitrary` implementation of the scalar values. + pub value_parameters: NParameters, +} + +/// A range of allowed dimensions for use in generation of matrices. +/// +/// The `DimRange` type is used to encode the range of dimensions that can be used for generation +/// of matrices with `proptest`. In most cases, you do not need to concern yourself with +/// `DimRange` directly, as it supports conversion from other types such as `U3` or inclusive +/// ranges such as `5 ..= 6`. The latter example corresponds to dimensions from (inclusive) +/// `Dynamic::new(5)` to `Dynamic::new(6)` (inclusive). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DimRange(RangeInclusive); + +impl DimRange { + /// The lower bound for dimensions generated. + pub fn lower_bound(&self) -> D { + *self.0.start() + } + + /// The upper bound for dimensions generated. + pub fn upper_bound(&self) -> D { + *self.0.end() + } +} + +impl From for DimRange { + fn from(dim: D) -> Self { + DimRange(dim..=dim) + } +} + +impl From> for DimRange { + fn from(range: RangeInclusive) -> Self { + DimRange(range) + } +} + +impl From> for DimRange { + fn from(range: RangeInclusive) -> Self { + DimRange::from(Dynamic::new(*range.start())..=Dynamic::new(*range.end())) + } +} + +impl From for DimRange { + fn from(dim: usize) -> Self { + DimRange::from(Dynamic::new(dim)) + } +} + +/// The default range used for Dynamic dimensions when generating arbitrary matrices. +fn dynamic_dim_range() -> DimRange { + DimRange::from(0..=6) +} + +/// Create a strategy to generate matrices containing values drawn from the given strategy, +/// with rows and columns in the provided ranges. +/// +/// ## Examples +/// ``` +/// use nalgebra::proptest::matrix; +/// use nalgebra::{MatrixMN, U3, Dynamic}; +/// use proptest::prelude::*; +/// +/// proptest! { +/// # /* +/// #[test] +/// # */ +/// fn my_test(a in matrix(0 .. 5i32, U3, 0 ..= 5)) { +/// // Let's make sure we've got the correct type first +/// let a: MatrixMN<_, U3, Dynamic> = a; +/// prop_assert!(a.nrows() == 3); +/// prop_assert!(a.ncols() <= 5); +/// prop_assert!(a.iter().all(|x_ij| *x_ij >= 0 && *x_ij < 5)); +/// } +/// } +/// +/// # fn main() { my_test(); } +/// ``` +/// +/// ## Limitations +/// The current implementation has some limitations that lead to suboptimal shrinking behavior. +/// See the [module-level documentation](index.html) for more. +pub fn matrix( + value_strategy: ScalarStrategy, + rows: impl Into>, + cols: impl Into>, +) -> MatrixStrategy +where + ScalarStrategy: Strategy + Clone + 'static, + ScalarStrategy::Value: Scalar, + R: Dim, + C: Dim, + DefaultAllocator: Allocator, +{ + matrix_(value_strategy, rows.into(), cols.into()) +} + +/// Same as `matrix`, but without the additional anonymous generic types +fn matrix_( + value_strategy: ScalarStrategy, + rows: DimRange, + cols: DimRange, +) -> MatrixStrategy +where + ScalarStrategy: Strategy + Clone + 'static, + ScalarStrategy::Value: Scalar, + R: Dim, + C: Dim, + DefaultAllocator: Allocator, +{ + let nrows = rows.lower_bound().value()..=rows.upper_bound().value(); + let ncols = cols.lower_bound().value()..=cols.upper_bound().value(); + + // Even though we can use this function to generate fixed-size matrices, + // we currently generate all matrices with heap allocated Vec data. + // TODO: Avoid heap allocation for fixed-size matrices. + // Doing this *properly* would probably require us to implement a custom + // strategy and valuetree with custom shrinking logic, which is not trivial + + // Perhaps more problematic, however, is the poor shrinking behavior the current setup leads to. + // Shrinking in proptest basically happens in "reverse" of the combinators, so + // by first generating the dimensions and then the elements, we get shrinking that first + // tries to completely shrink the individual elements before trying to reduce the dimension. + // This is clearly the opposite of what we want. I can't find any good way around this + // short of writing our own custom value tree, which we should probably do at some point. + // TODO: Custom implementation of value tree for better shrinking behavior. + + let strategy = nrows + .prop_flat_map(move |nrows| (Just(nrows), ncols.clone())) + .prop_flat_map(move |(nrows, ncols)| { + ( + Just(nrows), + Just(ncols), + vec(value_strategy.clone(), nrows * ncols), + ) + }) + .prop_map(|(nrows, ncols, values)| { + // Note: R/C::from_usize will panic if nrows/ncols does not fit in the dimension type. + // However, this should never fail, because we should only be generating + // this stuff in the first place + MatrixMN::from_iterator_generic(R::from_usize(nrows), C::from_usize(ncols), values) + }) + .boxed(); + + MatrixStrategy { strategy } +} + +/// Create a strategy to generate column vectors containing values drawn from the given strategy, +/// with length in the provided range. +/// +/// This is a convenience function for calling +/// [matrix(value_strategy, length, U1)](fn.matrix.html) and should +/// be used when you only want to generate column vectors, as it's simpler and makes the intent +/// clear. +pub fn vector( + value_strategy: ScalarStrategy, + length: impl Into>, +) -> MatrixStrategy +where + ScalarStrategy: Strategy + Clone + 'static, + ScalarStrategy::Value: Scalar, + D: Dim, + DefaultAllocator: Allocator, +{ + matrix_(value_strategy, length.into(), U1.into()) +} + +impl Default for MatrixParameters +where + NParameters: Default, + R: DimName, + C: DimName, +{ + fn default() -> Self { + Self { + rows: DimRange::from(R::name()), + cols: DimRange::from(C::name()), + value_parameters: NParameters::default(), + } + } +} + +impl Default for MatrixParameters +where + NParameters: Default, + R: DimName, +{ + fn default() -> Self { + Self { + rows: DimRange::from(R::name()), + cols: dynamic_dim_range(), + value_parameters: NParameters::default(), + } + } +} + +impl Default for MatrixParameters +where + NParameters: Default, + C: DimName, +{ + fn default() -> Self { + Self { + rows: dynamic_dim_range(), + cols: DimRange::from(C::name()), + value_parameters: NParameters::default(), + } + } +} + +impl Default for MatrixParameters +where + NParameters: Default, +{ + fn default() -> Self { + Self { + rows: dynamic_dim_range(), + cols: dynamic_dim_range(), + value_parameters: NParameters::default(), + } + } +} + +impl Arbitrary for MatrixMN +where + N: Scalar + Arbitrary, + ::Strategy: Clone, + R: Dim, + C: Dim, + MatrixParameters: Default, + DefaultAllocator: Allocator, +{ + type Parameters = MatrixParameters; + + fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { + let value_strategy = N::arbitrary_with(args.value_parameters); + matrix(value_strategy, args.rows, args.cols) + } + + type Strategy = MatrixStrategy; +} + +/// A strategy for generating matrices. +#[derive(Debug)] +pub struct MatrixStrategy +where + NStrategy: Strategy, + NStrategy::Value: Scalar, + DefaultAllocator: Allocator, +{ + // For now we only internally hold a boxed strategy. The reason for introducing this + // separate wrapper struct is so that we can replace the strategy logic with custom logic + // later down the road without introducing significant breaking changes + strategy: BoxedStrategy>, +} + +impl Strategy for MatrixStrategy +where + NStrategy: Strategy, + NStrategy::Value: Scalar, + R: Dim, + C: Dim, + DefaultAllocator: Allocator, +{ + type Tree = MatrixValueTree; + type Value = MatrixMN; + + fn new_tree(&self, runner: &mut TestRunner) -> NewTree { + let underlying_tree = self.strategy.new_tree(runner)?; + Ok(MatrixValueTree { + value_tree: underlying_tree, + }) + } +} + +/// A value tree for matrices. +pub struct MatrixValueTree +where + N: Scalar, + R: Dim, + C: Dim, + DefaultAllocator: Allocator, +{ + // For now we only wrap a boxed value tree. The reason for wrapping is that this allows us + // to swap out the value tree logic down the road without significant breaking changes. + value_tree: Box>>, +} + +impl ValueTree for MatrixValueTree +where + N: Scalar, + R: Dim, + C: Dim, + DefaultAllocator: Allocator, +{ + type Value = MatrixMN; + + fn current(&self) -> Self::Value { + self.value_tree.current() + } + + fn simplify(&mut self) -> bool { + self.value_tree.simplify() + } + + fn complicate(&mut self) -> bool { + self.value_tree.complicate() + } +} diff --git a/tests/lib.rs b/tests/lib.rs index 02044b97..47386e33 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -19,5 +19,9 @@ extern crate quickcheck; mod core; mod geometry; mod linalg; + +#[cfg(feature = "proptest")] +mod proptest; + //#[cfg(feature = "sparse")] //mod sparse; diff --git a/tests/proptest/mod.rs b/tests/proptest/mod.rs new file mode 100644 index 00000000..a5068344 --- /dev/null +++ b/tests/proptest/mod.rs @@ -0,0 +1,184 @@ +//! Tests for proptest-related functionality. +use nalgebra::base::dimension::*; +use nalgebra::proptest::{matrix, DimRange, MatrixStrategy}; +use nalgebra::{DMatrix, DVector, Dim, Matrix3, Matrix4, MatrixMN, Vector3}; +use proptest::prelude::*; +use proptest::strategy::ValueTree; +use proptest::test_runner::TestRunner; + +/// Generate a proptest that tests that all matrices generated with the +/// provided rows and columns conform to the constraints defined by the +/// input. +macro_rules! generate_matrix_sanity_test { + ($test_name:ident, $rows:expr, $cols:expr) => { + proptest! { + #[test] + fn $test_name(a in matrix(-5 ..= 5i32, $rows, $cols)) { + // let a: MatrixMN<_, $rows, $cols> = a; + let rows_range = DimRange::from($rows); + let cols_range = DimRange::from($cols); + prop_assert!(a.nrows() >= rows_range.lower_bound().value() + && a.nrows() <= rows_range.upper_bound().value()); + prop_assert!(a.ncols() >= cols_range.lower_bound().value() + && a.ncols() <= cols_range.upper_bound().value()); + prop_assert!(a.iter().all(|x_ij| *x_ij >= -5 && *x_ij <= 5)); + } + } + }; +} + +// Test all fixed-size matrices with row/col dimensions up to 3 +generate_matrix_sanity_test!(test_matrix_u0_u0, U0, U0); +generate_matrix_sanity_test!(test_matrix_u1_u0, U1, U0); +generate_matrix_sanity_test!(test_matrix_u0_u1, U0, U1); +generate_matrix_sanity_test!(test_matrix_u1_u1, U1, U1); +generate_matrix_sanity_test!(test_matrix_u2_u1, U2, U1); +generate_matrix_sanity_test!(test_matrix_u1_u2, U1, U2); +generate_matrix_sanity_test!(test_matrix_u2_u2, U2, U2); +generate_matrix_sanity_test!(test_matrix_u3_u2, U3, U2); +generate_matrix_sanity_test!(test_matrix_u2_u3, U2, U3); +generate_matrix_sanity_test!(test_matrix_u3_u3, U3, U3); + +// Similarly test all heap-allocated but fixed dim ranges +generate_matrix_sanity_test!(test_matrix_0_0, 0, 0); +generate_matrix_sanity_test!(test_matrix_0_1, 0, 1); +generate_matrix_sanity_test!(test_matrix_1_0, 1, 0); +generate_matrix_sanity_test!(test_matrix_1_1, 1, 1); +generate_matrix_sanity_test!(test_matrix_2_1, 2, 1); +generate_matrix_sanity_test!(test_matrix_1_2, 1, 2); +generate_matrix_sanity_test!(test_matrix_2_2, 2, 2); +generate_matrix_sanity_test!(test_matrix_3_2, 3, 2); +generate_matrix_sanity_test!(test_matrix_2_3, 2, 3); +generate_matrix_sanity_test!(test_matrix_3_3, 3, 3); + +// Test arbitrary inputs +generate_matrix_sanity_test!(test_matrix_input_1, U5, 1..=5); +generate_matrix_sanity_test!(test_matrix_input_2, 3..=4, 1..=5); +generate_matrix_sanity_test!(test_matrix_input_3, 1..=2, U3); +generate_matrix_sanity_test!(test_matrix_input_4, 3, U4); + +#[test] +fn test_matrix_output_types() { + // Test that the dimension types are correct for the given inputs + let _: MatrixStrategy<_, U3, U4> = matrix(-5..5, U3, U4); + let _: MatrixStrategy<_, U3, U3> = matrix(-5..5, U3, U3); + let _: MatrixStrategy<_, U3, Dynamic> = matrix(-5..5, U3, 1..=5); + let _: MatrixStrategy<_, Dynamic, U3> = matrix(-5..5, 1..=5, U3); + let _: MatrixStrategy<_, Dynamic, Dynamic> = matrix(-5..5, 1..=5, 1..=5); +} + +// Below we have some tests to ensure that specific instances of MatrixMN are usable +// in a typical proptest scenario where we (implicitly) use the `Arbitrary` trait +proptest! { + #[test] + fn ensure_arbitrary_test_compiles_matrix3(_: Matrix3) {} + + #[test] + fn ensure_arbitrary_test_compiles_matrixmn_u3_dynamic(_: MatrixMN) {} + + #[test] + fn ensure_arbitrary_test_compiles_matrixmn_dynamic_u3(_: MatrixMN) {} + + #[test] + fn ensure_arbitrary_test_compiles_dmatrix(_: DMatrix) {} + + #[test] + fn ensure_arbitrary_test_compiles_vector3(_: Vector3) {} + + #[test] + fn ensure_arbitrary_test_compiles_dvector(_: DVector) {} +} + +#[test] +fn matrix_samples_all_possible_outputs() { + // Test that the proptest generation covers all possible outputs for a small space of inputs + // given enough samples. + + // We use a deterministic test runner to make the test "stable". + let mut runner = TestRunner::deterministic(); + + let strategy = matrix(0..=2usize, 0..=3, 0..=3); + + // We use flags to record whether values and combinations of dimensions were encountered. + // For example, if we encounter value 1, we set the value flag of 1 to true, + // and if we encounted matrix dimensions 4x3, we set the flag of [4, 3] to true. + let mut value_encountered = Vector3::new(false, false, false); + let mut dimensions_encountered = Matrix4::repeat(false); + + for _ in 0..1000 { + let tree = strategy + .new_tree(&mut runner) + .expect("Tree generation should not fail"); + let matrix = tree.current(); + + dimensions_encountered[(matrix.nrows(), matrix.ncols())] = true; + + for &value in matrix.iter() { + value_encountered[value] = true; + } + } + + assert!( + value_encountered.iter().all(|v| *v), + "Did not sample all possible values." + ); + assert!( + dimensions_encountered.iter().all(|v| *v), + "Did not sample all possible matrix dimensions." + ); +} + +#[test] +fn matrix_shrinking_satisfies_constraints() { + // We use a deterministic test runner to make the test "stable". + let mut runner = TestRunner::deterministic(); + + let strategy = matrix(-1..=2, 1..=3, 2..=4); + + let num_matrices = 25; + + macro_rules! maybeprintln { + ($($arg:tt)*) => { + // Uncomment the below line to enable printing of matrix sequences. This is handy + // for manually inspecting the sequences of simplified matrices. + // println!($($arg)*) + }; + } + + maybeprintln!("========================== (begin generation process)"); + + for _ in 0..num_matrices { + let mut tree = strategy + .new_tree(&mut runner) + .expect("Tree generation should not fail."); + + let mut current = Some(tree.current()); + + maybeprintln!("------------------"); + + while let Some(matrix) = current { + maybeprintln!("{}", matrix); + + assert!( + matrix.iter().all(|&v| v >= -1 && v <= 2), + "All matrix elements must satisfy constraints" + ); + assert!( + matrix.nrows() >= 1 && matrix.nrows() <= 3, + "Number of rows in matrix must satisfy constraints." + ); + assert!( + matrix.ncols() >= 2 && matrix.ncols() <= 4, + "Number of columns in matrix must satisfy constraints." + ); + + current = if tree.simplify() { + Some(tree.current()) + } else { + None + } + } + } + + maybeprintln!("========================== (end of generation process)"); +} From cbef37ed9c710f13d0fe8c9004b5c6a1e54fccf3 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 16 Nov 2020 11:12:53 +0100 Subject: [PATCH 002/183] Fix proptest functionality test The previous test claimed to verify that all possible outputs are sampled, but it didn't. This commits fixes this issue by actually computing all possible combinations. However, to accomplish this we needed to add itertools as a test dependency. Otherwise we'd have to implement our own way of generating the Cartesian product of an arbitrary number of sets. --- Cargo.toml | 1 + tests/proptest/mod.rs | 63 ++++++++++++++++++++++++++++--------------- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9a92dedd..a74d39cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,7 @@ matrixcompare = "0.1.3" # RNG for certain tests. However, different versions of proptest may give different sequences of numbers, # which may cause more brittle tests (although ideally they should take enough samples for it not to matter). proptest = { version = "=0.10.1" } +itertools = "0.9" [workspace] members = [ "nalgebra-lapack", "nalgebra-glm" ] diff --git a/tests/proptest/mod.rs b/tests/proptest/mod.rs index a5068344..40d61864 100644 --- a/tests/proptest/mod.rs +++ b/tests/proptest/mod.rs @@ -1,10 +1,13 @@ //! Tests for proptest-related functionality. use nalgebra::base::dimension::*; use nalgebra::proptest::{matrix, DimRange, MatrixStrategy}; -use nalgebra::{DMatrix, DVector, Dim, Matrix3, Matrix4, MatrixMN, Vector3}; +use nalgebra::{DMatrix, DVector, Dim, Matrix3, MatrixMN, Vector3}; use proptest::prelude::*; use proptest::strategy::ValueTree; use proptest::test_runner::TestRunner; +use itertools::Itertools; +use std::iter::repeat; +use std::collections::HashSet; /// Generate a proptest that tests that all matrices generated with the /// provided rows and columns conform to the constraints defined by the @@ -97,35 +100,51 @@ fn matrix_samples_all_possible_outputs() { // We use a deterministic test runner to make the test "stable". let mut runner = TestRunner::deterministic(); - let strategy = matrix(0..=2usize, 0..=3, 0..=3); + // This number needs to be high enough so that we with high probability sample + // all possible cases + let num_generated_matrices = 200000; - // We use flags to record whether values and combinations of dimensions were encountered. - // For example, if we encounter value 1, we set the value flag of 1 to true, - // and if we encounted matrix dimensions 4x3, we set the flag of [4, 3] to true. - let mut value_encountered = Vector3::new(false, false, false); - let mut dimensions_encountered = Matrix4::repeat(false); + let values = -1..=1; + let rows = 0..=2; + let cols = 0..=3; + let strategy = matrix(values.clone(), rows.clone(), cols.clone()); - for _ in 0..1000 { + // Enumerate all possible combinations + let mut all_combinations = HashSet::new(); + for nrows in rows { + for ncols in cols.clone() { + // For the given number of rows and columns + let n_values = nrows * ncols; + + if n_values == 0 { + // If we have zero rows or columns, the set of matrices with the given + // rows and columns is a single element: an empty matrix + all_combinations.insert(DMatrix::from_row_slice(nrows, ncols, &[])); + } else { + // Otherwise, we need to sample all possible matrices. + // To do this, we generate the values as the (multi) Cartesian product + // of the value sets. For example, for a 2x2 matrices, we consider + // all possible 4-element arrays that the matrices can take by + // considering all elements in the cartesian product + // V x V x V x V + // where V is the set of eligible values, e.g. V := -1 ..= 1 + for matrix_values in repeat(values.clone()).take(n_values).multi_cartesian_product() { + all_combinations.insert(DMatrix::from_row_slice(nrows, ncols, &matrix_values)); + } + } + } + } + + let mut visited_combinations = HashSet::new(); + for _ in 0..num_generated_matrices { let tree = strategy .new_tree(&mut runner) .expect("Tree generation should not fail"); let matrix = tree.current(); - - dimensions_encountered[(matrix.nrows(), matrix.ncols())] = true; - - for &value in matrix.iter() { - value_encountered[value] = true; - } + visited_combinations.insert(matrix.clone()); } - assert!( - value_encountered.iter().all(|v| *v), - "Did not sample all possible values." - ); - assert!( - dimensions_encountered.iter().all(|v| *v), - "Did not sample all possible matrix dimensions." - ); + assert_eq!(visited_combinations, all_combinations, "Did not sample all possible values."); } #[test] From f909638bf4595ce49fab41fb2be4b6877811fcee Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 17 Nov 2020 08:37:56 +0100 Subject: [PATCH 003/183] Designate exhaustive matrix proptest as slow-tests The slow-tests feature flag is intended to be used for tests that take substantially more time to run than other unit tests. --- .circleci/config.yml | 4 ++-- Cargo.toml | 3 +++ tests/proptest/mod.rs | 11 ++++++++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c947dbd7..033bc01a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -57,10 +57,10 @@ jobs: - checkout - run: name: test - command: cargo test --features arbitrary --features serde-serialize --features abomonation-serialize --features sparse --features debug --features io --features compare --features libm --features proptest + command: cargo test --features arbitrary --features serde-serialize --features abomonation-serialize --features sparse --features debug --features io --features compare --features libm --features proptest --features slow-tests - run: name: test nalgebra-glm - command: cargo test -p nalgebra-glm --features arbitrary --features serde-serialize --features abomonation-serialize --features sparse --features debug --features io --features compare --features libm + command: cargo test -p nalgebra-glm --features arbitrary --features serde-serialize --features abomonation-serialize --features sparse --features debug --features io --features compare --features libm --features slow-tests build-wasm: executor: rust-executor steps: diff --git a/Cargo.toml b/Cargo.toml index a74d39cf..b0fe191e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,9 @@ compare = [ "matrixcompare-core" ] libm = [ "simba/libm" ] libm-force = [ "simba/libm_force" ] +# This feature is only used for tests, and enables tests that require more time to run +slow-tests = [] + [dependencies] typenum = "1.12" diff --git a/tests/proptest/mod.rs b/tests/proptest/mod.rs index 40d61864..306dd507 100644 --- a/tests/proptest/mod.rs +++ b/tests/proptest/mod.rs @@ -5,9 +5,13 @@ use nalgebra::{DMatrix, DVector, Dim, Matrix3, MatrixMN, Vector3}; use proptest::prelude::*; use proptest::strategy::ValueTree; use proptest::test_runner::TestRunner; -use itertools::Itertools; -use std::iter::repeat; -use std::collections::HashSet; + +#[cfg(feature = "slow-tests")] +use { + itertools::Itertools, + std::iter::repeat, + std::collections::HashSet, +}; /// Generate a proptest that tests that all matrices generated with the /// provided rows and columns conform to the constraints defined by the @@ -92,6 +96,7 @@ proptest! { fn ensure_arbitrary_test_compiles_dvector(_: DVector) {} } +#[cfg(feature = "slow-tests")] #[test] fn matrix_samples_all_possible_outputs() { // Test that the proptest generation covers all possible outputs for a small space of inputs From 402de4d045b9ff049b56836abc494d042a10b2a5 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 23 Nov 2020 13:41:10 +0100 Subject: [PATCH 004/183] Move nalgebra proptest slow tests into `slow` submodule This way it's easier to keep track of what imports are only necessary for the slow tests. --- tests/proptest/mod.rs | 137 +++++++++++++++++++++++------------------- 1 file changed, 74 insertions(+), 63 deletions(-) diff --git a/tests/proptest/mod.rs b/tests/proptest/mod.rs index 306dd507..fbe14ee7 100644 --- a/tests/proptest/mod.rs +++ b/tests/proptest/mod.rs @@ -6,13 +6,6 @@ use proptest::prelude::*; use proptest::strategy::ValueTree; use proptest::test_runner::TestRunner; -#[cfg(feature = "slow-tests")] -use { - itertools::Itertools, - std::iter::repeat, - std::collections::HashSet, -}; - /// Generate a proptest that tests that all matrices generated with the /// provided rows and columns conform to the constraints defined by the /// input. @@ -96,62 +89,6 @@ proptest! { fn ensure_arbitrary_test_compiles_dvector(_: DVector) {} } -#[cfg(feature = "slow-tests")] -#[test] -fn matrix_samples_all_possible_outputs() { - // Test that the proptest generation covers all possible outputs for a small space of inputs - // given enough samples. - - // We use a deterministic test runner to make the test "stable". - let mut runner = TestRunner::deterministic(); - - // This number needs to be high enough so that we with high probability sample - // all possible cases - let num_generated_matrices = 200000; - - let values = -1..=1; - let rows = 0..=2; - let cols = 0..=3; - let strategy = matrix(values.clone(), rows.clone(), cols.clone()); - - // Enumerate all possible combinations - let mut all_combinations = HashSet::new(); - for nrows in rows { - for ncols in cols.clone() { - // For the given number of rows and columns - let n_values = nrows * ncols; - - if n_values == 0 { - // If we have zero rows or columns, the set of matrices with the given - // rows and columns is a single element: an empty matrix - all_combinations.insert(DMatrix::from_row_slice(nrows, ncols, &[])); - } else { - // Otherwise, we need to sample all possible matrices. - // To do this, we generate the values as the (multi) Cartesian product - // of the value sets. For example, for a 2x2 matrices, we consider - // all possible 4-element arrays that the matrices can take by - // considering all elements in the cartesian product - // V x V x V x V - // where V is the set of eligible values, e.g. V := -1 ..= 1 - for matrix_values in repeat(values.clone()).take(n_values).multi_cartesian_product() { - all_combinations.insert(DMatrix::from_row_slice(nrows, ncols, &matrix_values)); - } - } - } - } - - let mut visited_combinations = HashSet::new(); - for _ in 0..num_generated_matrices { - let tree = strategy - .new_tree(&mut runner) - .expect("Tree generation should not fail"); - let matrix = tree.current(); - visited_combinations.insert(matrix.clone()); - } - - assert_eq!(visited_combinations, all_combinations, "Did not sample all possible values."); -} - #[test] fn matrix_shrinking_satisfies_constraints() { // We use a deterministic test runner to make the test "stable". @@ -206,3 +143,77 @@ fn matrix_shrinking_satisfies_constraints() { maybeprintln!("========================== (end of generation process)"); } + +#[cfg(feature = "slow-tests")] +mod slow { + use super::*; + use itertools::Itertools; + use std::collections::HashSet; + use std::iter::repeat; + + #[cfg(feature = "slow-tests")] + #[test] + fn matrix_samples_all_possible_outputs() { + // Test that the proptest generation covers all possible outputs for a small space of inputs + // given enough samples. + + // We use a deterministic test runner to make the test "stable". + let mut runner = TestRunner::deterministic(); + + // This number needs to be high enough so that we with high probability sample + // all possible cases + let num_generated_matrices = 200000; + + let values = -1..=1; + let rows = 0..=2; + let cols = 0..=3; + let strategy = matrix(values.clone(), rows.clone(), cols.clone()); + + // Enumerate all possible combinations + let mut all_combinations = HashSet::new(); + for nrows in rows { + for ncols in cols.clone() { + // For the given number of rows and columns + let n_values = nrows * ncols; + + if n_values == 0 { + // If we have zero rows or columns, the set of matrices with the given + // rows and columns is a single element: an empty matrix + all_combinations.insert(DMatrix::from_row_slice(nrows, ncols, &[])); + } else { + // Otherwise, we need to sample all possible matrices. + // To do this, we generate the values as the (multi) Cartesian product + // of the value sets. For example, for a 2x2 matrices, we consider + // all possible 4-element arrays that the matrices can take by + // considering all elements in the cartesian product + // V x V x V x V + // where V is the set of eligible values, e.g. V := -1 ..= 1 + for matrix_values in repeat(values.clone()) + .take(n_values) + .multi_cartesian_product() + { + all_combinations.insert(DMatrix::from_row_slice( + nrows, + ncols, + &matrix_values, + )); + } + } + } + } + + let mut visited_combinations = HashSet::new(); + for _ in 0..num_generated_matrices { + let tree = strategy + .new_tree(&mut runner) + .expect("Tree generation should not fail"); + let matrix = tree.current(); + visited_combinations.insert(matrix.clone()); + } + + assert_eq!( + visited_combinations, all_combinations, + "Did not sample all possible values." + ); + } +} From 561501a08f2b1d5331869abfb4615700884d03f8 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 26 Jan 2021 08:48:29 +0100 Subject: [PATCH 005/183] Upgrade nalgebra matrixcompare version to 0.2.0 (dev-dep) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b0fe191e..c99ed83b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,7 +70,7 @@ rand_isaac = "0.2" #criterion = "0.2.10" # For matrix comparison macro -matrixcompare = "0.1.3" +matrixcompare = "0.2.0" # Make sure that we use a specific version of proptest for tests. The reason is that we use a deterministic # RNG for certain tests. However, different versions of proptest may give different sequences of numbers, From b2dbcf3168555e0e12656c3094ab22e10dd7a77a Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Wed, 20 Jan 2021 17:42:25 +0100 Subject: [PATCH 006/183] Add D=Dynamic default and ::to_range_inclusive for DimRange (nalgebra) --- src/proptest/mod.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/proptest/mod.rs b/src/proptest/mod.rs index 093e1525..56c0f9cf 100644 --- a/src/proptest/mod.rs +++ b/src/proptest/mod.rs @@ -169,7 +169,7 @@ pub struct MatrixParameters { /// ranges such as `5 ..= 6`. The latter example corresponds to dimensions from (inclusive) /// `Dynamic::new(5)` to `Dynamic::new(6)` (inclusive). #[derive(Debug, Clone, PartialEq, Eq)] -pub struct DimRange(RangeInclusive); +pub struct DimRange(RangeInclusive); impl DimRange { /// The lower bound for dimensions generated. @@ -201,6 +201,13 @@ impl From> for DimRange { } } +impl DimRange { + /// Converts the `DimRange` into an instance of `RangeInclusive`. + pub fn to_range_inclusive(&self) -> RangeInclusive { + self.lower_bound().value() ..= self.upper_bound().value() + } +} + impl From for DimRange { fn from(dim: usize) -> Self { DimRange::from(Dynamic::new(dim)) From 5dfe06897f2b2fcc64d577ffc6f83ccc8699fda1 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 26 Jan 2021 08:47:47 +0100 Subject: [PATCH 007/183] Rename nalgebra/proptest to /proptest-support This gives us some freedom in the future, in case we need additional dependencies for the proptest integration. --- .circleci/config.yml | 2 +- Cargo.toml | 3 ++- src/lib.rs | 2 +- src/proptest/mod.rs | 2 +- tests/lib.rs | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 033bc01a..6d293d37 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -57,7 +57,7 @@ jobs: - checkout - run: name: test - command: cargo test --features arbitrary --features serde-serialize --features abomonation-serialize --features sparse --features debug --features io --features compare --features libm --features proptest --features slow-tests + command: cargo test --features arbitrary --features serde-serialize --features abomonation-serialize --features sparse --features debug --features io --features compare --features libm --features proptest-support --features slow-tests - run: name: test nalgebra-glm command: cargo test -p nalgebra-glm --features arbitrary --features serde-serialize --features abomonation-serialize --features sparse --features debug --features io --features compare --features libm --features slow-tests diff --git a/Cargo.toml b/Cargo.toml index c99ed83b..2662fe2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ io = [ "pest", "pest_derive" ] compare = [ "matrixcompare-core" ] libm = [ "simba/libm" ] libm-force = [ "simba/libm_force" ] +proptest-support = [ "proptest" ] # This feature is only used for tests, and enables tests that require more time to run slow-tests = [] @@ -91,4 +92,4 @@ lto = true [package.metadata.docs.rs] # Enable certain features when building docs for docs.rs -features = [ "proptest" ] +features = [ "proptest-support", "compare" ] diff --git a/src/lib.rs b/src/lib.rs index 22f39ae7..8015e57d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -127,7 +127,7 @@ pub mod geometry; #[cfg(feature = "io")] pub mod io; pub mod linalg; -#[cfg(feature = "proptest")] +#[cfg(feature = "proptest-support")] pub mod proptest; #[cfg(feature = "sparse")] pub mod sparse; diff --git a/src/proptest/mod.rs b/src/proptest/mod.rs index 56c0f9cf..58c41b67 100644 --- a/src/proptest/mod.rs +++ b/src/proptest/mod.rs @@ -1,6 +1,6 @@ //! `proptest`-related features for `nalgebra` data structures. //! -//! **This module is only available when the `proptest` feature is enabled in `nalgebra`**. +//! **This module is only available when the `proptest-support` feature is enabled in `nalgebra`**. //! //! `proptest` is a library for *property-based testing*. While similar to QuickCheck, //! which may be more familiar to some users, it has a more sophisticated design that diff --git a/tests/lib.rs b/tests/lib.rs index 47386e33..29e6cbbe 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -20,7 +20,7 @@ mod core; mod geometry; mod linalg; -#[cfg(feature = "proptest")] +#[cfg(feature = "proptest-support")] mod proptest; //#[cfg(feature = "sparse")] From 646f62a293a878b66651c4ce4855c16237cf306b Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 26 Jan 2021 09:12:06 +0100 Subject: [PATCH 008/183] rustfmt (nalgebra) --- src/proptest/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/proptest/mod.rs b/src/proptest/mod.rs index 58c41b67..0ac2a377 100644 --- a/src/proptest/mod.rs +++ b/src/proptest/mod.rs @@ -169,7 +169,7 @@ pub struct MatrixParameters { /// ranges such as `5 ..= 6`. The latter example corresponds to dimensions from (inclusive) /// `Dynamic::new(5)` to `Dynamic::new(6)` (inclusive). #[derive(Debug, Clone, PartialEq, Eq)] -pub struct DimRange(RangeInclusive); +pub struct DimRange(RangeInclusive); impl DimRange { /// The lower bound for dimensions generated. @@ -204,7 +204,7 @@ impl From> for DimRange { impl DimRange { /// Converts the `DimRange` into an instance of `RangeInclusive`. pub fn to_range_inclusive(&self) -> RangeInclusive { - self.lower_bound().value() ..= self.upper_bound().value() + self.lower_bound().value()..=self.upper_bound().value() } } From 1dbccfeb7c3a452a78cd973627f793f7caee3f81 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 13 Jul 2020 18:44:40 +0200 Subject: [PATCH 009/183] Initial COO implementation --- Cargo.toml | 2 +- nalgebra-sparse/Cargo.toml | 9 ++ nalgebra-sparse/src/coo.rs | 202 ++++++++++++++++++++++++ nalgebra-sparse/src/lib.rs | 56 +++++++ nalgebra-sparse/src/ops.rs | 70 ++++++++ nalgebra-sparse/tests/common/mod.rs | 20 +++ nalgebra-sparse/tests/unit.rs | 5 + nalgebra-sparse/tests/unit_tests/coo.rs | 190 ++++++++++++++++++++++ nalgebra-sparse/tests/unit_tests/mod.rs | 2 + nalgebra-sparse/tests/unit_tests/ops.rs | 28 ++++ 10 files changed, 583 insertions(+), 1 deletion(-) create mode 100644 nalgebra-sparse/Cargo.toml create mode 100644 nalgebra-sparse/src/coo.rs create mode 100644 nalgebra-sparse/src/lib.rs create mode 100644 nalgebra-sparse/src/ops.rs create mode 100644 nalgebra-sparse/tests/common/mod.rs create mode 100644 nalgebra-sparse/tests/unit.rs create mode 100644 nalgebra-sparse/tests/unit_tests/coo.rs create mode 100644 nalgebra-sparse/tests/unit_tests/mod.rs create mode 100644 nalgebra-sparse/tests/unit_tests/ops.rs diff --git a/Cargo.toml b/Cargo.toml index 2662fe2c..9d689dac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,7 +80,7 @@ proptest = { version = "=0.10.1" } itertools = "0.9" [workspace] -members = [ "nalgebra-lapack", "nalgebra-glm" ] +members = [ "nalgebra-lapack", "nalgebra-glm", "nalgebra-sparse" ] [[bench]] name = "nalgebra_bench" diff --git a/nalgebra-sparse/Cargo.toml b/nalgebra-sparse/Cargo.toml new file mode 100644 index 00000000..04b5dfc9 --- /dev/null +++ b/nalgebra-sparse/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "nalgebra-sparse" +version = "0.1.0" +authors = [ "Andreas Longva", "Sébastien Crozet " ] +edition = "2018" + +[dependencies] +nalgebra = { version="0.21", path = "../" } +num-traits = { version = "0.2", default-features = false } \ No newline at end of file diff --git a/nalgebra-sparse/src/coo.rs b/nalgebra-sparse/src/coo.rs new file mode 100644 index 00000000..a1b28233 --- /dev/null +++ b/nalgebra-sparse/src/coo.rs @@ -0,0 +1,202 @@ +use crate::SparseFormatError; +use nalgebra::{ClosedAdd, DMatrix, Scalar}; +use num_traits::Zero; + +/// 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` +/// correspond to row and column indices of the entry, and `v` to the value of the entry. +/// With the rare exception of matrix-vector multiplication of certain extremely sparse matrices, +/// it is of limited use for standard matrix operations. Its main purpose is to facilitate +/// easy construction of other, more efficient matrix formats (such as CSR/COO), and the +/// conversion between different formats. +/// +/// Representation +/// -------------- +/// +/// For given dimensions `nrows` and `ncols`, the matrix is represented by three same-length +/// arrays `row_indices`, `col_indices` and `values` that constitute the coordinate triplets +/// of the matrix. The indices must be in bounds, but *duplicate entries are explicitly allowed*. +/// Upon conversion to other formats, the duplicate entries may be summed together. See the +/// documentation for the respective conversion functions. +/// +/// Example +/// ------- +/// +/// ```rust +/// # use nalgebra_sparse::CooMatrix; +/// // Create a zero matrix +/// let mut coo = CooMatrix::new(4, 4); +/// // Or initialize it with a set of triplets +/// coo = CooMatrix::try_from_triplets(4, 4, vec![1, 2], vec![0, 1], vec![3.0, 4.0]).unwrap(); +/// +/// // Push a single triplet +/// coo.push(2, 0, 1.0); +/// +/// // TODO: Convert to CSR +/// ``` +#[derive(Debug, Clone)] +pub struct CooMatrix { + nrows: usize, + ncols: usize, + row_indices: Vec, + col_indices: Vec, + values: Vec, +} + +impl CooMatrix +where + T: Scalar, +{ + /// Construct a zero COO matrix of the given dimensions. + /// + /// Specifically, the collection of triplets - corresponding to explicitly stored entries - + /// is empty, so that the matrix (implicitly) represented by the COO matrix consists of all + /// zero entries. + pub fn new(nrows: usize, ncols: usize) -> Self { + Self { + nrows, + ncols, + row_indices: Vec::new(), + col_indices: Vec::new(), + values: Vec::new(), + } + } + + /// Try to construct a COO matrix from the given dimensions and a collection of + /// (i, j, v) triplets. + /// + /// Returns an error if either row or column indices contain indices out of bounds, + /// or if the data arrays do not all have the same length. Note that the COO format + /// inherently supports duplicate entries. + pub fn try_from_triplets( + nrows: usize, + ncols: usize, + row_indices: Vec, + col_indices: Vec, + values: Vec, + ) -> Result { + if row_indices.len() != col_indices.len() { + return Err(SparseFormatError::InvalidStructure( + Box::from("Number of row and col indices must be the same.") + )); + } else if col_indices.len() != values.len() { + return Err(SparseFormatError::InvalidStructure( + Box::from("Number of col indices and values must be the same.") + )); + } + + let row_indices_in_bounds = row_indices.iter().all(|i| *i < nrows); + let col_indices_in_bounds = col_indices.iter().all(|j| *j < ncols); + + if !row_indices_in_bounds { + Err(SparseFormatError::IndexOutOfBounds(Box::from( + "Row index out of bounds.", + ))) + } else if !col_indices_in_bounds { + Err(SparseFormatError::IndexOutOfBounds(Box::from( + "Col index out of bounds.", + ))) + } else { + Ok(Self { + nrows, + ncols, + row_indices, + col_indices, + values, + }) + } + } + + /// An iterator over triplets (i, j, v). + // TODO: Consider giving the iterator a concrete type instead of impl trait...? + pub fn triplet_iter(&self) -> impl Iterator { + self.row_indices + .iter() + .zip(&self.col_indices) + .zip(&self.values) + .map(|((i, j), v)| (*i, *j, v)) + } + + /// Push a single triplet to the matrix. + /// + /// This adds the value `v` to the `i`th row and `j`th column in the matrix. + /// + /// Panics + /// ------ + /// + /// Panics if `i` or `j` is out of bounds. + #[inline(always)] + pub fn push(&mut self, i: usize, j: usize, v: T) { + assert!(i < self.nrows); + assert!(j < self.ncols); + self.row_indices.push(i); + self.col_indices.push(j); + self.values.push(v); + } + + /// The number of rows in the matrix. + #[inline(always)] + pub fn nrows(&self) -> usize { + self.nrows + } + + /// The number of columns in the matrix. + #[inline(always)] + pub fn ncols(&self) -> usize { + self.ncols + } + + /// The row indices of the explicitly stored entries. + pub fn row_indices(&self) -> &[usize] { + &self.row_indices + } + + /// The column indices of the explicitly stored entries. + pub fn col_indices(&self) -> &[usize] { + &self.col_indices + } + + /// The values of the explicitly stored entries. + pub fn values(&self) -> &[T] { + &self.values + } + + /// Disassembles the matrix into individual triplet arrays. + /// + /// Examples + /// -------- + /// + /// ``` + /// # use nalgebra_sparse::CooMatrix; + /// let row_indices = vec![0, 1]; + /// let col_indices = vec![1, 2]; + /// let values = vec![1.0, 2.0]; + /// let coo = CooMatrix::try_from_triplets(2, 3, row_indices, col_indices, values) + /// .unwrap(); + /// + /// let (row_idx, col_idx, val) = coo.disassemble(); + /// assert_eq!(row_idx, vec![0, 1]); + /// assert_eq!(col_idx, vec![1, 2]); + /// assert_eq!(val, vec![1.0, 2.0]); + /// ``` + pub fn disassemble(self) -> (Vec, Vec, Vec) { + (self.row_indices, self.col_indices, self.values) + } + + /// Construct the dense representation of the COO matrix. + /// + /// Duplicate entries are summed together. + pub fn to_dense(&self) -> DMatrix + where + T: ClosedAdd + Zero, + { + let mut result = DMatrix::zeros(self.nrows, self.ncols); + + for (i, j, v) in self.triplet_iter() { + result[(i, j)] += v.clone(); + } + + result + } +} diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs new file mode 100644 index 00000000..9967e94a --- /dev/null +++ b/nalgebra-sparse/src/lib.rs @@ -0,0 +1,56 @@ +mod coo; +mod csr; +mod pattern; + +pub mod ops; + +pub use coo::CooMatrix; +pub use csr::CsrMatrix; +pub use pattern::{SparsityPattern}; + +/// Iterator types for matrices. +/// +/// Most users will not need to interface with these types directly. Instead, refer to the +/// iterator methods for the respective matrix formats. +pub mod iter { + // Iterators are best implemented in the same modules as the matrices they iterate over, + // since they are so closely tied to their respective implementations. However, + // in the crate's public API we move them into a separate `iter` module in order to avoid + // cluttering the docs with iterator types that most users will never need to explicitly + // know about. + pub use crate::pattern::SparsityPatternIter; + pub use crate::csr::{CsrTripletIter, CsrTripletIterMut}; +} + +use std::error::Error; +use std::fmt; + +#[derive(Debug)] +#[non_exhaustive] +pub enum SparseFormatError { + /// Indicates that the index data associated with the format contains at least one index + /// out of bounds. + IndexOutOfBounds(Box), + + /// Indicates that the provided data contains at least one duplicate entry, and the + /// current format does not support duplicate entries. + DuplicateEntry(Box), + + /// Indicates that the provided data for the format does not conform to the high-level + /// structure of the format. + /// + /// For example, the arrays defining the format data might have incompatible sizes. + InvalidStructure(Box), +} + +impl fmt::Display for SparseFormatError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::IndexOutOfBounds(err) => err.fmt(f), + Self::DuplicateEntry(err) => err.fmt(f), + Self::InvalidStructure(err) => err.fmt(f) + } + } +} + +impl Error for SparseFormatError {} diff --git a/nalgebra-sparse/src/ops.rs b/nalgebra-sparse/src/ops.rs new file mode 100644 index 00000000..b7b2c47b --- /dev/null +++ b/nalgebra-sparse/src/ops.rs @@ -0,0 +1,70 @@ +//! Matrix operations involving sparse matrices. + +use crate::CooMatrix; +use nalgebra::base::storage::{Storage, StorageMut}; +use nalgebra::{ClosedAdd, ClosedMul, Dim, Scalar, Vector}; +use num_traits::{One, Zero}; + +/// Sparse matrix-vector multiplication `y = beta * y + alpha * A * x`. +/// +/// Computes a matrix-vector product with the COO matrix "A" and the vector `x`, storing the +/// result in `y`. +/// +/// If `beta == 0`, the elements in `y` are never read. +/// +/// Panics +/// ------ +/// +/// Panics if `y`, `a` and `x` do not have compatible dimensions. +pub fn spmv_coo( + beta: T, + y: &mut Vector, + alpha: T, + a: &CooMatrix, + x: &Vector, +) where + T: Scalar + ClosedAdd + ClosedMul + Zero + One, + YDim: Dim, + XDim: Dim, + Y: StorageMut, + X: Storage, +{ + assert_eq!( + y.len(), + a.nrows(), + "y and a must be dimensionally compatible" + ); + assert_eq!( + a.ncols(), + x.len(), + "a and x must be dimensionally compatible" + ); + + if beta == T::zero() { + // If `y` is constructed through `new_uninitialized()`, we must make sure to not read + // any of the elements in order to avoid UB, so we special case beta == 0 + // in order to ensure that we only write, not read, the elements in y. + for y_i in y.iter_mut() { + *y_i = T::zero(); + } + } else if beta != T::one() { + // Since the COO triplets have no particular structure, we cannot combine initialization + // of y with the triplet loop below, and instead have to do it in a pre-pass. + for y_i in y.iter_mut() { + *y_i *= beta.inlined_clone(); + } + } + + for (i, j, v) in a.triplet_iter() { + // TODO: We could skip bounds checks with unsafe here, since COO ensures that all indices + // are in bounds and we assert on dimensions up-front. + // The compiler will not be able to elide the checks, since we're doing + // random/unpredictable access to elements in `x` and `y`. + let (alpha, v, x_j) = ( + alpha.inlined_clone(), + v.inlined_clone(), + x[j].inlined_clone(), + ); + y[i] += alpha * v * x_j; + } +} diff --git a/nalgebra-sparse/tests/common/mod.rs b/nalgebra-sparse/tests/common/mod.rs new file mode 100644 index 00000000..bb77a10a --- /dev/null +++ b/nalgebra-sparse/tests/common/mod.rs @@ -0,0 +1,20 @@ +#[macro_export] +macro_rules! assert_panics { + ($e:expr) => {{ + use std::panic::{catch_unwind}; + use std::stringify; + let expr_string = stringify!($e); + + // Note: We cannot manipulate the panic hook here, because it is global and the test + // suite is run in parallel, which leads to race conditions in the sense + // that some regular tests that panic might not output anything anymore. + // Unfortunately this means that output is still printed to stdout if + // we run cargo test -- --nocapture. But Cargo does not forward this if the test + // binary is not run with nocapture, so it is somewhat acceptable nonetheless. + + let result = catch_unwind(|| $e); + if result.is_ok() { + panic!("assert_panics!({}) failed: the expression did not panic.", expr_string); + } + }}; +} \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit.rs b/nalgebra-sparse/tests/unit.rs new file mode 100644 index 00000000..ee44e73b --- /dev/null +++ b/nalgebra-sparse/tests/unit.rs @@ -0,0 +1,5 @@ +//! Unit tests +mod unit_tests; + +#[macro_use] +pub mod common; \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit_tests/coo.rs b/nalgebra-sparse/tests/unit_tests/coo.rs new file mode 100644 index 00000000..b7504d51 --- /dev/null +++ b/nalgebra-sparse/tests/unit_tests/coo.rs @@ -0,0 +1,190 @@ +use nalgebra_sparse::{CooMatrix, SparsePatternError}; +use nalgebra::DMatrix; +use crate::assert_panics; + +#[test] +fn coo_construction_for_valid_data() { + // Test that construction with try_from_triplets succeeds, that the state of the + // matrix afterwards is as expected, and that the dense representation matches expectations. + + { + // Zero matrix + let coo = CooMatrix::::try_from_triplets(3, 2, Vec::new(), Vec::new(), Vec::new()) + .unwrap(); + assert_eq!(coo.nrows(), 3); + assert_eq!(coo.ncols(), 2); + assert!(coo.triplet_iter().next().is_none()); + assert!(coo.row_indices().is_empty()); + assert!(coo.col_indices().is_empty()); + assert!(coo.values().is_empty()); + + assert_eq!(coo.to_dense(), DMatrix::repeat(3, 2, 0)); + } + + { + // Arbitrary 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 coo = CooMatrix::::try_from_triplets(3, 5, i.clone(), j.clone(), v.clone()) + .unwrap(); + assert_eq!(coo.nrows(), 3); + assert_eq!(coo.ncols(), 5); + + assert_eq!(i.as_slice(), coo.row_indices()); + assert_eq!(j.as_slice(), coo.col_indices()); + assert_eq!(v.as_slice(), coo.values()); + + let expected_triplets: Vec<_> = i + .iter() + .zip(&j) + .zip(&v) + .map(|((i, j), v)| (*i, *j, *v)) + .collect(); + let actual_triplets: Vec<_> = coo.triplet_iter().map(|(i, j, v)| (i, j, *v)).collect(); + assert_eq!(actual_triplets, expected_triplets); + + #[rustfmt::skip] + let expected_dense = DMatrix::from_row_slice(3, 5, &[ + 2, 7, 0, 3, 0, + 0, 0, 3, 0, 0, + 0, 0, 0, 1, 0 + ]); + assert_eq!(coo.to_dense(), expected_dense); + } + + { + // 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::::try_from_triplets(3, 5, i.clone(), j.clone(), v.clone()) + .unwrap(); + assert_eq!(coo.nrows(), 3); + assert_eq!(coo.ncols(), 5); + + assert_eq!(i.as_slice(), coo.row_indices()); + assert_eq!(j.as_slice(), coo.col_indices()); + assert_eq!(v.as_slice(), coo.values()); + + let expected_triplets: Vec<_> = i + .iter() + .zip(&j) + .zip(&v) + .map(|((i, j), v)| (*i, *j, *v)) + .collect(); + let actual_triplets: Vec<_> = coo.triplet_iter().map(|(i, j, v)| (i, j, *v)).collect(); + assert_eq!(actual_triplets, expected_triplets); + + #[rustfmt::skip] + let expected_dense = DMatrix::from_row_slice(3, 5, &[ + 7, 7, 0, 3, 0, + 0, 0, 8, 0, 0, + 0, 0, 0, 1, 0 + ]); + assert_eq!(coo.to_dense(), expected_dense); + } +} + +#[test] +fn coo_try_from_triplets_reports_out_of_bounds_indices() { + { + // 0x0 matrix + let result = CooMatrix::::try_from_triplets(0, 0, vec![0], vec![0], vec![2]); + assert!(matches!(result, Err(SparsePatternError::IndexOutOfBounds(_)))); + } + + { + // 1x1 matrix, row out of bounds + let result = CooMatrix::::try_from_triplets(1, 1, vec![1], vec![0], vec![2]); + assert!(matches!(result, Err(SparsePatternError::IndexOutOfBounds(_)))); + } + + { + // 1x1 matrix, col out of bounds + let result = CooMatrix::::try_from_triplets(1, 1, vec![0], vec![1], vec![2]); + assert!(matches!(result, Err(SparsePatternError::IndexOutOfBounds(_)))); + } + + { + // 1x1 matrix, row and col out of bounds + let result = CooMatrix::::try_from_triplets(1, 1, vec![1], vec![1], vec![2]); + assert!(matches!(result, Err(SparsePatternError::IndexOutOfBounds(_)))); + } + + { + // Arbitrary matrix, row out of bounds + let i = vec![0, 1, 0, 3, 2]; + let j = vec![0, 2, 1, 3, 3]; + let v = vec![2, 3, 7, 3, 1]; + let result = CooMatrix::::try_from_triplets(3, 5, i, j, v); + assert!(matches!(result, Err(SparsePatternError::IndexOutOfBounds(_)))); + } + + { + // Arbitrary matrix, col out of bounds + let i = vec![0, 1, 0, 0, 2]; + let j = vec![0, 2, 1, 5, 3]; + let v = vec![2, 3, 7, 3, 1]; + let result = CooMatrix::::try_from_triplets(3, 5, i, j, v); + assert!(matches!(result, Err(SparsePatternError::IndexOutOfBounds(_)))); + } +} + +#[test] +fn coo_try_from_triplets_panics_on_mismatched_vectors() { + // Check that try_from_triplets panics when the triplet vectors have different lengths + macro_rules! assert_errs { + ($result:expr) => { + assert!(matches!($result, Err(SparseFormatError::InvalidStructure(_)))) + } + } + + assert_errs!(CooMatrix::::try_from_triplets(3, 5, vec![1, 2], vec![0], vec![0])); + assert_errs!(CooMatrix::::try_from_triplets(3, 5, vec![1], vec![0, 0], vec![0])); + assert_errs!(CooMatrix::::try_from_triplets(3, 5, vec![1], vec![0], vec![0, 1])); + assert_errs!(CooMatrix::::try_from_triplets(3, 5, vec![1, 2], vec![0, 1], vec![0])); + assert_errs!(CooMatrix::::try_from_triplets(3, 5, vec![1], vec![0, 1], vec![0, 1])); + assert_errs!(CooMatrix::::try_from_triplets(3, 5, vec![1, 1], vec![0], vec![0, 1])); +} + +#[test] +fn coo_push_valid_entries() { + let mut coo = CooMatrix::new(3, 3); + + coo.push(0, 0, 1); + assert_eq!(coo.triplet_iter().collect::>(), vec![(0, 0, &1)]); + + coo.push(0, 0, 2); + assert_eq!(coo.triplet_iter().collect::>(), vec![(0, 0, &1), (0, 0, &2)]); + + coo.push(2, 2, 3); + assert_eq!(coo.triplet_iter().collect::>(), vec![(0, 0, &1), (0, 0, &2), (2, 2, &3)]); +} + +#[test] +fn coo_push_out_of_bounds_entries() { + { + // 0x0 matrix + let coo = CooMatrix::new(0, 0); + assert_panics!(coo.clone().push(0, 0, 1)); + } + + { + // 0x1 matrix + assert_panics!(CooMatrix::new(0, 1).push(0, 0, 1)); + } + + { + // 1x0 matrix + assert_panics!(CooMatrix::new(1, 0).push(0, 0, 1)); + } + + { + // Arbitrary matrix dimensions + let coo = CooMatrix::new(3, 2); + assert_panics!(coo.clone().push(3, 0, 1)); + assert_panics!(coo.clone().push(2, 2, 1)); + assert_panics!(coo.clone().push(3, 2, 1)); + } +} \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit_tests/mod.rs b/nalgebra-sparse/tests/unit_tests/mod.rs new file mode 100644 index 00000000..0c1631c4 --- /dev/null +++ b/nalgebra-sparse/tests/unit_tests/mod.rs @@ -0,0 +1,2 @@ +mod coo; +mod ops; \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs new file mode 100644 index 00000000..c4b86c38 --- /dev/null +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -0,0 +1,28 @@ +use nalgebra_sparse::CooMatrix; +use nalgebra_sparse::ops::spmv_coo; +use nalgebra::DVector; + +#[test] +fn spmv_coo_agrees_with_dense_gemv() { + let x = DVector::from_column_slice(&[2, 3, 4, 5]); + + let i = vec![0, 0, 1, 1, 2, 2]; + let j = vec![0, 3, 0, 1, 1, 3]; + let v = vec![3, 2, 1, 2, 3, 1]; + let a = CooMatrix::try_from_triplets(3, 4, i, j, v).unwrap(); + + let betas = [0, 1, 2]; + let alphas = [0, 1, 2]; + + for &beta in &betas { + for &alpha in &alphas { + let mut y = DVector::from_column_slice(&[2, 5, 3]); + let mut y_dense = y.clone(); + spmv_coo(beta, &mut y, alpha, &a, &x); + + y_dense.gemv(alpha, &a.to_dense(), &x, beta); + + assert_eq!(y, y_dense); + } + } +} \ No newline at end of file From b0ffd559628d82d98a19127d1be23ce62962d0f7 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Fri, 17 Jul 2020 09:52:09 +0200 Subject: [PATCH 010/183] Initial CSR and SparsityPattern impls (WIP) --- nalgebra-sparse/src/csr.rs | 195 ++++++++++++++++++++++++ nalgebra-sparse/src/pattern.rs | 167 ++++++++++++++++++++ nalgebra-sparse/tests/unit_tests/coo.rs | 14 +- 3 files changed, 369 insertions(+), 7 deletions(-) create mode 100644 nalgebra-sparse/src/csr.rs create mode 100644 nalgebra-sparse/src/pattern.rs diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs new file mode 100644 index 00000000..139c3499 --- /dev/null +++ b/nalgebra-sparse/src/csr.rs @@ -0,0 +1,195 @@ +use crate::{SparsityPattern, SparseFormatError}; +use crate::iter::SparsityPatternIter; + +use std::sync::Arc; +use std::slice::{IterMut, Iter}; + +/// A CSR representation of a sparse matrix. +/// +/// The Compressed Row Storage (CSR) format is well-suited as a general-purpose storage format +/// for many sparse matrix applications. +/// +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CsrMatrix { + // Rows are major, cols are minor in the sparsity pattern + sparsity_pattern: Arc, + values: Vec, +} + +impl CsrMatrix { + /// Create a zero CSR matrix with no explicitly stored entries. + pub fn new(nrows: usize, ncols: usize) -> Self { + Self { + sparsity_pattern: Arc::new(SparsityPattern::new(nrows, ncols)), + values: vec![], + } + } + + /// The number of rows in the matrix. + #[inline(always)] + pub fn nrows(&self) -> usize { + self.sparsity_pattern.major_dim() + } + + /// The number of columns in the matrix. + #[inline(always)] + pub fn ncols(&self) -> usize { + self.sparsity_pattern.minor_dim() + } + + /// The number of non-zeros in the matrix. + /// + /// Note that this corresponds to the number of explicitly stored entries, *not* the actual + /// number of algebraically zero entries in the matrix. Explicitly stored entries can still + /// be zero. Corresponds to the number of entries in the sparsity pattern. + #[inline(always)] + pub fn nnz(&self) -> usize { + self.sparsity_pattern.nnz() + } + + /// The row offsets defining part of the CSR format. + #[inline(always)] + pub fn row_offsets(&self) -> &[usize] { + self.sparsity_pattern.major_offsets() + } + + /// The column indices defining part of the CSR format. + #[inline(always)] + pub fn column_indices(&self) -> &[usize] { + self.sparsity_pattern.minor_indices() + } + + /// The non-zero values defining part of the CSR format. + #[inline(always)] + pub fn values(&self) -> &[T] { + &self.values + } + + /// Mutable access to the non-zero values. + #[inline(always)] + pub fn values_mut(&mut self) -> &mut [T] { + &mut self.values + } + + /// Try to construct a CSR matrix from raw CSR data. + /// + /// It is assumed that each row contains unique and sorted column indices that are in + /// bounds with respect to the number of columns in the matrix. If this is not the case, + /// an error is returned to indicate the failure. + /// + /// Panics + /// ------ + /// Panics if the lengths of the provided arrays are not compatible with the CSR format. + pub fn try_from_csr_data( + num_rows: usize, + num_cols: usize, + row_offsets: Vec, + col_indices: Vec, + values: Vec, + ) -> Result { + assert_eq!(col_indices.len(), values.len(), + "Number of values and column indices must be the same"); + let pattern = SparsityPattern::try_from_offsets_and_indices( + num_rows, num_cols, row_offsets, col_indices)?; + Ok(Self { + sparsity_pattern: Arc::new(pattern), + values, + }) + } + + /// An iterator over non-zero triplets (i, j, v). + /// + /// The iteration happens in row-major fashion, meaning that i increases monotonically, + /// and j increases monotonically within each row. + /// + /// Examples + /// -------- + /// ``` + /// # use nalgebra_sparse::CsrMatrix; + /// let row_offsets = vec![0, 2, 3, 4]; + /// let col_indices = vec![0, 2, 1, 0]; + /// let values = vec![1, 2, 3, 4]; + /// let mut csr = CsrMatrix::try_from_csr_data(3, 4, row_offsets, col_indices, values) + /// .unwrap(); + /// + /// let triplets: Vec<_> = csr.triplet_iter().map(|(i, j, v)| (i, j, *v)).collect(); + /// assert_eq!(triplets, vec![(0, 0, 1), (0, 2, 2), (1, 1, 3), (2, 0, 4)]); + /// ``` + pub fn triplet_iter(&self) -> CsrTripletIter { + CsrTripletIter { + pattern_iter: self.sparsity_pattern.entries(), + values_iter: self.values.iter() + } + } + + /// A mutable iterator over non-zero triplets (i, j, v). + /// + /// Iteration happens in the same order as for [triplet_iter](#method.triplet_iter). + /// + /// Examples + /// -------- + /// ``` + /// # use nalgebra_sparse::CsrMatrix; + /// # let row_offsets = vec![0, 2, 3, 4]; + /// # let col_indices = vec![0, 2, 1, 0]; + /// # let values = vec![1, 2, 3, 4]; + /// // Using the same data as in the `triplet_iter` example + /// let mut csr = CsrMatrix::try_from_csr_data(3, 4, row_offsets, col_indices, values) + /// .unwrap(); + /// + /// // Zero out lower-triangular terms + /// csr.triplet_iter_mut() + /// .filter(|(i, j, _)| j < i) + /// .for_each(|(_, _, v)| *v = 0); + /// + /// let triplets: Vec<_> = csr.triplet_iter().map(|(i, j, v)| (i, j, *v)).collect(); + /// assert_eq!(triplets, vec![(0, 0, 1), (0, 2, 2), (1, 1, 3), (2, 0, 0)]); + /// ``` + pub fn triplet_iter_mut(&mut self) -> CsrTripletIterMut { + CsrTripletIterMut { + pattern_iter: self.sparsity_pattern.entries(), + values_mut_iter: self.values.iter_mut() + } + } +} + +#[derive(Debug)] +pub struct CsrTripletIter<'a, T> { + pattern_iter: SparsityPatternIter<'a>, + values_iter: Iter<'a, T> +} + +impl<'a, T> Iterator for CsrTripletIter<'a, T> { + type Item = (usize, usize, &'a T); + + fn next(&mut self) -> Option { + let next_entry = self.pattern_iter.next(); + let next_value = self.values_iter.next(); + + match (next_entry, next_value) { + (Some((i, j)), Some(v)) => Some((i, j, v)), + _ => None + } + } +} + +#[derive(Debug)] +pub struct CsrTripletIterMut<'a, T> { + pattern_iter: SparsityPatternIter<'a>, + values_mut_iter: IterMut<'a, T> +} + +impl<'a, T> Iterator for CsrTripletIterMut<'a, T> { + type Item = (usize, usize, &'a mut T); + + #[inline(always)] + fn next(&mut self) -> Option { + let next_entry = self.pattern_iter.next(); + let next_value = self.values_mut_iter.next(); + + match (next_entry, next_value) { + (Some((i, j)), Some(v)) => Some((i, j, v)), + _ => None + } + } +} \ No newline at end of file diff --git a/nalgebra-sparse/src/pattern.rs b/nalgebra-sparse/src/pattern.rs new file mode 100644 index 00000000..bb9006e6 --- /dev/null +++ b/nalgebra-sparse/src/pattern.rs @@ -0,0 +1,167 @@ +use crate::SparseFormatError; + +/// A representation of the sparsity pattern of a CSR or COO matrix. +#[derive(Debug, Clone, PartialEq, Eq)] +// TODO: Make SparsityPattern parametrized by index type +// (need a solid abstraction for index types though) +pub struct SparsityPattern { + major_offsets: Vec, + minor_indices: Vec, + minor_dim: usize, +} + +impl SparsityPattern { + /// Create a sparsity pattern of the given dimensions without explicitly stored entries. + pub fn new(major_dim: usize, minor_dim: usize) -> Self { + Self { + major_offsets: vec![0; major_dim + 1], + minor_indices: vec![], + minor_dim, + } + } + + /// The offsets for the major dimension. + #[inline(always)] + pub fn major_offsets(&self) -> &[usize] { + &self.major_offsets + } + + /// The indices for the minor dimension. + #[inline(always)] + pub fn minor_indices(&self) -> &[usize] { + &self.minor_indices + } + + /// The major dimension. + #[inline(always)] + pub fn major_dim(&self) -> usize { + assert!(self.major_offsets.len() > 0); + self.major_offsets.len() - 1 + } + + /// The minor dimension. + #[inline(always)] + pub fn minor_dim(&self) -> usize { + self.minor_dim + } + + /// The number of "non-zeros", i.e. explicitly stored entries in the pattern. + #[inline(always)] + pub fn nnz(&self) -> usize { + self.minor_indices.len() + } + + /// Get the lane at the given index. + #[inline(always)] + pub fn lane(&self, major_index: usize) -> Option<&[usize]> { + let offset_begin = *self.major_offsets().get(major_index)?; + let offset_end = *self.major_offsets().get(major_index + 1)?; + Some(&self.minor_indices()[offset_begin..offset_end]) + } + + /// Try to construct a sparsity pattern from the given dimensions, major offsets + /// and minor indices. + /// + /// Returns an error if the data does not conform to the requirements. + /// + /// TODO: Maybe we should not do any assertions in any of the construction functions + pub fn try_from_offsets_and_indices( + major_dim: usize, + minor_dim: usize, + major_offsets: Vec, + minor_indices: Vec, + ) -> Result { + assert_eq!(major_offsets.len(), major_dim + 1); + assert_eq!(*major_offsets.last().unwrap(), minor_indices.len()); + Ok(Self { + major_offsets, + minor_indices, + minor_dim, + }) + } + + /// An iterator over the explicitly stored "non-zero" entries (i, j). + /// + /// The iteration happens in a lane-major fashion, meaning that the lane index i + /// increases monotonically. and the minor index j increases monotonically within each + /// lane i. + /// + /// Examples + /// -------- + /// + /// ``` + /// # use nalgebra_sparse::{SparsityPattern}; + /// let offsets = vec![0, 2, 3, 4]; + /// let minor_indices = vec![0, 2, 1, 0]; + /// let pattern = SparsityPattern::try_from_offsets_and_indices(3, 4, offsets, minor_indices) + /// .unwrap(); + /// + /// let entries: Vec<_> = pattern.entries().collect(); + /// assert_eq!(entries, vec![(0, 0), (0, 2), (1, 1), (2, 0)]); + /// ``` + /// + pub fn entries(&self) -> SparsityPatternIter { + SparsityPatternIter::from_pattern(self) + } +} + +#[derive(Debug, Clone)] +pub struct SparsityPatternIter<'a> { + // See implementation of Iterator::next for an explanation of how these members are used + major_offsets: &'a [usize], + minor_indices: &'a [usize], + current_lane_idx: usize, + remaining_minors_in_lane: &'a [usize], +} + +impl<'a> SparsityPatternIter<'a> { + fn from_pattern(pattern: &'a SparsityPattern) -> Self { + let first_lane_end = pattern.major_offsets().get(1).unwrap_or(&0); + let minors_in_first_lane = &pattern.minor_indices()[0 .. *first_lane_end]; + Self { + major_offsets: pattern.major_offsets(), + minor_indices: pattern.minor_indices(), + current_lane_idx: 0, + remaining_minors_in_lane: minors_in_first_lane + } + } +} + +impl<'a> Iterator for SparsityPatternIter<'a> { + type Item = (usize, usize); + + #[inline(always)] + fn next(&mut self) -> Option { + // We ensure fast iteration across each lane by iteratively "draining" a slice + // corresponding to the remaining column indices in the particular lane. + // When we reach the end of this slice, we are at the end of a lane, + // and we must do some bookkeeping for preparing the iteration of the next lane + // (or stop iteration if we're through all lanes). + // This way we can avoid doing unnecessary bookkeeping on every iteration, + // instead paying a small price whenever we jump to a new lane. + if let Some(minor_idx) = self.remaining_minors_in_lane.first() { + let item = Some((self.current_lane_idx, *minor_idx)); + self.remaining_minors_in_lane = &self.remaining_minors_in_lane[1..]; + item + } else { + loop { + // Keep skipping lanes until we found a non-empty lane or there are no more lanes + if self.current_lane_idx + 2 >= self.major_offsets.len() { + // We've processed all lanes, so we're at the end of the iterator + // (note: keep in mind that offsets.len() == major_dim() + 1, hence we need +2) + return None; + } else { + // Bump lane index and check if the lane is non-empty + self.current_lane_idx += 1; + let lower = self.major_offsets[self.current_lane_idx]; + let upper = self.major_offsets[self.current_lane_idx + 1]; + if upper > lower { + self.remaining_minors_in_lane = &self.minor_indices[(lower + 1) .. upper]; + return Some((self.current_lane_idx, self.minor_indices[lower])) + } + } + } + } + } +} + diff --git a/nalgebra-sparse/tests/unit_tests/coo.rs b/nalgebra-sparse/tests/unit_tests/coo.rs index b7504d51..1ffa286b 100644 --- a/nalgebra-sparse/tests/unit_tests/coo.rs +++ b/nalgebra-sparse/tests/unit_tests/coo.rs @@ -1,4 +1,4 @@ -use nalgebra_sparse::{CooMatrix, SparsePatternError}; +use nalgebra_sparse::{CooMatrix, SparseFormatError}; use nalgebra::DMatrix; use crate::assert_panics; @@ -91,25 +91,25 @@ fn coo_try_from_triplets_reports_out_of_bounds_indices() { { // 0x0 matrix let result = CooMatrix::::try_from_triplets(0, 0, vec![0], vec![0], vec![2]); - assert!(matches!(result, Err(SparsePatternError::IndexOutOfBounds(_)))); + assert!(matches!(result, Err(SparseFormatError::IndexOutOfBounds(_)))); } { // 1x1 matrix, row out of bounds let result = CooMatrix::::try_from_triplets(1, 1, vec![1], vec![0], vec![2]); - assert!(matches!(result, Err(SparsePatternError::IndexOutOfBounds(_)))); + assert!(matches!(result, Err(SparseFormatError::IndexOutOfBounds(_)))); } { // 1x1 matrix, col out of bounds let result = CooMatrix::::try_from_triplets(1, 1, vec![0], vec![1], vec![2]); - assert!(matches!(result, Err(SparsePatternError::IndexOutOfBounds(_)))); + assert!(matches!(result, Err(SparseFormatError::IndexOutOfBounds(_)))); } { // 1x1 matrix, row and col out of bounds let result = CooMatrix::::try_from_triplets(1, 1, vec![1], vec![1], vec![2]); - assert!(matches!(result, Err(SparsePatternError::IndexOutOfBounds(_)))); + assert!(matches!(result, Err(SparseFormatError::IndexOutOfBounds(_)))); } { @@ -118,7 +118,7 @@ fn coo_try_from_triplets_reports_out_of_bounds_indices() { let j = vec![0, 2, 1, 3, 3]; let v = vec![2, 3, 7, 3, 1]; let result = CooMatrix::::try_from_triplets(3, 5, i, j, v); - assert!(matches!(result, Err(SparsePatternError::IndexOutOfBounds(_)))); + assert!(matches!(result, Err(SparseFormatError::IndexOutOfBounds(_)))); } { @@ -127,7 +127,7 @@ fn coo_try_from_triplets_reports_out_of_bounds_indices() { let j = vec![0, 2, 1, 5, 3]; let v = vec![2, 3, 7, 3, 1]; let result = CooMatrix::::try_from_triplets(3, 5, i, j, v); - assert!(matches!(result, Err(SparsePatternError::IndexOutOfBounds(_)))); + assert!(matches!(result, Err(SparseFormatError::IndexOutOfBounds(_)))); } } From 41425ae52cc296baaf5bda8d49055c8191e62c0b Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Fri, 17 Jul 2020 17:59:19 +0200 Subject: [PATCH 011/183] Use inline instead of inline(always) --- nalgebra-sparse/src/coo.rs | 6 +++--- nalgebra-sparse/src/csr.rs | 16 ++++++++-------- nalgebra-sparse/src/pattern.rs | 14 +++++++------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/nalgebra-sparse/src/coo.rs b/nalgebra-sparse/src/coo.rs index a1b28233..f7c839ab 100644 --- a/nalgebra-sparse/src/coo.rs +++ b/nalgebra-sparse/src/coo.rs @@ -126,7 +126,7 @@ where /// ------ /// /// Panics if `i` or `j` is out of bounds. - #[inline(always)] + #[inline] pub fn push(&mut self, i: usize, j: usize, v: T) { assert!(i < self.nrows); assert!(j < self.ncols); @@ -136,13 +136,13 @@ where } /// The number of rows in the matrix. - #[inline(always)] + #[inline] pub fn nrows(&self) -> usize { self.nrows } /// The number of columns in the matrix. - #[inline(always)] + #[inline] pub fn ncols(&self) -> usize { self.ncols } diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index 139c3499..6fab9212 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -26,13 +26,13 @@ impl CsrMatrix { } /// The number of rows in the matrix. - #[inline(always)] + #[inline] pub fn nrows(&self) -> usize { self.sparsity_pattern.major_dim() } /// The number of columns in the matrix. - #[inline(always)] + #[inline] pub fn ncols(&self) -> usize { self.sparsity_pattern.minor_dim() } @@ -42,31 +42,31 @@ impl CsrMatrix { /// Note that this corresponds to the number of explicitly stored entries, *not* the actual /// number of algebraically zero entries in the matrix. Explicitly stored entries can still /// be zero. Corresponds to the number of entries in the sparsity pattern. - #[inline(always)] + #[inline] pub fn nnz(&self) -> usize { self.sparsity_pattern.nnz() } /// The row offsets defining part of the CSR format. - #[inline(always)] + #[inline] pub fn row_offsets(&self) -> &[usize] { self.sparsity_pattern.major_offsets() } /// The column indices defining part of the CSR format. - #[inline(always)] + #[inline] pub fn column_indices(&self) -> &[usize] { self.sparsity_pattern.minor_indices() } /// The non-zero values defining part of the CSR format. - #[inline(always)] + #[inline] pub fn values(&self) -> &[T] { &self.values } /// Mutable access to the non-zero values. - #[inline(always)] + #[inline] pub fn values_mut(&mut self) -> &mut [T] { &mut self.values } @@ -182,7 +182,7 @@ pub struct CsrTripletIterMut<'a, T> { impl<'a, T> Iterator for CsrTripletIterMut<'a, T> { type Item = (usize, usize, &'a mut T); - #[inline(always)] + #[inline] fn next(&mut self) -> Option { let next_entry = self.pattern_iter.next(); let next_value = self.values_mut_iter.next(); diff --git a/nalgebra-sparse/src/pattern.rs b/nalgebra-sparse/src/pattern.rs index bb9006e6..91e21902 100644 --- a/nalgebra-sparse/src/pattern.rs +++ b/nalgebra-sparse/src/pattern.rs @@ -21,38 +21,38 @@ impl SparsityPattern { } /// The offsets for the major dimension. - #[inline(always)] + #[inline] pub fn major_offsets(&self) -> &[usize] { &self.major_offsets } /// The indices for the minor dimension. - #[inline(always)] + #[inline] pub fn minor_indices(&self) -> &[usize] { &self.minor_indices } /// The major dimension. - #[inline(always)] + #[inline] pub fn major_dim(&self) -> usize { assert!(self.major_offsets.len() > 0); self.major_offsets.len() - 1 } /// The minor dimension. - #[inline(always)] + #[inline] pub fn minor_dim(&self) -> usize { self.minor_dim } /// The number of "non-zeros", i.e. explicitly stored entries in the pattern. - #[inline(always)] + #[inline] pub fn nnz(&self) -> usize { self.minor_indices.len() } /// Get the lane at the given index. - #[inline(always)] + #[inline] pub fn lane(&self, major_index: usize) -> Option<&[usize]> { let offset_begin = *self.major_offsets().get(major_index)?; let offset_end = *self.major_offsets().get(major_index + 1)?; @@ -130,7 +130,7 @@ impl<'a> SparsityPatternIter<'a> { impl<'a> Iterator for SparsityPatternIter<'a> { type Item = (usize, usize); - #[inline(always)] + #[inline] fn next(&mut self) -> Option { // We ensure fast iteration across each lane by iteratively "draining" a slice // corresponding to the remaining column indices in the particular lane. From 7f5b702a49bfa08603f8a592d03f778df3c8c6c5 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 21 Jul 2020 17:39:06 +0200 Subject: [PATCH 012/183] CSR row access and iterators --- nalgebra-sparse/src/csr.rs | 246 +++++++++++++++++++++++++++++++++ nalgebra-sparse/src/lib.rs | 13 +- nalgebra-sparse/src/pattern.rs | 1 + 3 files changed, 259 insertions(+), 1 deletion(-) diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index 6fab9212..5c526fb1 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -3,12 +3,17 @@ use crate::iter::SparsityPatternIter; use std::sync::Arc; use std::slice::{IterMut, Iter}; +use std::ops::Range; +use num_traits::Zero; +use std::ptr::slice_from_raw_parts_mut; /// A CSR representation of a sparse matrix. /// /// The Compressed Row Storage (CSR) format is well-suited as a general-purpose storage format /// for many sparse matrix applications. /// +/// TODO: Storage explanation and examples +/// #[derive(Debug, Clone, PartialEq, Eq)] pub struct CsrMatrix { // Rows are major, cols are minor in the sparsity pattern @@ -151,8 +156,103 @@ impl CsrMatrix { values_mut_iter: self.values.iter_mut() } } + + /// Return the row at the given row index. + /// + /// Panics + /// ------ + /// Panics if row index is out of bounds. + #[inline] + pub fn row(&self, index: usize) -> CsrRow { + self.get_row(index) + .expect("Row index must be in bounds") + } + + /// Mutable row access for the given row index. + /// + /// Panics + /// ------ + /// Panics if row index is out of bounds. + #[inline] + pub fn row_mut(&mut self, index: usize) -> CsrRowMut { + self.get_row_mut(index) + .expect("Row index must be in bounds") + } + + /// Return the row at the given row index, or `None` if out of bounds. + #[inline] + pub fn get_row(&self, index: usize) -> Option> { + let range = self.get_index_range(index)?; + Some(CsrRow { + col_indices: &self.sparsity_pattern.minor_indices()[range.clone()], + values: &self.values[range], + ncols: self.ncols() + }) + } + + /// Mutable row access for the given row index, or `None` if out of bounds. + #[inline] + pub fn get_row_mut(&mut self, index: usize) -> Option> { + let range = self.get_index_range(index)?; + Some(CsrRowMut { + ncols: self.ncols(), + col_indices: &self.sparsity_pattern.minor_indices()[range.clone()], + values: &mut self.values[range] + }) + } + + /// Internal method for simplifying access to a row's data. + fn get_index_range(&self, row_index: usize) -> Option> { + let row_begin = *self.sparsity_pattern.major_offsets().get(row_index)?; + let row_end = *self.sparsity_pattern.major_offsets().get(row_index + 1)?; + Some(row_begin .. row_end) + } + + /// An iterator over rows in the matrix. + pub fn row_iter(&self) -> CsrRowIter { + CsrRowIter { + current_row_idx: 0, + matrix: self + } + } + + /// A mutable iterator over rows in the matrix. + pub fn row_iter_mut(&mut self) -> CsrRowIterMut { + CsrRowIterMut { + current_row_idx: 0, + pattern: &self.sparsity_pattern, + remaining_values: self.values.as_mut_ptr() + } + } } +impl CsrMatrix { + /// Return the value in the matrix at the given global row/col indices, or `None` if out of + /// bounds. + /// + /// If the indices are in bounds, but no explicitly stored entry is associated with it, + /// `T::zero()` is returned. Note that this methods offers no way of distinguishing + /// explicitly stored zero entries from zero values that are only implicitly represented. + /// + /// Each call to this function incurs the cost of a binary search among the explicitly + /// stored column entries for the given row. + #[inline] + pub fn get(&self, row_index: usize, col_index: usize) -> Option { + self.get_row(row_index)?.get(col_index) + } + + /// Same as `get`, but panics if indices are out of bounds. + /// + /// Panics + /// ------ + /// Panics if either index is out of bounds. + #[inline] + pub fn index(&self, row_index: usize, col_index: usize) -> T { + self.get(row_index, col_index).unwrap() + } +} + +/// Iterator type for iterating over triplets in a CSR matrix. #[derive(Debug)] pub struct CsrTripletIter<'a, T> { pattern_iter: SparsityPatternIter<'a>, @@ -173,6 +273,7 @@ impl<'a, T> Iterator for CsrTripletIter<'a, T> { } } +/// Iterator type for mutably iterating over triplets in a CSR matrix. #[derive(Debug)] pub struct CsrTripletIterMut<'a, T> { pattern_iter: SparsityPatternIter<'a>, @@ -192,4 +293,149 @@ impl<'a, T> Iterator for CsrTripletIterMut<'a, T> { _ => None } } +} + +/// An immutable representation of a row in a CSR matrix. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CsrRow<'a, T> { + ncols: usize, + col_indices: &'a [usize], + values: &'a [T], +} + +/// A mutable representation of a row in a CSR matrix. +/// +/// Note that only explicitly stored entries can be mutated. The sparsity pattern belonging +/// to the row cannot be modified. +#[derive(Debug, PartialEq, Eq)] +pub struct CsrRowMut<'a, T> { + ncols: usize, + col_indices: &'a [usize], + values: &'a mut [T] +} + +/// Implement the methods common to both CsrRow and CsrRowMut +macro_rules! impl_csr_row_common_methods { + ($name:ty) => { + impl<'a, T> $name { + /// The number of global columns in the row. + #[inline] + pub fn ncols(&self) -> usize { + self.ncols + } + + /// The number of non-zeros in this row. + #[inline] + pub fn nnz(&self) -> usize { + self.col_indices.len() + } + + /// The column indices corresponding to explicitly stored entries in this row. + #[inline] + pub fn col_indices(&self) -> &[usize] { + self.col_indices + } + + /// The values corresponding to explicitly stored entries in this row. + #[inline] + pub fn values(&self) -> &[T] { + self.values + } + } + + impl<'a, T: Clone + Zero> $name { + /// Return the value in the matrix at the given global column index, or `None` if out of + /// bounds. + /// + /// If the index is in bounds, but no explicitly stored entry is associated with it, + /// `T::zero()` is returned. Note that this methods offers no way of distinguishing + /// explicitly stored zero entries from zero values that are only implicitly represented. + /// + /// Each call to this function incurs the cost of a binary search among the explicitly + /// stored column entries for the current row. + pub fn get(&self, global_col_index: usize) -> Option { + let local_index = self.col_indices().binary_search(&global_col_index); + if let Ok(local_index) = local_index { + Some(self.values[local_index].clone()) + } else if global_col_index < self.ncols { + Some(T::zero()) + } else { + None + } + } + } + } +} + +impl_csr_row_common_methods!(CsrRow<'a, T>); +impl_csr_row_common_methods!(CsrRowMut<'a, T>); + +impl<'a, T> CsrRowMut<'a, T> { + /// Mutable access to the values corresponding to explicitly stored entries in this row. + pub fn values_mut(&mut self) -> &mut [T] { + self.values + } + + /// Provides simultaneous access to column indices and mutable values corresponding to the + /// explicitly stored entries in this row. + /// + /// This method primarily facilitates low-level access for methods that process data stored + /// in CSR format directly. + pub fn cols_and_values_mut(&mut self) -> (&[usize], &mut [T]) { + (self.col_indices, self.values) + } +} + +pub struct CsrRowIter<'a, T> { + // The index of the row that will be returned on the next + current_row_idx: usize, + matrix: &'a CsrMatrix +} + +impl<'a, T> Iterator for CsrRowIter<'a, T> { + type Item = CsrRow<'a, T>; + + fn next(&mut self) -> Option { + let row = self.matrix.get_row(self.current_row_idx); + self.current_row_idx += 1; + row + } +} + +pub struct CsrRowIterMut<'a, T> { + current_row_idx: usize, + pattern: &'a SparsityPattern, + remaining_values: *mut T, +} + +impl<'a, T> Iterator for CsrRowIterMut<'a, T> +where + T: 'a +{ + type Item = CsrRowMut<'a, T>; + + fn next(&mut self) -> Option { + let lane = self.pattern.lane(self.current_row_idx); + let ncols = self.pattern.minor_dim(); + + if let Some(col_indices) = lane { + let count = col_indices.len(); + + // Note: I can't think of any way to construct this iterator without unsafe. + let values_in_row; + unsafe { + values_in_row = &mut *slice_from_raw_parts_mut(self.remaining_values, count); + self.remaining_values = self.remaining_values.add(count); + } + self.current_row_idx += 1; + + Some(CsrRowMut { + ncols, + col_indices, + values: values_in_row + }) + } else { + None + } + } } \ No newline at end of file diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index 9967e94a..ce77de62 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -1,3 +1,13 @@ +//! Sparse matrices and algorithms for nalgebra. +//! +//! TODO: Docs +#![deny(non_camel_case_types)] +#![deny(unused_parens)] +#![deny(non_upper_case_globals)] +#![deny(unused_qualifications)] +#![deny(unused_results)] +#![deny(missing_docs)] + mod coo; mod csr; mod pattern; @@ -5,7 +15,7 @@ mod pattern; pub mod ops; pub use coo::CooMatrix; -pub use csr::CsrMatrix; +pub use csr::{CsrMatrix, CsrRow, CsrRowMut}; pub use pattern::{SparsityPattern}; /// Iterator types for matrices. @@ -25,6 +35,7 @@ pub mod iter { use std::error::Error; use std::fmt; +/// Errors produced by functions that expect well-formed sparse format data. #[derive(Debug)] #[non_exhaustive] pub enum SparseFormatError { diff --git a/nalgebra-sparse/src/pattern.rs b/nalgebra-sparse/src/pattern.rs index 91e21902..c64d7054 100644 --- a/nalgebra-sparse/src/pattern.rs +++ b/nalgebra-sparse/src/pattern.rs @@ -105,6 +105,7 @@ impl SparsityPattern { } } +/// Iterator type for iterating over entries in a sparsity pattern. #[derive(Debug, Clone)] pub struct SparsityPatternIter<'a> { // See implementation of Iterator::next for an explanation of how these members are used From b1199da206037aeabca8c0c62ef2684ecbf34da1 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 22 Sep 2020 10:40:12 +0200 Subject: [PATCH 013/183] Verify data validity in try_* constructors We can easily create the CSR and CSC constructors by using the SparsityPattern constructors. However, one lingering problem is giving meaningful error messages. When forwarding error messages from the SparsityPattern constructor, the error messages must be written in terms of "major" or "minor" dimensions, and using general terms like "lanes", instead of "rows" and "columns". When forwarding these messages up to CSR or CSC constructors, they are not directly meaningful to an end user. We should find a better solution to this problem, so that end users get more meaningful messages. --- nalgebra-sparse/src/csr.rs | 30 ++++++++---- nalgebra-sparse/src/pattern.rs | 87 +++++++++++++++++++++++++++++++--- 2 files changed, 102 insertions(+), 15 deletions(-) diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index 5c526fb1..9bf88f83 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -82,9 +82,8 @@ impl CsrMatrix { /// bounds with respect to the number of columns in the matrix. If this is not the case, /// an error is returned to indicate the failure. /// - /// Panics - /// ------ - /// Panics if the lengths of the provided arrays are not compatible with the CSR format. + /// An error is returned if the data given does not conform to the CSR storage format. + /// See the documentation for [CsrMatrix](struct.CsrMatrix.html) for more information. pub fn try_from_csr_data( num_rows: usize, num_cols: usize, @@ -92,16 +91,29 @@ impl CsrMatrix { col_indices: Vec, values: Vec, ) -> Result { - assert_eq!(col_indices.len(), values.len(), - "Number of values and column indices must be the same"); let pattern = SparsityPattern::try_from_offsets_and_indices( num_rows, num_cols, row_offsets, col_indices)?; - Ok(Self { - sparsity_pattern: Arc::new(pattern), - values, - }) + Self::try_from_pattern_and_values(Arc::new(pattern), values) } + /// Try to construct a CSR matrix from a sparsity pattern and associated non-zero values. + /// + /// Returns an error if the number of values does not match the number of minor indices + /// in the pattern. + pub fn try_from_pattern_and_values(pattern: Arc, values: Vec) + -> Result { + if pattern.nnz() == values.len() { + Ok(Self { + sparsity_pattern: pattern, + values, + }) + } else { + return Err(SparseFormatError::InvalidStructure( + Box::from("Number of values and column indices must be the same"))); + } + } + + /// An iterator over non-zero triplets (i, j, v). /// /// The iteration happens in row-major fashion, meaning that i increases monotonically, diff --git a/nalgebra-sparse/src/pattern.rs b/nalgebra-sparse/src/pattern.rs index c64d7054..8d36f4a0 100644 --- a/nalgebra-sparse/src/pattern.rs +++ b/nalgebra-sparse/src/pattern.rs @@ -1,6 +1,17 @@ use crate::SparseFormatError; -/// A representation of the sparsity pattern of a CSR or COO matrix. +/// A representation of the sparsity pattern of a CSR or CSC matrix. +/// +/// ## Format specification +/// +/// TODO: Write this out properly +/// +/// - offsets[0] == 0 +/// - Major offsets must be monotonically increasing +/// - major_offsets.len() == major_dim + 1 +/// - Column indices within each lane must be sorted +/// - Column indices must be in-bounds +/// - The last entry in major offsets must correspond to the number of minor indices #[derive(Debug, Clone, PartialEq, Eq)] // TODO: Make SparsityPattern parametrized by index type // (need a solid abstraction for index types though) @@ -63,16 +74,80 @@ impl SparsityPattern { /// and minor indices. /// /// Returns an error if the data does not conform to the requirements. - /// - /// TODO: Maybe we should not do any assertions in any of the construction functions pub fn try_from_offsets_and_indices( major_dim: usize, minor_dim: usize, major_offsets: Vec, minor_indices: Vec, ) -> Result { - assert_eq!(major_offsets.len(), major_dim + 1); - assert_eq!(*major_offsets.last().unwrap(), minor_indices.len()); + // TODO: If these errors are *directly* propagated to errors from e.g. + // CSR construction, the error messages will be confusing to users, + // as the error messages refer to "major" and "minor" lanes, as opposed to + // rows and columns + + if major_offsets.len() != major_dim + 1 { + return Err(SparseFormatError::InvalidStructure( + Box::from("Size of major_offsets must be equal to (major_dim + 1)"))); + } + + // Check that the first and last offsets conform to the specification + { + if *major_offsets.first().unwrap() != 0 { + return Err(SparseFormatError::InvalidStructure( + Box::from("First entry in major_offsets must always be 0.") + )); + } else if *major_offsets.last().unwrap() != minor_indices.len() { + return Err(SparseFormatError::InvalidStructure( + Box::from("Last entry in major_offsets must always be equal to minor_indices.len()") + )); + } + } + + // Test that each lane has strictly monotonically increasing minor indices, i.e. + // minor indices within a lane are sorted, unique. In addition, each minor index + // must be in bounds with respect to the minor dimension. + { + for lane_idx in 0 .. major_dim { + let range_start = major_offsets[lane_idx]; + let range_end = major_offsets[lane_idx + 1]; + + // Test that major offsets are monotonically increasing + if range_start > range_end { + return Err(SparseFormatError::InvalidStructure( + Box::from("Major offsets are not monotonically increasing.") + )); + } + + let minor_indices = &minor_indices[range_start .. range_end]; + + // We test for in-bounds, uniqueness and monotonicity at the same time + // to ensure that we only visit each minor index once + let mut iter = minor_indices.iter(); + let mut prev = None; + + while let Some(next) = iter.next().copied() { + if next > minor_dim { + return Err(SparseFormatError::IndexOutOfBounds( + Box::from("Minor index out of bounds.") + )); + } + + if let Some(prev) = prev { + if prev > next { + return Err(SparseFormatError::InvalidStructure( + Box::from("Minor indices within a lane must be monotonically increasing (sorted).") + )); + } else if prev == next { + return Err(SparseFormatError::DuplicateEntry( + Box::from("Duplicate minor entries detected.") + )); + } + } + prev = Some(next); + } + } + } + Ok(Self { major_offsets, minor_indices, @@ -83,7 +158,7 @@ impl SparsityPattern { /// An iterator over the explicitly stored "non-zero" entries (i, j). /// /// The iteration happens in a lane-major fashion, meaning that the lane index i - /// increases monotonically. and the minor index j increases monotonically within each + /// increases monotonically, and the minor index j increases monotonically within each /// lane i. /// /// Examples From 7e94a1539a6dc6a28ae59dde721ed424e7dbf82d Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 22 Sep 2020 10:51:29 +0200 Subject: [PATCH 014/183] Add an (incomplete) overview of planned functionality for nalgebra-sparse The way it is currently written is temporary, but it helps structure the work towards an initial MVP. --- nalgebra-sparse/src/lib.rs | 57 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index ce77de62..c53714dc 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -1,6 +1,63 @@ //! Sparse matrices and algorithms for nalgebra. //! //! TODO: Docs +//! +//! +//! ### Planned functionality +//! +//! Below we list desired functionality. This further needs to be refined into what is needed +//! for an initial contribution, and what can be added in future contributions. +//! +//! - Sparsity pattern type. Functionality: +//! - [x] Access to offsets, indices as slices. +//! - [x] Return number of nnz +//! - [x] Access a given lane as a slice of minor indices +//! - [x] Construct from valid offset + index data +//! - [ ] Construct from unsorted (but otherwise valid) offset + index data +//! - [x] Iterate over entries (i, j) in the pattern +//! - CSR matrix type. Functionality: +//! - [x] Access to CSR data as slices. +//! - [x] Return number of nnz +//! - [x] Access a given row, which gives convenient access to the data associated +//! with a particular row +//! - [x] Construct from valid CSR data +//! - [ ] Construct from unsorted CSR data +//! - [x] Iterate over entries (i, j, v) in the matrix (+mutable). +//! - [x] Iterate over rows in the matrix (+ mutable). +//! +//! - CSC matrix type. Functionality: +//! - Same as CSR, but with columns instead of rows. +//! - COO matrix type. Functionality: +//! - [x] Construct new "empty" COO matrix +//! - [x] Construct from triplet arrays. +//! - [x] Push new triplets to the matrix. +//! - [x] Iterate over triplets. +//! - [x] "Disassemble" the COO matrix into its underlying triplet arrays. +//! - Format conversion: +//! - [x] COO -> Dense +//! - [ ] CSR -> Dense +//! - [ ] CSC -> Dense +//! - [ ] COO -> CSR +//! - [ ] COO -> CSC +//! - [ ] CSR -> CSC +//! - [ ] CSC -> CSR +//! - [ ] CSR -> COO +//! - [ ] CSC -> COO +//! - Arithmetic. In general arithmetic is only implemented between instances of the same format, +//! or between dense and instances of a given format. For example, we do not implement +//! CSR * CSC, only CSR * CSR and CSC * CSC. +//! - CSR: +//! - [ ] Dense = CSR * Dense (the other way around is not particularly useful) +//! - [ ] CSR = CSR * CSR +//! - [ ] CSR = CSR +- CSR +//! - [ ] CSR +=/-= CSR +//! - COO: +//! - [ ] Dense = COO * Dense (sometimes useful for very sparse matrices) +//! - CSC: +//! - Same as CSR +//! - Cholesky factorization (port existing factorization from nalgebra's sparse module) +//! +//! #![deny(non_camel_case_types)] #![deny(unused_parens)] #![deny(non_upper_case_globals)] From 7a5f8ef1ea4816b2cd50d7c744deb3ac10336a91 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 22 Sep 2020 17:50:47 +0200 Subject: [PATCH 015/183] Redesign error handling for CSR and SparsityPattern construction SparsityPattern's constructor now returns a fine-grained error enum that enumerates possible errors. We use this to build a more user-friendly error when constructing CSR matrices. We also overhauled the main SparseFormatError error type by making it a struct containing a *Kind type and an underlying error that contains the message. --- nalgebra-sparse/src/coo.rs | 17 ++-- nalgebra-sparse/src/csr.rs | 44 +++++++-- nalgebra-sparse/src/lib.rs | 46 +++++++--- nalgebra-sparse/src/pattern.rs | 113 +++++++++++++++++++----- nalgebra-sparse/tests/unit_tests/coo.rs | 16 ++-- 5 files changed, 178 insertions(+), 58 deletions(-) diff --git a/nalgebra-sparse/src/coo.rs b/nalgebra-sparse/src/coo.rs index f7c839ab..80f80f32 100644 --- a/nalgebra-sparse/src/coo.rs +++ b/nalgebra-sparse/src/coo.rs @@ -76,13 +76,14 @@ where col_indices: Vec, values: Vec, ) -> Result { + use crate::SparseFormatErrorKind::*; if row_indices.len() != col_indices.len() { - return Err(SparseFormatError::InvalidStructure( - Box::from("Number of row and col indices must be the same.") + return Err(SparseFormatError::from_kind_and_msg( + InvalidStructure, "Number of row and col indices must be the same." )); } else if col_indices.len() != values.len() { - return Err(SparseFormatError::InvalidStructure( - Box::from("Number of col indices and values must be the same.") + return Err(SparseFormatError::from_kind_and_msg( + InvalidStructure, "Number of col indices and values must be the same." )); } @@ -90,13 +91,9 @@ where let col_indices_in_bounds = col_indices.iter().all(|j| *j < ncols); if !row_indices_in_bounds { - Err(SparseFormatError::IndexOutOfBounds(Box::from( - "Row index out of bounds.", - ))) + Err(SparseFormatError::from_kind_and_msg(IndexOutOfBounds, "Row index out of bounds.")) } else if !col_indices_in_bounds { - Err(SparseFormatError::IndexOutOfBounds(Box::from( - "Col index out of bounds.", - ))) + Err(SparseFormatError::from_kind_and_msg(IndexOutOfBounds, "Col index out of bounds.")) } else { Ok(Self { nrows, diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index 9bf88f83..895fc089 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -1,4 +1,4 @@ -use crate::{SparsityPattern, SparseFormatError}; +use crate::{SparsityPattern, SparseFormatError, SparsityPatternFormatError, SparseFormatErrorKind}; use crate::iter::SparsityPatternIter; use std::sync::Arc; @@ -92,7 +92,8 @@ impl CsrMatrix { values: Vec, ) -> Result { let pattern = SparsityPattern::try_from_offsets_and_indices( - num_rows, num_cols, row_offsets, col_indices)?; + num_rows, num_cols, row_offsets, col_indices) + .map_err(pattern_format_error_to_csr_error)?; Self::try_from_pattern_and_values(Arc::new(pattern), values) } @@ -108,8 +109,9 @@ impl CsrMatrix { values, }) } else { - return Err(SparseFormatError::InvalidStructure( - Box::from("Number of values and column indices must be the same"))); + Err(SparseFormatError::from_kind_and_msg( + SparseFormatErrorKind::InvalidStructure, + "Number of values and column indices must be the same")) } } @@ -264,6 +266,38 @@ impl CsrMatrix { } } +/// Convert pattern format errors into more meaningful CSR-specific errors. +/// +/// This ensures that the terminology is consistent: we are talking about rows and columns, +/// not lanes, major and minor dimensions. +fn pattern_format_error_to_csr_error(err: SparsityPatternFormatError) -> SparseFormatError { + use SparsityPatternFormatError::*; + use SparsityPatternFormatError::DuplicateEntry as PatternDuplicateEntry; + use SparseFormatError as E; + use SparseFormatErrorKind as K; + + match err { + InvalidOffsetArrayLength => E::from_kind_and_msg( + K::InvalidStructure, + "Length of row offset array is not equal to nrows + 1."), + InvalidOffsetFirstLast => E::from_kind_and_msg( + K::InvalidStructure, + "First or last row offset is inconsistent with format specification."), + NonmonotonicOffsets => E::from_kind_and_msg( + K::InvalidStructure, + "Row offsets are not monotonically increasing."), + NonmonotonicMinorIndices => E::from_kind_and_msg( + K::InvalidStructure, + "Column indices are not monotonically increasing (sorted) within each row."), + MinorIndexOutOfBounds => E::from_kind_and_msg( + K::IndexOutOfBounds, + "Column indices are out of bounds."), + PatternDuplicateEntry => E::from_kind_and_msg( + K::DuplicateEntry, + "Matrix data contains duplicate entries."), + } +} + /// Iterator type for iterating over triplets in a CSR matrix. #[derive(Debug)] pub struct CsrTripletIter<'a, T> { @@ -360,7 +394,7 @@ macro_rules! impl_csr_row_common_methods { /// bounds. /// /// If the index is in bounds, but no explicitly stored entry is associated with it, - /// `T::zero()` is returned. Note that this methods offers no way of distinguishing + /// `T::zero()` is returned. Note that this method offers no way of distinguishing /// explicitly stored zero entries from zero values that are only implicitly represented. /// /// Each call to this function incurs the cost of a binary search among the explicitly diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index c53714dc..773f5065 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -73,7 +73,7 @@ pub mod ops; pub use coo::CooMatrix; pub use csr::{CsrMatrix, CsrRow, CsrRowMut}; -pub use pattern::{SparsityPattern}; +pub use pattern::{SparsityPattern, SparsityPatternFormatError}; /// Iterator types for matrices. /// @@ -94,31 +94,53 @@ use std::fmt; /// Errors produced by functions that expect well-formed sparse format data. #[derive(Debug)] -#[non_exhaustive] -pub enum SparseFormatError { +pub struct SparseFormatError { + kind: SparseFormatErrorKind, + // Currently we only use an underlying error for generating the `Display` impl + error: Box +} + +impl SparseFormatError { + /// The type of error. + pub fn kind(&self) -> &SparseFormatErrorKind { + &self.kind + } + + pub(crate) fn from_kind_and_error(kind: SparseFormatErrorKind, error: Box) -> Self { + Self { + kind, + error + } + } + + /// Helper functionality for more conveniently creating errors. + pub(crate) fn from_kind_and_msg(kind: SparseFormatErrorKind, msg: &'static str) -> Self { + Self::from_kind_and_error(kind, Box::::from(msg)) + } +} + +/// The type of format error described by a [SparseFormatError](struct.SparseFormatError.html). +#[derive(Debug, Clone)] +pub enum SparseFormatErrorKind { /// Indicates that the index data associated with the format contains at least one index /// out of bounds. - IndexOutOfBounds(Box), + IndexOutOfBounds, /// Indicates that the provided data contains at least one duplicate entry, and the /// current format does not support duplicate entries. - DuplicateEntry(Box), + DuplicateEntry, /// Indicates that the provided data for the format does not conform to the high-level /// structure of the format. /// /// For example, the arrays defining the format data might have incompatible sizes. - InvalidStructure(Box), + InvalidStructure, } impl fmt::Display for SparseFormatError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::IndexOutOfBounds(err) => err.fmt(f), - Self::DuplicateEntry(err) => err.fmt(f), - Self::InvalidStructure(err) => err.fmt(f) - } + write!(f, "{}", self.error) } } -impl Error for SparseFormatError {} +impl Error for SparseFormatError {} \ No newline at end of file diff --git a/nalgebra-sparse/src/pattern.rs b/nalgebra-sparse/src/pattern.rs index 8d36f4a0..a93bc1ef 100644 --- a/nalgebra-sparse/src/pattern.rs +++ b/nalgebra-sparse/src/pattern.rs @@ -1,4 +1,6 @@ use crate::SparseFormatError; +use std::fmt; +use std::error::Error; /// A representation of the sparsity pattern of a CSR or CSC matrix. /// @@ -79,27 +81,24 @@ impl SparsityPattern { minor_dim: usize, major_offsets: Vec, minor_indices: Vec, - ) -> Result { + ) -> Result { // TODO: If these errors are *directly* propagated to errors from e.g. // CSR construction, the error messages will be confusing to users, // as the error messages refer to "major" and "minor" lanes, as opposed to // rows and columns + use SparsityPatternFormatError::*; + if major_offsets.len() != major_dim + 1 { - return Err(SparseFormatError::InvalidStructure( - Box::from("Size of major_offsets must be equal to (major_dim + 1)"))); + return Err(InvalidOffsetArrayLength); } // Check that the first and last offsets conform to the specification { - if *major_offsets.first().unwrap() != 0 { - return Err(SparseFormatError::InvalidStructure( - Box::from("First entry in major_offsets must always be 0.") - )); - } else if *major_offsets.last().unwrap() != minor_indices.len() { - return Err(SparseFormatError::InvalidStructure( - Box::from("Last entry in major_offsets must always be equal to minor_indices.len()") - )); + let first_offset_ok = *major_offsets.first().unwrap() == 0; + let last_offset_ok = *major_offsets.last().unwrap() == minor_indices.len(); + if !first_offset_ok || !last_offset_ok { + return Err(InvalidOffsetFirstLast); } } @@ -113,9 +112,7 @@ impl SparsityPattern { // Test that major offsets are monotonically increasing if range_start > range_end { - return Err(SparseFormatError::InvalidStructure( - Box::from("Major offsets are not monotonically increasing.") - )); + return Err(NonmonotonicOffsets); } let minor_indices = &minor_indices[range_start .. range_end]; @@ -127,20 +124,14 @@ impl SparsityPattern { while let Some(next) = iter.next().copied() { if next > minor_dim { - return Err(SparseFormatError::IndexOutOfBounds( - Box::from("Minor index out of bounds.") - )); + return Err(MinorIndexOutOfBounds); } if let Some(prev) = prev { if prev > next { - return Err(SparseFormatError::InvalidStructure( - Box::from("Minor indices within a lane must be monotonically increasing (sorted).") - )); + return Err(NonmonotonicMinorIndices); } else if prev == next { - return Err(SparseFormatError::DuplicateEntry( - Box::from("Duplicate minor entries detected.") - )); + return Err(DuplicateEntry); } } prev = Some(next); @@ -180,6 +171,82 @@ impl SparsityPattern { } } +/// Error type for `SparsityPattern` format errors. +#[non_exhaustive] +#[derive(Debug)] +pub enum SparsityPatternFormatError { + /// Indicates an invalid number of offsets. + /// + /// The number of offsets must be equal to (major_dim + 1). + InvalidOffsetArrayLength, + /// Indicates that the first or last entry in the offset array did not conform to + /// specifications. + /// + /// The first entry must be 0, and the last entry must be exactly one greater than the + /// major dimension. + InvalidOffsetFirstLast, + /// Indicates that the major offsets are not monotonically increasing. + NonmonotonicOffsets, + /// One or more minor indices are out of bounds. + MinorIndexOutOfBounds, + /// One or more duplicate entries were detected. + /// + /// Two entries are considered duplicates if they are part of the same major lane and have + /// the same minor index. + DuplicateEntry, + /// Indicates that minor indices are not monotonically increasing within each lane. + NonmonotonicMinorIndices, +} + +impl From for SparseFormatError { + fn from(err: SparsityPatternFormatError) -> Self { + use SparsityPatternFormatError::*; + use SparsityPatternFormatError::DuplicateEntry as PatternDuplicateEntry; + use crate::SparseFormatErrorKind; + use crate::SparseFormatErrorKind::*; + match err { + InvalidOffsetArrayLength + | InvalidOffsetFirstLast + | NonmonotonicOffsets + | NonmonotonicMinorIndices + => SparseFormatError::from_kind_and_error(InvalidStructure, Box::from(err)), + MinorIndexOutOfBounds + => SparseFormatError::from_kind_and_error(IndexOutOfBounds, + Box::from(err)), + PatternDuplicateEntry + => SparseFormatError::from_kind_and_error(SparseFormatErrorKind::DuplicateEntry, + Box::from(err)), + } + } +} + +impl fmt::Display for SparsityPatternFormatError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SparsityPatternFormatError::InvalidOffsetArrayLength => { + write!(f, "Length of offset array is not equal to (major_dim + 1).") + }, + SparsityPatternFormatError::InvalidOffsetFirstLast => { + write!(f, "First or last offset is incompatible with format.") + }, + SparsityPatternFormatError::NonmonotonicOffsets => { + write!(f, "Offsets are not monotonically increasing.") + }, + SparsityPatternFormatError::MinorIndexOutOfBounds => { + write!(f, "A minor index is out of bounds.") + }, + SparsityPatternFormatError::DuplicateEntry => { + write!(f, "Input data contains duplicate entries.") + }, + SparsityPatternFormatError::NonmonotonicMinorIndices => { + write!(f, "Minor indices are not monotonically increasing within each lane.") + }, + } + } +} + +impl Error for SparsityPatternFormatError {} + /// Iterator type for iterating over entries in a sparsity pattern. #[derive(Debug, Clone)] pub struct SparsityPatternIter<'a> { diff --git a/nalgebra-sparse/tests/unit_tests/coo.rs b/nalgebra-sparse/tests/unit_tests/coo.rs index 1ffa286b..21383b90 100644 --- a/nalgebra-sparse/tests/unit_tests/coo.rs +++ b/nalgebra-sparse/tests/unit_tests/coo.rs @@ -1,4 +1,4 @@ -use nalgebra_sparse::{CooMatrix, SparseFormatError}; +use nalgebra_sparse::{CooMatrix, SparseFormatErrorKind}; use nalgebra::DMatrix; use crate::assert_panics; @@ -91,25 +91,25 @@ fn coo_try_from_triplets_reports_out_of_bounds_indices() { { // 0x0 matrix let result = CooMatrix::::try_from_triplets(0, 0, vec![0], vec![0], vec![2]); - assert!(matches!(result, Err(SparseFormatError::IndexOutOfBounds(_)))); + assert!(matches!(result.unwrap_err().kind(), SparseFormatErrorKind::IndexOutOfBounds)); } { // 1x1 matrix, row out of bounds let result = CooMatrix::::try_from_triplets(1, 1, vec![1], vec![0], vec![2]); - assert!(matches!(result, Err(SparseFormatError::IndexOutOfBounds(_)))); + assert!(matches!(result.unwrap_err().kind(), SparseFormatErrorKind::IndexOutOfBounds)); } { // 1x1 matrix, col out of bounds let result = CooMatrix::::try_from_triplets(1, 1, vec![0], vec![1], vec![2]); - assert!(matches!(result, Err(SparseFormatError::IndexOutOfBounds(_)))); + assert!(matches!(result.unwrap_err().kind(), SparseFormatErrorKind::IndexOutOfBounds)); } { // 1x1 matrix, row and col out of bounds let result = CooMatrix::::try_from_triplets(1, 1, vec![1], vec![1], vec![2]); - assert!(matches!(result, Err(SparseFormatError::IndexOutOfBounds(_)))); + assert!(matches!(result.unwrap_err().kind(), SparseFormatErrorKind::IndexOutOfBounds)); } { @@ -118,7 +118,7 @@ fn coo_try_from_triplets_reports_out_of_bounds_indices() { let j = vec![0, 2, 1, 3, 3]; let v = vec![2, 3, 7, 3, 1]; let result = CooMatrix::::try_from_triplets(3, 5, i, j, v); - assert!(matches!(result, Err(SparseFormatError::IndexOutOfBounds(_)))); + assert!(matches!(result.unwrap_err().kind(), SparseFormatErrorKind::IndexOutOfBounds)); } { @@ -127,7 +127,7 @@ fn coo_try_from_triplets_reports_out_of_bounds_indices() { let j = vec![0, 2, 1, 5, 3]; let v = vec![2, 3, 7, 3, 1]; let result = CooMatrix::::try_from_triplets(3, 5, i, j, v); - assert!(matches!(result, Err(SparseFormatError::IndexOutOfBounds(_)))); + assert!(matches!(result.unwrap_err().kind(), SparseFormatErrorKind::IndexOutOfBounds)); } } @@ -136,7 +136,7 @@ fn coo_try_from_triplets_panics_on_mismatched_vectors() { // Check that try_from_triplets panics when the triplet vectors have different lengths macro_rules! assert_errs { ($result:expr) => { - assert!(matches!($result, Err(SparseFormatError::InvalidStructure(_)))) + assert!(matches!($result.unwrap_err().kind(), SparseFormatErrorKind::InvalidStructure)) } } From a15e78a6b7afe8370fea805ddf8e4fc9eb870c32 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Wed, 23 Sep 2020 09:34:19 +0200 Subject: [PATCH 016/183] Put COO, CSR, SparsityPattern and related types in their own modules This mimics how std does it, e.g. std::vec::Vec. This avoids potential problems down the road, where adding more types might clutter the API interface and generated documentation. --- nalgebra-sparse/src/coo.rs | 6 ++++-- nalgebra-sparse/src/csr.rs | 12 ++++++++---- nalgebra-sparse/src/lib.rs | 25 +++---------------------- nalgebra-sparse/src/ops.rs | 2 +- nalgebra-sparse/src/pattern.rs | 3 ++- nalgebra-sparse/tests/unit_tests/coo.rs | 3 ++- nalgebra-sparse/tests/unit_tests/ops.rs | 2 +- 7 files changed, 21 insertions(+), 32 deletions(-) diff --git a/nalgebra-sparse/src/coo.rs b/nalgebra-sparse/src/coo.rs index 80f80f32..a22f65cd 100644 --- a/nalgebra-sparse/src/coo.rs +++ b/nalgebra-sparse/src/coo.rs @@ -1,3 +1,5 @@ +//! An implementation of the COO sparse matrix format. + use crate::SparseFormatError; use nalgebra::{ClosedAdd, DMatrix, Scalar}; use num_traits::Zero; @@ -24,7 +26,7 @@ use num_traits::Zero; /// ------- /// /// ```rust -/// # use nalgebra_sparse::CooMatrix; +/// # use nalgebra_sparse::coo::CooMatrix; /// // Create a zero matrix /// let mut coo = CooMatrix::new(4, 4); /// // Or initialize it with a set of triplets @@ -165,7 +167,7 @@ where /// -------- /// /// ``` - /// # use nalgebra_sparse::CooMatrix; + /// # use nalgebra_sparse::coo::CooMatrix; /// let row_indices = vec![0, 1]; /// let col_indices = vec![1, 2]; /// let values = vec![1.0, 2.0]; diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index 895fc089..e8d7d447 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -1,5 +1,7 @@ -use crate::{SparsityPattern, SparseFormatError, SparsityPatternFormatError, SparseFormatErrorKind}; -use crate::iter::SparsityPatternIter; +//! An implementation of the CSR sparse matrix format. + +use crate::{SparseFormatError, SparseFormatErrorKind}; +use crate::pattern::{SparsityPattern, SparsityPatternFormatError, SparsityPatternIter}; use std::sync::Arc; use std::slice::{IterMut, Iter}; @@ -124,7 +126,7 @@ impl CsrMatrix { /// Examples /// -------- /// ``` - /// # use nalgebra_sparse::CsrMatrix; + /// # use nalgebra_sparse::csr::CsrMatrix; /// let row_offsets = vec![0, 2, 3, 4]; /// let col_indices = vec![0, 2, 1, 0]; /// let values = vec![1, 2, 3, 4]; @@ -148,7 +150,7 @@ impl CsrMatrix { /// Examples /// -------- /// ``` - /// # use nalgebra_sparse::CsrMatrix; + /// # use nalgebra_sparse::csr::CsrMatrix; /// # let row_offsets = vec![0, 2, 3, 4]; /// # let col_indices = vec![0, 2, 1, 0]; /// # let values = vec![1, 2, 3, 4]; @@ -432,6 +434,7 @@ impl<'a, T> CsrRowMut<'a, T> { } } +/// Row iterator for [CsrMatrix](struct.CsrMatrix.html). pub struct CsrRowIter<'a, T> { // The index of the row that will be returned on the next current_row_idx: usize, @@ -448,6 +451,7 @@ impl<'a, T> Iterator for CsrRowIter<'a, T> { } } +/// Mutable row iterator for [CsrMatrix](struct.CsrMatrix.html). pub struct CsrRowIterMut<'a, T> { current_row_idx: usize, pattern: &'a SparsityPattern, diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index 773f5065..36b9bc40 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -65,30 +65,11 @@ #![deny(unused_results)] #![deny(missing_docs)] -mod coo; -mod csr; -mod pattern; - +pub mod coo; +pub mod csr; +pub mod pattern; pub mod ops; -pub use coo::CooMatrix; -pub use csr::{CsrMatrix, CsrRow, CsrRowMut}; -pub use pattern::{SparsityPattern, SparsityPatternFormatError}; - -/// Iterator types for matrices. -/// -/// Most users will not need to interface with these types directly. Instead, refer to the -/// iterator methods for the respective matrix formats. -pub mod iter { - // Iterators are best implemented in the same modules as the matrices they iterate over, - // since they are so closely tied to their respective implementations. However, - // in the crate's public API we move them into a separate `iter` module in order to avoid - // cluttering the docs with iterator types that most users will never need to explicitly - // know about. - pub use crate::pattern::SparsityPatternIter; - pub use crate::csr::{CsrTripletIter, CsrTripletIterMut}; -} - use std::error::Error; use std::fmt; diff --git a/nalgebra-sparse/src/ops.rs b/nalgebra-sparse/src/ops.rs index b7b2c47b..bc3b9399 100644 --- a/nalgebra-sparse/src/ops.rs +++ b/nalgebra-sparse/src/ops.rs @@ -1,6 +1,6 @@ //! Matrix operations involving sparse matrices. -use crate::CooMatrix; +use crate::coo::CooMatrix; use nalgebra::base::storage::{Storage, StorageMut}; use nalgebra::{ClosedAdd, ClosedMul, Dim, Scalar, Vector}; use num_traits::{One, Zero}; diff --git a/nalgebra-sparse/src/pattern.rs b/nalgebra-sparse/src/pattern.rs index a93bc1ef..4f78fbb4 100644 --- a/nalgebra-sparse/src/pattern.rs +++ b/nalgebra-sparse/src/pattern.rs @@ -1,3 +1,4 @@ +//! Sparsity patterns for CSR and CSC matrices. use crate::SparseFormatError; use std::fmt; use std::error::Error; @@ -156,7 +157,7 @@ impl SparsityPattern { /// -------- /// /// ``` - /// # use nalgebra_sparse::{SparsityPattern}; + /// # use nalgebra_sparse::pattern::SparsityPattern; /// let offsets = vec![0, 2, 3, 4]; /// let minor_indices = vec![0, 2, 1, 0]; /// let pattern = SparsityPattern::try_from_offsets_and_indices(3, 4, offsets, minor_indices) diff --git a/nalgebra-sparse/tests/unit_tests/coo.rs b/nalgebra-sparse/tests/unit_tests/coo.rs index 21383b90..7bf0c05c 100644 --- a/nalgebra-sparse/tests/unit_tests/coo.rs +++ b/nalgebra-sparse/tests/unit_tests/coo.rs @@ -1,4 +1,5 @@ -use nalgebra_sparse::{CooMatrix, SparseFormatErrorKind}; +use nalgebra_sparse::{SparseFormatErrorKind}; +use nalgebra_sparse::coo::CooMatrix; use nalgebra::DMatrix; use crate::assert_panics; diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index c4b86c38..a2c06b96 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -1,4 +1,4 @@ -use nalgebra_sparse::CooMatrix; +use nalgebra_sparse::coo::CooMatrix; use nalgebra_sparse::ops::spmv_coo; use nalgebra::DVector; From ff435110b97520702fb24bd94884cd7fc5e85412 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Thu, 24 Sep 2020 09:55:09 +0200 Subject: [PATCH 017/183] Implement CSR::disassemble and SparsityPattern::disassemble --- nalgebra-sparse/src/csr.rs | 57 +++++++++++++++++++++++++++++++++- nalgebra-sparse/src/lib.rs | 2 ++ nalgebra-sparse/src/pattern.rs | 23 ++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index e8d7d447..8c4be726 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -240,6 +240,61 @@ impl CsrMatrix { remaining_values: self.values.as_mut_ptr() } } + + /// Returns the underlying vector containing the values for the explicitly stored entries. + pub fn take_values(self) -> Vec { + self.values + } + + /// Disassembles the CSR matrix into its underlying offset, index and value arrays. + /// + /// If the matrix contains the sole reference to the sparsity pattern, + /// then the data is returned as-is. Otherwise, the sparsity pattern is cloned. + /// + /// Examples + /// -------- + /// + /// ``` + /// # use nalgebra_sparse::csr::CsrMatrix; + /// let row_offsets = vec![0, 2, 3, 4]; + /// let col_indices = vec![0, 2, 1, 0]; + /// let values = vec![1, 2, 3, 4]; + /// let mut csr = CsrMatrix::try_from_csr_data( + /// 3, + /// 4, + /// row_offsets.clone(), + /// col_indices.clone(), + /// values.clone()) + /// .unwrap(); + /// let (row_offsets2, col_indices2, values2) = csr.disassemble(); + /// assert_eq!(row_offsets2, row_offsets); + /// assert_eq!(col_indices2, col_indices); + /// assert_eq!(values2, values); + /// ``` + pub fn disassemble(self) -> (Vec, Vec, Vec) { + // Take an Arc to the pattern, which might be the sole reference to the data after + // taking the values. This is important, because it might let us avoid cloning the data + // further below. + let pattern = self.pattern(); + let values = self.take_values(); + + // Try to take the pattern out of the `Arc` if possible, + // otherwise clone the pattern. + let owned_pattern = Arc::try_unwrap(pattern) + .unwrap_or_else(|arc| SparsityPattern::clone(&*arc)); + let (offsets, indices) = owned_pattern.disassemble(); + + (offsets, indices, values) + } + + /// Returns the underlying sparsity pattern. + /// + /// The sparsity pattern is stored internally inside an `Arc`. This allows users to re-use + /// the same sparsity pattern for multiple matrices without storing the same pattern multiple + /// times in memory. + pub fn pattern(&self) -> Arc { + Arc::clone(&self.sparsity_pattern) + } } impl CsrMatrix { @@ -247,7 +302,7 @@ impl CsrMatrix { /// bounds. /// /// If the indices are in bounds, but no explicitly stored entry is associated with it, - /// `T::zero()` is returned. Note that this methods offers no way of distinguishing + /// `T::zero()` is returned. Note that this method offers no way of distinguishing /// explicitly stored zero entries from zero values that are only implicitly represented. /// /// Each call to this function incurs the cost of a binary search among the explicitly diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index 36b9bc40..872126a8 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -15,6 +15,7 @@ //! - [x] Construct from valid offset + index data //! - [ ] Construct from unsorted (but otherwise valid) offset + index data //! - [x] Iterate over entries (i, j) in the pattern +//! - [x] "Disassemble" the sparsity pattern into the raw index data arrays. //! - CSR matrix type. Functionality: //! - [x] Access to CSR data as slices. //! - [x] Return number of nnz @@ -24,6 +25,7 @@ //! - [ ] Construct from unsorted CSR data //! - [x] Iterate over entries (i, j, v) in the matrix (+mutable). //! - [x] Iterate over rows in the matrix (+ mutable). +//! - [x] "Disassemble" the CSR matrix into the raw CSR data arrays. //! //! - CSC matrix type. Functionality: //! - Same as CSR, but with columns instead of rows. diff --git a/nalgebra-sparse/src/pattern.rs b/nalgebra-sparse/src/pattern.rs index 4f78fbb4..54e0f2e8 100644 --- a/nalgebra-sparse/src/pattern.rs +++ b/nalgebra-sparse/src/pattern.rs @@ -170,6 +170,29 @@ impl SparsityPattern { pub fn entries(&self) -> SparsityPatternIter { SparsityPatternIter::from_pattern(self) } + + /// Returns the raw offset and index data for the sparsity pattern. + /// + /// Examples + /// -------- + /// + /// ``` + /// # use nalgebra_sparse::pattern::SparsityPattern; + /// let offsets = vec![0, 2, 3, 4]; + /// let minor_indices = vec![0, 2, 1, 0]; + /// let pattern = SparsityPattern::try_from_offsets_and_indices( + /// 3, + /// 4, + /// offsets.clone(), + /// minor_indices.clone()) + /// .unwrap(); + /// let (offsets2, minor_indices2) = pattern.disassemble(); + /// assert_eq!(offsets2, offsets); + /// assert_eq!(minor_indices2, minor_indices); + /// ``` + pub fn disassemble(self) -> (Vec, Vec) { + (self.major_offsets, self.minor_indices) + } } /// Error type for `SparsityPattern` format errors. From d98f2d2ad7a88e34950f9ad88b29e91ed165503c Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Thu, 24 Sep 2020 09:58:04 +0200 Subject: [PATCH 018/183] sparse-nalgebra: Update nalgebra version --- nalgebra-sparse/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nalgebra-sparse/Cargo.toml b/nalgebra-sparse/Cargo.toml index 04b5dfc9..99593ed7 100644 --- a/nalgebra-sparse/Cargo.toml +++ b/nalgebra-sparse/Cargo.toml @@ -5,5 +5,5 @@ authors = [ "Andreas Longva", "Sébastien Crozet " ] edition = "2018" [dependencies] -nalgebra = { version="0.21", path = "../" } +nalgebra = { version="0.22", path = "../" } num-traits = { version = "0.2", default-features = false } \ No newline at end of file From df1ef991f3e8d9a362364e2e0b25dda8dff51938 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Fri, 25 Sep 2020 14:48:10 +0200 Subject: [PATCH 019/183] Test SparsityPattern and CSR try_* constructors --- nalgebra-sparse/src/csr.rs | 4 +- nalgebra-sparse/src/lib.rs | 2 +- nalgebra-sparse/src/pattern.rs | 17 +- nalgebra-sparse/tests/unit_tests/csr.rs | 231 ++++++++++++++++++++ nalgebra-sparse/tests/unit_tests/mod.rs | 4 +- nalgebra-sparse/tests/unit_tests/pattern.rs | 130 +++++++++++ 6 files changed, 381 insertions(+), 7 deletions(-) create mode 100644 nalgebra-sparse/tests/unit_tests/csr.rs create mode 100644 nalgebra-sparse/tests/unit_tests/pattern.rs diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index 8c4be726..c612e5b4 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -62,7 +62,7 @@ impl CsrMatrix { /// The column indices defining part of the CSR format. #[inline] - pub fn column_indices(&self) -> &[usize] { + pub fn col_indices(&self) -> &[usize] { self.sparsity_pattern.minor_indices() } @@ -520,7 +520,7 @@ where type Item = CsrRowMut<'a, T>; fn next(&mut self) -> Option { - let lane = self.pattern.lane(self.current_row_idx); + let lane = self.pattern.get_lane(self.current_row_idx); let ncols = self.pattern.minor_dim(); if let Some(col_indices) = lane { diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index 872126a8..9621d973 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -103,7 +103,7 @@ impl SparseFormatError { } /// The type of format error described by a [SparseFormatError](struct.SparseFormatError.html). -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum SparseFormatErrorKind { /// Indicates that the index data associated with the format contains at least one index /// out of bounds. diff --git a/nalgebra-sparse/src/pattern.rs b/nalgebra-sparse/src/pattern.rs index 54e0f2e8..f95f23c8 100644 --- a/nalgebra-sparse/src/pattern.rs +++ b/nalgebra-sparse/src/pattern.rs @@ -66,8 +66,19 @@ impl SparsityPattern { } /// Get the lane at the given index. + /// + /// Panics + /// ------ + /// + /// Panics if `major_index` is out of bounds. #[inline] - pub fn lane(&self, major_index: usize) -> Option<&[usize]> { + pub fn lane(&self, major_index: usize) -> &[usize] { + self.get_lane(major_index).unwrap() + } + + /// Get the lane at the given index, or `None` if out of bounds. + #[inline] + pub fn get_lane(&self, major_index: usize) -> Option<&[usize]> { let offset_begin = *self.major_offsets().get(major_index)?; let offset_end = *self.major_offsets().get(major_index + 1)?; Some(&self.minor_indices()[offset_begin..offset_end]) @@ -124,7 +135,7 @@ impl SparsityPattern { let mut prev = None; while let Some(next) = iter.next().copied() { - if next > minor_dim { + if next >= minor_dim { return Err(MinorIndexOutOfBounds); } @@ -197,7 +208,7 @@ impl SparsityPattern { /// Error type for `SparsityPattern` format errors. #[non_exhaustive] -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum SparsityPatternFormatError { /// Indicates an invalid number of offsets. /// diff --git a/nalgebra-sparse/tests/unit_tests/csr.rs b/nalgebra-sparse/tests/unit_tests/csr.rs new file mode 100644 index 00000000..693e092f --- /dev/null +++ b/nalgebra-sparse/tests/unit_tests/csr.rs @@ -0,0 +1,231 @@ +use nalgebra_sparse::csr::CsrMatrix; +use nalgebra_sparse::SparseFormatErrorKind; + +#[test] +fn csr_matrix_valid_data() { + // Construct matrix from valid data and check that selected methods return results + // that agree with expectations. + + { + // A CSR matrix with zero explicitly stored entries + let offsets = vec![0, 0, 0, 0]; + let indices = vec![]; + let values = Vec::::new(); + let mut matrix = CsrMatrix::try_from_csr_data(3, 2, offsets, indices, values).unwrap(); + + assert_eq!(matrix, CsrMatrix::new(3, 2)); + + assert_eq!(matrix.nrows(), 3); + assert_eq!(matrix.ncols(), 2); + assert_eq!(matrix.nnz(), 0); + assert_eq!(matrix.row_offsets(), &[0, 0, 0, 0]); + assert_eq!(matrix.col_indices(), &[]); + assert_eq!(matrix.values(), &[]); + + assert!(matrix.triplet_iter().next().is_none()); + assert!(matrix.triplet_iter_mut().next().is_none()); + + assert_eq!(matrix.row(0).ncols(), 2); + assert_eq!(matrix.row(0).nnz(), 0); + assert_eq!(matrix.row(0).col_indices(), &[]); + assert_eq!(matrix.row(0).values(), &[]); + assert_eq!(matrix.row_mut(0).ncols(), 2); + assert_eq!(matrix.row_mut(0).nnz(), 0); + assert_eq!(matrix.row_mut(0).col_indices(), &[]); + assert_eq!(matrix.row_mut(0).values(), &[]); + assert_eq!(matrix.row_mut(0).values_mut(), &[]); + assert_eq!(matrix.row_mut(0).cols_and_values_mut(), ([].as_ref(), [].as_mut())); + + assert_eq!(matrix.row(1).ncols(), 2); + assert_eq!(matrix.row(1).nnz(), 0); + assert_eq!(matrix.row(1).col_indices(), &[]); + assert_eq!(matrix.row(1).values(), &[]); + assert_eq!(matrix.row_mut(1).ncols(), 2); + assert_eq!(matrix.row_mut(1).nnz(), 0); + assert_eq!(matrix.row_mut(1).col_indices(), &[]); + assert_eq!(matrix.row_mut(1).values(), &[]); + assert_eq!(matrix.row_mut(1).values_mut(), &[]); + assert_eq!(matrix.row_mut(1).cols_and_values_mut(), ([].as_ref(), [].as_mut())); + + assert_eq!(matrix.row(2).ncols(), 2); + assert_eq!(matrix.row(2).nnz(), 0); + assert_eq!(matrix.row(2).col_indices(), &[]); + assert_eq!(matrix.row(2).values(), &[]); + assert_eq!(matrix.row_mut(2).ncols(), 2); + assert_eq!(matrix.row_mut(2).nnz(), 0); + assert_eq!(matrix.row_mut(2).col_indices(), &[]); + assert_eq!(matrix.row_mut(2).values(), &[]); + assert_eq!(matrix.row_mut(2).values_mut(), &[]); + assert_eq!(matrix.row_mut(2).cols_and_values_mut(), ([].as_ref(), [].as_mut())); + + assert!(matrix.get_row(3).is_none()); + assert!(matrix.get_row_mut(3).is_none()); + + let (offsets, indices, values) = matrix.disassemble(); + + assert_eq!(offsets, vec![0, 0, 0, 0]); + assert_eq!(indices, vec![]); + assert_eq!(values, vec![]); + } + + { + // 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 mut matrix = CsrMatrix::try_from_csr_data(3, + 6, + offsets.clone(), + indices.clone(), + values.clone()).unwrap(); + + assert_eq!(matrix.nrows(), 3); + assert_eq!(matrix.ncols(), 6); + assert_eq!(matrix.nnz(), 5); + assert_eq!(matrix.row_offsets(), &[0, 2, 2, 5]); + assert_eq!(matrix.col_indices(), &[0, 5, 1, 2, 3]); + assert_eq!(matrix.values(), &[0, 1, 2, 3, 4]); + + let expected_triplets = vec![(0, 0, 0), (0, 5, 1), (2, 1, 2), (2, 2, 3), (2, 3, 4)]; + assert_eq!(matrix.triplet_iter().map(|(i, j, v)| (i, j, *v)).collect::>(), + expected_triplets); + assert_eq!(matrix.triplet_iter_mut().map(|(i, j, v)| (i, j, *v)).collect::>(), + expected_triplets); + + assert_eq!(matrix.row(0).ncols(), 6); + assert_eq!(matrix.row(0).nnz(), 2); + assert_eq!(matrix.row(0).col_indices(), &[0, 5]); + assert_eq!(matrix.row(0).values(), &[0, 1]); + assert_eq!(matrix.row_mut(0).ncols(), 6); + assert_eq!(matrix.row_mut(0).nnz(), 2); + assert_eq!(matrix.row_mut(0).col_indices(), &[0, 5]); + assert_eq!(matrix.row_mut(0).values(), &[0, 1]); + assert_eq!(matrix.row_mut(0).values_mut(), &[0, 1]); + assert_eq!(matrix.row_mut(0).cols_and_values_mut(), ([0, 5].as_ref(), [0, 1].as_mut())); + + assert_eq!(matrix.row(1).ncols(), 6); + assert_eq!(matrix.row(1).nnz(), 0); + assert_eq!(matrix.row(1).col_indices(), &[]); + assert_eq!(matrix.row(1).values(), &[]); + assert_eq!(matrix.row_mut(1).ncols(), 6); + assert_eq!(matrix.row_mut(1).nnz(), 0); + assert_eq!(matrix.row_mut(1).col_indices(), &[]); + assert_eq!(matrix.row_mut(1).values(), &[]); + assert_eq!(matrix.row_mut(1).values_mut(), &[]); + assert_eq!(matrix.row_mut(1).cols_and_values_mut(), ([].as_ref(), [].as_mut())); + + assert_eq!(matrix.row(2).ncols(), 6); + assert_eq!(matrix.row(2).nnz(), 3); + assert_eq!(matrix.row(2).col_indices(), &[1, 2, 3]); + assert_eq!(matrix.row(2).values(), &[2, 3, 4]); + assert_eq!(matrix.row_mut(2).ncols(), 6); + assert_eq!(matrix.row_mut(2).nnz(), 3); + assert_eq!(matrix.row_mut(2).col_indices(), &[1, 2, 3]); + assert_eq!(matrix.row_mut(2).values(), &[2, 3, 4]); + assert_eq!(matrix.row_mut(2).values_mut(), &[2, 3, 4]); + assert_eq!(matrix.row_mut(2).cols_and_values_mut(), ([1, 2, 3].as_ref(), [2, 3, 4].as_mut())); + + assert!(matrix.get_row(3).is_none()); + assert!(matrix.get_row_mut(3).is_none()); + + let (offsets2, indices2, values2) = matrix.disassemble(); + + assert_eq!(offsets2, offsets); + assert_eq!(indices2, indices); + assert_eq!(values2, values); + } +} + +#[test] +fn csr_matrix_try_from_invalid_csr_data() { + + { + // Empty offset array (invalid length) + let matrix = CsrMatrix::try_from_csr_data(0, 0, Vec::new(), Vec::new(), Vec::::new()); + assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + } + + { + // Offset array invalid length for arbitrary data + let offsets = vec![0, 3, 5]; + let indices = vec![0, 1, 2, 3, 5]; + let values = vec![0, 1, 2, 3, 4]; + + let matrix = CsrMatrix::try_from_csr_data(3, 6, offsets, indices, values); + assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + } + + { + // Invalid first entry in offsets array + let offsets = vec![1, 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, indices, values); + assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + } + + { + // Invalid last entry in offsets array + let offsets = vec![0, 2, 2, 4]; + 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, indices, values); + assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + } + + { + // Invalid length of offsets array + let offsets = vec![0, 2, 2]; + 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, indices, values); + assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + } + + { + // Nonmonotonic offsets + let offsets = vec![0, 3, 2, 5]; + let indices = vec![0, 1, 2, 3, 4]; + let values = vec![0, 1, 2, 3, 4]; + let matrix = CsrMatrix::try_from_csr_data(3, 6, offsets, indices, values); + assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + } + + { + // Nonmonotonic minor indices + let offsets = vec![0, 2, 2, 5]; + let indices = vec![0, 2, 3, 1, 4]; + let values = vec![0, 1, 2, 3, 4]; + let matrix = CsrMatrix::try_from_csr_data(3, 6, offsets, indices, values); + assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + } + + { + // Minor index out of bounds + let offsets = vec![0, 2, 2, 5]; + let indices = vec![0, 6, 1, 2, 3]; + let values = vec![0, 1, 2, 3, 4]; + let matrix = CsrMatrix::try_from_csr_data(3, 6, offsets, indices, values); + assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::IndexOutOfBounds); + } + + { + // Duplicate entry + let offsets = vec![0, 2, 2, 5]; + let indices = vec![0, 5, 2, 2, 3]; + let values = vec![0, 1, 2, 3, 4]; + let matrix = CsrMatrix::try_from_csr_data(3, 6, offsets, indices, values); + assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::DuplicateEntry); + } + +} + +#[test] +fn csr_matrix_get_index() { + // TODO: Implement tests for ::get() and index() +} + +#[test] +fn csr_matrix_row_iter() { + +} \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit_tests/mod.rs b/nalgebra-sparse/tests/unit_tests/mod.rs index 0c1631c4..2db7ab12 100644 --- a/nalgebra-sparse/tests/unit_tests/mod.rs +++ b/nalgebra-sparse/tests/unit_tests/mod.rs @@ -1,2 +1,4 @@ mod coo; -mod ops; \ No newline at end of file +mod ops; +mod pattern; +mod csr; \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit_tests/pattern.rs b/nalgebra-sparse/tests/unit_tests/pattern.rs new file mode 100644 index 00000000..4664ad74 --- /dev/null +++ b/nalgebra-sparse/tests/unit_tests/pattern.rs @@ -0,0 +1,130 @@ +use nalgebra_sparse::pattern::{SparsityPattern, SparsityPatternFormatError}; + +#[test] +fn sparsity_pattern_valid_data() { + // Construct pattern from valid data and check that selected methods return results + // that agree with expectations. + + { + // 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!(pattern.major_dim(), 3); + assert_eq!(pattern.minor_dim(), 2); + assert_eq!(pattern.nnz(), 0); + assert_eq!(pattern.major_offsets(), &[0, 0, 0, 0]); + assert_eq!(pattern.minor_indices(), &[]); + assert_eq!(pattern.lane(0), &[]); + assert_eq!(pattern.lane(1), &[]); + assert_eq!(pattern.lane(2), &[]); + assert!(pattern.entries().next().is_none()); + + assert_eq!(pattern, SparsityPattern::new(3, 2)); + + let (offsets, indices) = pattern.disassemble(); + assert_eq!(offsets, vec![0, 0, 0, 0]); + assert_eq!(indices, vec![]); + } + + { + // 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!(pattern.major_dim(), 3); + assert_eq!(pattern.minor_dim(), 6); + assert_eq!(pattern.major_offsets(), offsets.as_slice()); + assert_eq!(pattern.minor_indices(), indices.as_slice()); + assert_eq!(pattern.nnz(), 5); + assert_eq!(pattern.lane(0), &[0, 5]); + assert_eq!(pattern.lane(1), &[]); + assert_eq!(pattern.lane(2), &[1, 2, 3]); + assert_eq!(pattern.entries().collect::>(), + vec![(0, 0), (0, 5), (2, 1), (2, 2), (2, 3)]); + + let (offsets2, indices2) = pattern.disassemble(); + assert_eq!(offsets2, offsets); + assert_eq!(indices2, indices); + } +} + +#[test] +fn sparsity_pattern_try_from_invalid_data() { + { + // Empty offset array (invalid length) + let pattern = SparsityPattern::try_from_offsets_and_indices(0, 0, Vec::new(), Vec::new()); + assert_eq!(pattern, Err(SparsityPatternFormatError::InvalidOffsetArrayLength)); + } + + { + // Offset array invalid length for arbitrary data + let offsets = vec![0, 3, 5]; + let indices = vec![0, 1, 2, 3, 5]; + + let pattern = SparsityPattern::try_from_offsets_and_indices(3, 6, offsets, indices); + assert!(matches!(pattern, Err(SparsityPatternFormatError::InvalidOffsetArrayLength))); + } + + { + // Invalid first entry in offsets array + let offsets = vec![1, 2, 2, 5]; + let indices = vec![0, 5, 1, 2, 3]; + let pattern = SparsityPattern::try_from_offsets_and_indices(3, 6, offsets, indices); + assert!(matches!(pattern, Err(SparsityPatternFormatError::InvalidOffsetFirstLast))); + } + + { + // Invalid last entry in offsets array + let offsets = vec![0, 2, 2, 4]; + let indices = vec![0, 5, 1, 2, 3]; + let pattern = SparsityPattern::try_from_offsets_and_indices(3, 6, offsets, indices); + assert!(matches!(pattern, Err(SparsityPatternFormatError::InvalidOffsetFirstLast))); + } + + { + // Invalid length of offsets array + let offsets = vec![0, 2, 2]; + let indices = vec![0, 5, 1, 2, 3]; + let pattern = SparsityPattern::try_from_offsets_and_indices(3, 6, offsets, indices); + assert!(matches!(pattern, Err(SparsityPatternFormatError::InvalidOffsetArrayLength))); + } + + { + // Nonmonotonic offsets + let offsets = vec![0, 3, 2, 5]; + let indices = vec![0, 1, 2, 3, 4]; + let pattern = SparsityPattern::try_from_offsets_and_indices(3, 6, offsets, indices); + assert_eq!(pattern, Err(SparsityPatternFormatError::NonmonotonicOffsets)); + } + + { + // Nonmonotonic minor indices + let offsets = vec![0, 2, 2, 5]; + let indices = vec![0, 2, 3, 1, 4]; + let pattern = SparsityPattern::try_from_offsets_and_indices(3, 6, offsets, indices); + assert_eq!(pattern, Err(SparsityPatternFormatError::NonmonotonicMinorIndices)); + } + + { + // Minor index out of bounds + let offsets = vec![0, 2, 2, 5]; + let indices = vec![0, 6, 1, 2, 3]; + let pattern = SparsityPattern::try_from_offsets_and_indices(3, 6, offsets, indices); + assert_eq!(pattern, Err(SparsityPatternFormatError::MinorIndexOutOfBounds)); + } + + { + // Duplicate entry + let offsets = vec![0, 2, 2, 5]; + let indices = vec![0, 5, 2, 2, 3]; + let pattern = SparsityPattern::try_from_offsets_and_indices(3, 6, offsets, indices); + assert_eq!(pattern, Err(SparsityPatternFormatError::DuplicateEntry)); + } +} \ No newline at end of file From 4cd47327c9bc4c756038cebc5e222cbdba4400a4 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Fri, 25 Sep 2020 14:52:33 +0200 Subject: [PATCH 020/183] Test that CSR::disassemble avoids cloning when possible --- nalgebra-sparse/tests/unit_tests/csr.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/nalgebra-sparse/tests/unit_tests/csr.rs b/nalgebra-sparse/tests/unit_tests/csr.rs index 693e092f..ab6f698e 100644 --- a/nalgebra-sparse/tests/unit_tests/csr.rs +++ b/nalgebra-sparse/tests/unit_tests/csr.rs @@ -220,6 +220,29 @@ fn csr_matrix_try_from_invalid_csr_data() { } +#[test] +fn csr_disassemble_avoids_clone_when_owned() { + // Test that disassemble avoids cloning the sparsity pattern when it holds the sole reference + // to the pattern. We do so by checking that the pointer to the data is unchanged. + + let offsets = vec![0, 2, 2, 5]; + let indices = vec![0, 5, 1, 2, 3]; + let values = vec![0, 1, 2, 3, 4]; + let offsets_ptr = offsets.as_ptr(); + let indices_ptr = indices.as_ptr(); + let values_ptr = values.as_ptr(); + let matrix = CsrMatrix::try_from_csr_data(3, + 6, + offsets, + indices, + values).unwrap(); + + let (offsets, indices, values) = matrix.disassemble(); + assert_eq!(offsets.as_ptr(), offsets_ptr); + assert_eq!(indices.as_ptr(), indices_ptr); + assert_eq!(values.as_ptr(), values_ptr); +} + #[test] fn csr_matrix_get_index() { // TODO: Implement tests for ::get() and index() From 082416e3ec12cd355d6b8f12c0d9d2ffcaa03d6b Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Fri, 25 Sep 2020 15:11:43 +0200 Subject: [PATCH 021/183] Make SparseFormatErrorKind #[non_exhaustive] --- nalgebra-sparse/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index 9621d973..7bcb78a9 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -103,6 +103,7 @@ impl SparseFormatError { } /// The type of format error described by a [SparseFormatError](struct.SparseFormatError.html). +#[non_exhaustive] #[derive(Debug, Clone, PartialEq, Eq)] pub enum SparseFormatErrorKind { /// Indicates that the index data associated with the format contains at least one index From ec339f9108be8b99405dbd494bab82e51063a357 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 28 Sep 2020 11:36:00 +0200 Subject: [PATCH 022/183] Implement CSC matrix basic API The CSC matrix API mirrors the CSR matrix API. However, there are subtle differences throughout (both in the available methods and the implementation) that I believe makes any attempt to avoid the duplicate effort futile. --- nalgebra-sparse/src/csc.rs | 546 ++++++++++++++++++++++++ nalgebra-sparse/src/csr.rs | 2 +- nalgebra-sparse/src/lib.rs | 11 +- nalgebra-sparse/tests/unit_tests/csc.rs | 254 +++++++++++ nalgebra-sparse/tests/unit_tests/mod.rs | 3 +- 5 files changed, 813 insertions(+), 3 deletions(-) create mode 100644 nalgebra-sparse/src/csc.rs create mode 100644 nalgebra-sparse/tests/unit_tests/csc.rs diff --git a/nalgebra-sparse/src/csc.rs b/nalgebra-sparse/src/csc.rs new file mode 100644 index 00000000..1a3f0639 --- /dev/null +++ b/nalgebra-sparse/src/csc.rs @@ -0,0 +1,546 @@ +//! An implementation of the CSC sparse matrix format. + +use crate::{SparseFormatError, SparseFormatErrorKind}; +use crate::pattern::{SparsityPattern, SparsityPatternFormatError, SparsityPatternIter}; + +use std::sync::Arc; +use std::slice::{IterMut, Iter}; +use std::ops::Range; +use num_traits::Zero; +use std::ptr::slice_from_raw_parts_mut; + +/// A CSC representation of a sparse matrix. +/// +/// The Compressed Sparse Column (CSC) format is well-suited as a general-purpose storage format +/// for many sparse matrix applications. +/// +/// TODO: Storage explanation and examples +/// +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CscMatrix { + // Cols are major, rows are minor in the sparsity pattern + sparsity_pattern: Arc, + values: Vec, +} + +impl CscMatrix { + /// Create a zero CSC matrix with no explicitly stored entries. + pub fn new(nrows: usize, ncols: usize) -> Self { + Self { + sparsity_pattern: Arc::new(SparsityPattern::new(ncols, nrows)), + values: vec![], + } + } + + /// The number of rows in the matrix. + #[inline] + pub fn nrows(&self) -> usize { + self.sparsity_pattern.minor_dim() + } + + /// The number of columns in the matrix. + #[inline] + pub fn ncols(&self) -> usize { + self.sparsity_pattern.major_dim() + } + + /// The number of non-zeros in the matrix. + /// + /// Note that this corresponds to the number of explicitly stored entries, *not* the actual + /// number of algebraically zero entries in the matrix. Explicitly stored entries can still + /// be zero. Corresponds to the number of entries in the sparsity pattern. + #[inline] + pub fn nnz(&self) -> usize { + self.sparsity_pattern.nnz() + } + + /// The column offsets defining part of the CSC format. + #[inline] + pub fn col_offsets(&self) -> &[usize] { + self.sparsity_pattern.major_offsets() + } + + /// The row indices defining part of the CSC format. + #[inline] + pub fn row_indices(&self) -> &[usize] { + self.sparsity_pattern.minor_indices() + } + + /// The non-zero values defining part of the CSC format. + #[inline] + pub fn values(&self) -> &[T] { + &self.values + } + + /// Mutable access to the non-zero values. + #[inline] + pub fn values_mut(&mut self) -> &mut [T] { + &mut self.values + } + + /// Try to construct a CSC matrix from raw CSC data. + /// + /// It is assumed that each column contains unique and sorted row indices that are in + /// bounds with respect to the number of rows in the matrix. If this is not the case, + /// an error is returned to indicate the failure. + /// + /// An error is returned if the data given does not conform to the CSC storage format. + /// See the documentation for [CscMatrix](struct.CscMatrix.html) for more information. + pub fn try_from_csc_data( + num_rows: usize, + num_cols: usize, + col_offsets: Vec, + row_indices: Vec, + values: Vec, + ) -> Result { + let pattern = SparsityPattern::try_from_offsets_and_indices( + num_cols, num_rows, col_offsets, row_indices) + .map_err(pattern_format_error_to_csc_error)?; + Self::try_from_pattern_and_values(Arc::new(pattern), values) + } + + /// Try to construct a CSC matrix from a sparsity pattern and associated non-zero values. + /// + /// Returns an error if the number of values does not match the number of minor indices + /// in the pattern. + pub fn try_from_pattern_and_values(pattern: Arc, values: Vec) + -> Result { + if pattern.nnz() == values.len() { + Ok(Self { + sparsity_pattern: pattern, + values, + }) + } else { + Err(SparseFormatError::from_kind_and_msg( + SparseFormatErrorKind::InvalidStructure, + "Number of values and row indices must be the same")) + } + } + + + /// An iterator over non-zero triplets (i, j, v). + /// + /// The iteration happens in column-major fashion, meaning that j increases monotonically, + /// and i increases monotonically within each row. + /// + /// Examples + /// -------- + /// ``` + /// # use nalgebra_sparse::csc::CscMatrix; + /// let col_offsets = vec![0, 2, 3, 4]; + /// let row_indices = vec![0, 2, 1, 0]; + /// let values = vec![1, 3, 2, 4]; + /// let mut csc = CscMatrix::try_from_csc_data(4, 3, col_offsets, row_indices, values) + /// .unwrap(); + /// + /// let triplets: Vec<_> = csc.triplet_iter().map(|(i, j, v)| (i, j, *v)).collect(); + /// assert_eq!(triplets, vec![(0, 0, 1), (2, 0, 3), (1, 1, 2), (0, 2, 4)]); + /// ``` + pub fn triplet_iter(&self) -> CscTripletIter { + CscTripletIter { + pattern_iter: self.sparsity_pattern.entries(), + values_iter: self.values.iter() + } + } + + /// A mutable iterator over non-zero triplets (i, j, v). + /// + /// Iteration happens in the same order as for [triplet_iter](#method.triplet_iter). + /// + /// Examples + /// -------- + /// ``` + /// # use nalgebra_sparse::csc::CscMatrix; + /// let col_offsets = vec![0, 2, 3, 4]; + /// let row_indices = vec![0, 2, 1, 0]; + /// let values = vec![1, 3, 2, 4]; + /// // Using the same data as in the `triplet_iter` example + /// let mut csc = CscMatrix::try_from_csc_data(4, 3, col_offsets, row_indices, values) + /// .unwrap(); + /// + /// // Zero out lower-triangular terms + /// csc.triplet_iter_mut() + /// .filter(|(i, j, _)| j < i) + /// .for_each(|(_, _, v)| *v = 0); + /// + /// let triplets: Vec<_> = csc.triplet_iter().map(|(i, j, v)| (i, j, *v)).collect(); + /// assert_eq!(triplets, vec![(0, 0, 1), (2, 0, 0), (1, 1, 2), (0, 2, 4)]); + /// ``` + pub fn triplet_iter_mut(&mut self) -> CscTripletIterMut { + CscTripletIterMut { + pattern_iter: self.sparsity_pattern.entries(), + values_mut_iter: self.values.iter_mut() + } + } + + /// Return the column at the given column index. + /// + /// Panics + /// ------ + /// Panics if column index is out of bounds. + #[inline] + pub fn col(&self, index: usize) -> CscCol { + self.get_col(index) + .expect("Row index must be in bounds") + } + + /// Mutable column access for the given column index. + /// + /// Panics + /// ------ + /// Panics if column index is out of bounds. + #[inline] + pub fn col_mut(&mut self, index: usize) -> CscColMut { + self.get_col_mut(index) + .expect("Row index must be in bounds") + } + + /// Return the column at the given column index, or `None` if out of bounds. + #[inline] + pub fn get_col(&self, index: usize) -> Option> { + let range = self.get_index_range(index)?; + Some(CscCol { + row_indices: &self.sparsity_pattern.minor_indices()[range.clone()], + values: &self.values[range], + nrows: self.nrows() + }) + } + + /// Mutable column access for the given column index, or `None` if out of bounds. + #[inline] + pub fn get_col_mut(&mut self, index: usize) -> Option> { + let range = self.get_index_range(index)?; + Some(CscColMut { + nrows: self.nrows(), + row_indices: &self.sparsity_pattern.minor_indices()[range.clone()], + values: &mut self.values[range] + }) + } + + /// Internal method for simplifying access to a column's data. + fn get_index_range(&self, col_index: usize) -> Option> { + let col_begin = *self.sparsity_pattern.major_offsets().get(col_index)?; + let col_end = *self.sparsity_pattern.major_offsets().get(col_index + 1)?; + Some(col_begin .. col_end) + } + + /// An iterator over columns in the matrix. + pub fn col_iter(&self) -> CscColIter { + CscColIter { + current_col_idx: 0, + matrix: self + } + } + + /// A mutable iterator over columns in the matrix. + pub fn col_iter_mut(&mut self) -> CscColIterMut { + CscColIterMut { + current_col_idx: 0, + pattern: &self.sparsity_pattern, + remaining_values: self.values.as_mut_ptr() + } + } + + /// Returns the underlying vector containing the values for the explicitly stored entries. + pub fn take_values(self) -> Vec { + self.values + } + + /// Disassembles the CSC matrix into its underlying offset, index and value arrays. + /// + /// If the matrix contains the sole reference to the sparsity pattern, + /// then the data is returned as-is. Otherwise, the sparsity pattern is cloned. + /// + /// Examples + /// -------- + /// + /// ``` + /// # use nalgebra_sparse::csc::CscMatrix; + /// let col_offsets = vec![0, 2, 3, 4]; + /// let row_indices = vec![0, 2, 1, 0]; + /// let values = vec![1, 3, 2, 4]; + /// let mut csc = CscMatrix::try_from_csc_data( + /// 4, + /// 3, + /// col_offsets.clone(), + /// row_indices.clone(), + /// values.clone()) + /// .unwrap(); + /// let (col_offsets2, row_indices2, values2) = csc.disassemble(); + /// assert_eq!(col_offsets2, col_offsets); + /// assert_eq!(row_indices2, row_indices); + /// assert_eq!(values2, values); + /// ``` + pub fn disassemble(self) -> (Vec, Vec, Vec) { + // Take an Arc to the pattern, which might be the sole reference to the data after + // taking the values. This is important, because it might let us avoid cloning the data + // further below. + let pattern = self.pattern(); + let values = self.take_values(); + + // Try to take the pattern out of the `Arc` if possible, + // otherwise clone the pattern. + let owned_pattern = Arc::try_unwrap(pattern) + .unwrap_or_else(|arc| SparsityPattern::clone(&*arc)); + let (offsets, indices) = owned_pattern.disassemble(); + + (offsets, indices, values) + } + + /// Returns the underlying sparsity pattern. + /// + /// The sparsity pattern is stored internally inside an `Arc`. This allows users to re-use + /// the same sparsity pattern for multiple matrices without storing the same pattern multiple + /// times in memory. + pub fn pattern(&self) -> Arc { + Arc::clone(&self.sparsity_pattern) + } +} + +impl CscMatrix { + /// Return the value in the matrix at the given global row/col indices, or `None` if out of + /// bounds. + /// + /// If the indices are in bounds, but no explicitly stored entry is associated with it, + /// `T::zero()` is returned. Note that this method offers no way of distinguishing + /// explicitly stored zero entries from zero values that are only implicitly represented. + /// + /// Each call to this function incurs the cost of a binary search among the explicitly + /// stored column entries for the given row. + #[inline] + pub fn get(&self, row_index: usize, col_index: usize) -> Option { + self.get_col(row_index)?.get(col_index) + } + + /// Same as `get`, but panics if indices are out of bounds. + /// + /// Panics + /// ------ + /// Panics if either index is out of bounds. + #[inline] + pub fn index(&self, row_index: usize, col_index: usize) -> T { + self.get(row_index, col_index).unwrap() + } +} + +/// Convert pattern format errors into more meaningful CSC-specific errors. +/// +/// This ensures that the terminology is consistent: we are talking about rows and columns, +/// not lanes, major and minor dimensions. +fn pattern_format_error_to_csc_error(err: SparsityPatternFormatError) -> SparseFormatError { + use SparsityPatternFormatError::*; + use SparsityPatternFormatError::DuplicateEntry as PatternDuplicateEntry; + use SparseFormatError as E; + use SparseFormatErrorKind as K; + + match err { + InvalidOffsetArrayLength => E::from_kind_and_msg( + K::InvalidStructure, + "Length of col offset array is not equal to ncols + 1."), + InvalidOffsetFirstLast => E::from_kind_and_msg( + K::InvalidStructure, + "First or last col offset is inconsistent with format specification."), + NonmonotonicOffsets => E::from_kind_and_msg( + K::InvalidStructure, + "Col offsets are not monotonically increasing."), + NonmonotonicMinorIndices => E::from_kind_and_msg( + K::InvalidStructure, + "Row indices are not monotonically increasing (sorted) within each column."), + MinorIndexOutOfBounds => E::from_kind_and_msg( + K::IndexOutOfBounds, + "Row indices are out of bounds."), + PatternDuplicateEntry => E::from_kind_and_msg( + K::DuplicateEntry, + "Matrix data contains duplicate entries."), + } +} + +/// Iterator type for iterating over triplets in a CSC matrix. +#[derive(Debug)] +pub struct CscTripletIter<'a, T> { + pattern_iter: SparsityPatternIter<'a>, + values_iter: Iter<'a, T> +} + +impl<'a, T> Iterator for CscTripletIter<'a, T> { + type Item = (usize, usize, &'a T); + + fn next(&mut self) -> Option { + let next_entry = self.pattern_iter.next(); + let next_value = self.values_iter.next(); + + match (next_entry, next_value) { + (Some((i, j)), Some(v)) => Some((j, i, v)), + _ => None + } + } +} + +/// Iterator type for mutably iterating over triplets in a CSC matrix. +#[derive(Debug)] +pub struct CscTripletIterMut<'a, T> { + pattern_iter: SparsityPatternIter<'a>, + values_mut_iter: IterMut<'a, T> +} + +impl<'a, T> Iterator for CscTripletIterMut<'a, T> { + type Item = (usize, usize, &'a mut T); + + #[inline] + fn next(&mut self) -> Option { + let next_entry = self.pattern_iter.next(); + let next_value = self.values_mut_iter.next(); + + match (next_entry, next_value) { + (Some((i, j)), Some(v)) => Some((j, i, v)), + _ => None + } + } +} + +/// An immutable representation of a column in a CSC matrix. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CscCol<'a, T> { + nrows: usize, + row_indices: &'a [usize], + values: &'a [T], +} + +/// A mutable representation of a column in a CSC matrix. +/// +/// Note that only explicitly stored entries can be mutated. The sparsity pattern belonging +/// to the column cannot be modified. +#[derive(Debug, PartialEq, Eq)] +pub struct CscColMut<'a, T> { + nrows: usize, + row_indices: &'a [usize], + values: &'a mut [T] +} + +/// Implement the methods common to both CscCol and CscColMut +macro_rules! impl_csc_col_common_methods { + ($name:ty) => { + impl<'a, T> $name { + /// The number of global rows in the column. + #[inline] + pub fn nrows(&self) -> usize { + self.nrows + } + + /// The number of non-zeros in this column. + #[inline] + pub fn nnz(&self) -> usize { + self.row_indices.len() + } + + /// The row indices corresponding to explicitly stored entries in this column. + #[inline] + pub fn row_indices(&self) -> &[usize] { + self.row_indices + } + + /// The values corresponding to explicitly stored entries in this column. + #[inline] + pub fn values(&self) -> &[T] { + self.values + } + } + + impl<'a, T: Clone + Zero> $name { + /// Return the value in the matrix at the given global row index, or `None` if out of + /// bounds. + /// + /// If the index is in bounds, but no explicitly stored entry is associated with it, + /// `T::zero()` is returned. Note that this method offers no way of distinguishing + /// explicitly stored zero entries from zero values that are only implicitly represented. + /// + /// Each call to this function incurs the cost of a binary search among the explicitly + /// stored row entries for the current column. + pub fn get(&self, global_row_index: usize) -> Option { + let local_index = self.row_indices().binary_search(&global_row_index); + if let Ok(local_index) = local_index { + Some(self.values[local_index].clone()) + } else if global_row_index < self.nrows { + Some(T::zero()) + } else { + None + } + } + } + } +} + +impl_csc_col_common_methods!(CscCol<'a, T>); +impl_csc_col_common_methods!(CscColMut<'a, T>); + +impl<'a, T> CscColMut<'a, T> { + /// Mutable access to the values corresponding to explicitly stored entries in this column. + pub fn values_mut(&mut self) -> &mut [T] { + self.values + } + + /// Provides simultaneous access to row indices and mutable values corresponding to the + /// explicitly stored entries in this column. + /// + /// This method primarily facilitates low-level access for methods that process data stored + /// in CSC format directly. + pub fn rows_and_values_mut(&mut self) -> (&[usize], &mut [T]) { + (self.row_indices, self.values) + } +} + +/// Column iterator for [CscMatrix](struct.CscMatrix.html). +pub struct CscColIter<'a, T> { + // The index of the row that will be returned on the next + current_col_idx: usize, + matrix: &'a CscMatrix +} + +impl<'a, T> Iterator for CscColIter<'a, T> { + type Item = CscCol<'a, T>; + + fn next(&mut self) -> Option { + let col = self.matrix.get_col(self.current_col_idx); + self.current_col_idx += 1; + col + } +} + +/// Mutable column iterator for [CscMatrix](struct.CscMatrix.html). +pub struct CscColIterMut<'a, T> { + current_col_idx: usize, + pattern: &'a SparsityPattern, + remaining_values: *mut T, +} + +impl<'a, T> Iterator for CscColIterMut<'a, T> +where + T: 'a +{ + type Item = CscColMut<'a, T>; + + fn next(&mut self) -> Option { + let lane = self.pattern.get_lane(self.current_col_idx); + let nrows = self.pattern.minor_dim(); + + if let Some(row_indices) = lane { + let count = row_indices.len(); + + // Note: I can't think of any way to construct this iterator without unsafe. + let values_in_row; + unsafe { + values_in_row = &mut *slice_from_raw_parts_mut(self.remaining_values, count); + self.remaining_values = self.remaining_values.add(count); + } + self.current_col_idx += 1; + + Some(CscColMut { + nrows, + row_indices, + values: values_in_row + }) + } else { + None + } + } +} \ No newline at end of file diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index c612e5b4..3adc25bd 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -11,7 +11,7 @@ use std::ptr::slice_from_raw_parts_mut; /// A CSR representation of a sparse matrix. /// -/// The Compressed Row Storage (CSR) format is well-suited as a general-purpose storage format +/// The Compressed Sparse Row (CSR) format is well-suited as a general-purpose storage format /// for many sparse matrix applications. /// /// TODO: Storage explanation and examples diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index 7bcb78a9..e1820b4a 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -28,7 +28,15 @@ //! - [x] "Disassemble" the CSR matrix into the raw CSR data arrays. //! //! - CSC matrix type. Functionality: -//! - Same as CSR, but with columns instead of rows. +//! - [x] Access to CSC data as slices. +//! - [x] Return number of nnz +//! - [x] Access a given column, which gives convenient access to the data associated +//! with a particular column +//! - [x] Construct from valid CSC data +//! - [ ] Construct from unsorted CSC data +//! - [x] Iterate over entries (i, j, v) in the matrix (+mutable). +//! - [x] Iterate over rows in the matrix (+ mutable). +//! - [x] "Disassemble" the CSC matrix into the raw CSC data arrays. //! - COO matrix type. Functionality: //! - [x] Construct new "empty" COO matrix //! - [x] Construct from triplet arrays. @@ -68,6 +76,7 @@ #![deny(missing_docs)] pub mod coo; +pub mod csc; pub mod csr; pub mod pattern; pub mod ops; diff --git a/nalgebra-sparse/tests/unit_tests/csc.rs b/nalgebra-sparse/tests/unit_tests/csc.rs new file mode 100644 index 00000000..140a5db2 --- /dev/null +++ b/nalgebra-sparse/tests/unit_tests/csc.rs @@ -0,0 +1,254 @@ +use nalgebra_sparse::csc::CscMatrix; +use nalgebra_sparse::SparseFormatErrorKind; + +#[test] +fn csc_matrix_valid_data() { + // Construct matrix from valid data and check that selected methods return results + // that agree with expectations. + + { + // A CSC matrix with zero explicitly stored entries + let offsets = vec![0, 0, 0, 0]; + let indices = vec![]; + let values = Vec::::new(); + let mut matrix = CscMatrix::try_from_csc_data(2, 3, offsets, indices, values).unwrap(); + + assert_eq!(matrix, CscMatrix::new(2, 3)); + + assert_eq!(matrix.nrows(), 2); + assert_eq!(matrix.ncols(), 3); + assert_eq!(matrix.nnz(), 0); + assert_eq!(matrix.col_offsets(), &[0, 0, 0, 0]); + assert_eq!(matrix.row_indices(), &[]); + assert_eq!(matrix.values(), &[]); + + assert!(matrix.triplet_iter().next().is_none()); + assert!(matrix.triplet_iter_mut().next().is_none()); + + assert_eq!(matrix.col(0).nrows(), 2); + assert_eq!(matrix.col(0).nnz(), 0); + assert_eq!(matrix.col(0).row_indices(), &[]); + assert_eq!(matrix.col(0).values(), &[]); + assert_eq!(matrix.col_mut(0).nrows(), 2); + assert_eq!(matrix.col_mut(0).nnz(), 0); + assert_eq!(matrix.col_mut(0).row_indices(), &[]); + assert_eq!(matrix.col_mut(0).values(), &[]); + assert_eq!(matrix.col_mut(0).values_mut(), &[]); + assert_eq!(matrix.col_mut(0).rows_and_values_mut(), ([].as_ref(), [].as_mut())); + + assert_eq!(matrix.col(1).nrows(), 2); + assert_eq!(matrix.col(1).nnz(), 0); + assert_eq!(matrix.col(1).row_indices(), &[]); + assert_eq!(matrix.col(1).values(), &[]); + assert_eq!(matrix.col_mut(1).nrows(), 2); + assert_eq!(matrix.col_mut(1).nnz(), 0); + assert_eq!(matrix.col_mut(1).row_indices(), &[]); + assert_eq!(matrix.col_mut(1).values(), &[]); + assert_eq!(matrix.col_mut(1).values_mut(), &[]); + assert_eq!(matrix.col_mut(1).rows_and_values_mut(), ([].as_ref(), [].as_mut())); + + assert_eq!(matrix.col(2).nrows(), 2); + assert_eq!(matrix.col(2).nnz(), 0); + assert_eq!(matrix.col(2).row_indices(), &[]); + assert_eq!(matrix.col(2).values(), &[]); + assert_eq!(matrix.col_mut(2).nrows(), 2); + assert_eq!(matrix.col_mut(2).nnz(), 0); + assert_eq!(matrix.col_mut(2).row_indices(), &[]); + assert_eq!(matrix.col_mut(2).values(), &[]); + assert_eq!(matrix.col_mut(2).values_mut(), &[]); + assert_eq!(matrix.col_mut(2).rows_and_values_mut(), ([].as_ref(), [].as_mut())); + + assert!(matrix.get_col(3).is_none()); + assert!(matrix.get_col_mut(3).is_none()); + + let (offsets, indices, values) = matrix.disassemble(); + + assert_eq!(offsets, vec![0, 0, 0, 0]); + assert_eq!(indices, vec![]); + assert_eq!(values, vec![]); + } + + { + // 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 mut matrix = CscMatrix::try_from_csc_data(6, + 3, + offsets.clone(), + indices.clone(), + values.clone()).unwrap(); + + assert_eq!(matrix.nrows(), 6); + assert_eq!(matrix.ncols(), 3); + assert_eq!(matrix.nnz(), 5); + assert_eq!(matrix.col_offsets(), &[0, 2, 2, 5]); + assert_eq!(matrix.row_indices(), &[0, 5, 1, 2, 3]); + assert_eq!(matrix.values(), &[0, 1, 2, 3, 4]); + + let expected_triplets = vec![(0, 0, 0), (5, 0, 1), (1, 2, 2), (2, 2, 3), (3, 2, 4)]; + assert_eq!(matrix.triplet_iter().map(|(i, j, v)| (i, j, *v)).collect::>(), + expected_triplets); + assert_eq!(matrix.triplet_iter_mut().map(|(i, j, v)| (i, j, *v)).collect::>(), + expected_triplets); + + assert_eq!(matrix.col(0).nrows(), 6); + assert_eq!(matrix.col(0).nnz(), 2); + assert_eq!(matrix.col(0).row_indices(), &[0, 5]); + assert_eq!(matrix.col(0).values(), &[0, 1]); + assert_eq!(matrix.col_mut(0).nrows(), 6); + assert_eq!(matrix.col_mut(0).nnz(), 2); + assert_eq!(matrix.col_mut(0).row_indices(), &[0, 5]); + assert_eq!(matrix.col_mut(0).values(), &[0, 1]); + assert_eq!(matrix.col_mut(0).values_mut(), &[0, 1]); + assert_eq!(matrix.col_mut(0).rows_and_values_mut(), ([0, 5].as_ref(), [0, 1].as_mut())); + + assert_eq!(matrix.col(1).nrows(), 6); + assert_eq!(matrix.col(1).nnz(), 0); + assert_eq!(matrix.col(1).row_indices(), &[]); + assert_eq!(matrix.col(1).values(), &[]); + assert_eq!(matrix.col_mut(1).nrows(), 6); + assert_eq!(matrix.col_mut(1).nnz(), 0); + assert_eq!(matrix.col_mut(1).row_indices(), &[]); + assert_eq!(matrix.col_mut(1).values(), &[]); + assert_eq!(matrix.col_mut(1).values_mut(), &[]); + assert_eq!(matrix.col_mut(1).rows_and_values_mut(), ([].as_ref(), [].as_mut())); + + assert_eq!(matrix.col(2).nrows(), 6); + assert_eq!(matrix.col(2).nnz(), 3); + assert_eq!(matrix.col(2).row_indices(), &[1, 2, 3]); + assert_eq!(matrix.col(2).values(), &[2, 3, 4]); + assert_eq!(matrix.col_mut(2).nrows(), 6); + assert_eq!(matrix.col_mut(2).nnz(), 3); + assert_eq!(matrix.col_mut(2).row_indices(), &[1, 2, 3]); + assert_eq!(matrix.col_mut(2).values(), &[2, 3, 4]); + assert_eq!(matrix.col_mut(2).values_mut(), &[2, 3, 4]); + assert_eq!(matrix.col_mut(2).rows_and_values_mut(), ([1, 2, 3].as_ref(), [2, 3, 4].as_mut())); + + assert!(matrix.get_col(3).is_none()); + assert!(matrix.get_col_mut(3).is_none()); + + let (offsets2, indices2, values2) = matrix.disassemble(); + + assert_eq!(offsets2, offsets); + assert_eq!(indices2, indices); + assert_eq!(values2, values); + } +} + +#[test] +fn csc_matrix_try_from_invalid_csc_data() { + + { + // Empty offset array (invalid length) + let matrix = CscMatrix::try_from_csc_data(0, 0, Vec::new(), Vec::new(), Vec::::new()); + assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + } + + { + // Offset array invalid length for arbitrary data + let offsets = vec![0, 3, 5]; + let indices = vec![0, 1, 2, 3, 5]; + let values = vec![0, 1, 2, 3, 4]; + + let matrix = CscMatrix::try_from_csc_data(6, 3, offsets, indices, values); + assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + } + + { + // Invalid first entry in offsets array + let offsets = vec![1, 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, indices, values); + assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + } + + { + // Invalid last entry in offsets array + let offsets = vec![0, 2, 2, 4]; + 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, indices, values); + assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + } + + { + // Invalid length of offsets array + let offsets = vec![0, 2, 2]; + 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, indices, values); + assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + } + + { + // Nonmonotonic offsets + let offsets = vec![0, 3, 2, 5]; + let indices = vec![0, 1, 2, 3, 4]; + let values = vec![0, 1, 2, 3, 4]; + let matrix = CscMatrix::try_from_csc_data(6, 3, offsets, indices, values); + assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + } + + { + // Nonmonotonic minor indices + let offsets = vec![0, 2, 2, 5]; + let indices = vec![0, 2, 3, 1, 4]; + let values = vec![0, 1, 2, 3, 4]; + let matrix = CscMatrix::try_from_csc_data(6, 3, offsets, indices, values); + assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + } + + { + // Minor index out of bounds + let offsets = vec![0, 2, 2, 5]; + let indices = vec![0, 6, 1, 2, 3]; + let values = vec![0, 1, 2, 3, 4]; + let matrix = CscMatrix::try_from_csc_data(6, 3, offsets, indices, values); + assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::IndexOutOfBounds); + } + + { + // Duplicate entry + let offsets = vec![0, 2, 2, 5]; + let indices = vec![0, 5, 2, 2, 3]; + let values = vec![0, 1, 2, 3, 4]; + let matrix = CscMatrix::try_from_csc_data(6, 3, offsets, indices, values); + assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::DuplicateEntry); + } + +} + +#[test] +fn csc_disassemble_avoids_clone_when_owned() { + // Test that disassemble avoids cloning the sparsity pattern when it holds the sole reference + // to the pattern. We do so by checking that the pointer to the data is unchanged. + + let offsets = vec![0, 2, 2, 5]; + let indices = vec![0, 5, 1, 2, 3]; + let values = vec![0, 1, 2, 3, 4]; + let offsets_ptr = offsets.as_ptr(); + let indices_ptr = indices.as_ptr(); + let values_ptr = values.as_ptr(); + let matrix = CscMatrix::try_from_csc_data(6, + 3, + offsets, + indices, + values).unwrap(); + + let (offsets, indices, values) = matrix.disassemble(); + assert_eq!(offsets.as_ptr(), offsets_ptr); + assert_eq!(indices.as_ptr(), indices_ptr); + assert_eq!(values.as_ptr(), values_ptr); +} + +#[test] +fn csc_matrix_get_index() { + // TODO: Implement tests for ::get() and index() +} + +#[test] +fn csc_matrix_col_iter() { + // TODO +} \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit_tests/mod.rs b/nalgebra-sparse/tests/unit_tests/mod.rs index 2db7ab12..733357c9 100644 --- a/nalgebra-sparse/tests/unit_tests/mod.rs +++ b/nalgebra-sparse/tests/unit_tests/mod.rs @@ -1,4 +1,5 @@ mod coo; mod ops; mod pattern; -mod csr; \ No newline at end of file +mod csr; +mod csc; \ No newline at end of file From 67aba826759c2ca87a6239451e15069ef3f58cf9 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Wed, 11 Nov 2020 09:28:48 +0100 Subject: [PATCH 023/183] Update nalgebra version for nalgebra-sparse --- nalgebra-sparse/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nalgebra-sparse/Cargo.toml b/nalgebra-sparse/Cargo.toml index 99593ed7..36230993 100644 --- a/nalgebra-sparse/Cargo.toml +++ b/nalgebra-sparse/Cargo.toml @@ -5,5 +5,5 @@ authors = [ "Andreas Longva", "Sébastien Crozet " ] edition = "2018" [dependencies] -nalgebra = { version="0.22", path = "../" } -num-traits = { version = "0.2", default-features = false } \ No newline at end of file +nalgebra = { version="0.23", path = "../" } +num-traits = { version = "0.2", default-features = false } From 46442d6060c2aec365c6c375d6d13f96129123f2 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Thu, 12 Nov 2020 11:49:19 +0100 Subject: [PATCH 024/183] Initial basic proptest support for CooMatrix (missing tests) --- nalgebra-sparse/Cargo.toml | 4 ++ nalgebra-sparse/src/lib.rs | 3 ++ nalgebra-sparse/src/proptest.rs | 41 ++++++++++++++++++++ nalgebra-sparse/tests/unit_tests/proptest.rs | 0 4 files changed, 48 insertions(+) create mode 100644 nalgebra-sparse/src/proptest.rs create mode 100644 nalgebra-sparse/tests/unit_tests/proptest.rs diff --git a/nalgebra-sparse/Cargo.toml b/nalgebra-sparse/Cargo.toml index 36230993..bf0d3849 100644 --- a/nalgebra-sparse/Cargo.toml +++ b/nalgebra-sparse/Cargo.toml @@ -4,6 +4,10 @@ version = "0.1.0" authors = [ "Andreas Longva", "Sébastien Crozet " ] edition = "2018" +[features] +proptest-support = ["proptest", "nalgebra/proptest"] + [dependencies] nalgebra = { version="0.23", path = "../" } num-traits = { version = "0.2", default-features = false } +proptest = { version = "0.10", optional = true } diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index e1820b4a..53bc846a 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -81,6 +81,9 @@ pub mod csr; pub mod pattern; pub mod ops; +#[cfg(feature = "proptest-support")] +pub mod proptest; + use std::error::Error; use std::fmt; diff --git a/nalgebra-sparse/src/proptest.rs b/nalgebra-sparse/src/proptest.rs new file mode 100644 index 00000000..59d832e7 --- /dev/null +++ b/nalgebra-sparse/src/proptest.rs @@ -0,0 +1,41 @@ +//! TODO +//! +//! TODO: Clarify that this module needs proptest-support feature + +use crate::coo::CooMatrix; +use proptest::prelude::*; +use proptest::collection::{SizeRange, vec}; +use nalgebra::Scalar; + +/// TODO +pub fn coo( + value_strategy: T, + rows: impl Strategy + 'static, + cols: impl Strategy + 'static, + max_nonzeros: usize) -> BoxedStrategy> +where + T: Strategy + Clone + 'static, + T::Value: Scalar, +{ + (rows, cols, (0 ..= max_nonzeros)) + .prop_flat_map(move |(nrows, ncols, nnz)| { + // If the numbers of rows and columns are small in comparison with the + // max nnz, it will lead to small matrices essentially always turning out to be dense. + // To address this, we correct the nnz by computing the modulo with the + // maximum number of non-zeros (ignoring duplicates) we can have for + // the given dimensions. + // This way we can still generate very sparse matrices for small matrices. + let max_nnz = nrows * ncols; + let nnz = if max_nnz == 0 { 0 } else { nnz % max_nnz }; + let row_index_strategy = if nrows > 0 { 0 .. nrows } else { 0 .. 1 }; + let col_index_strategy = if ncols > 0 { 0 .. ncols } else { 0 .. 1 }; + let row_indices = vec![row_index_strategy.clone(); nnz]; + let col_indices = vec![col_index_strategy.clone(); nnz]; + let values_strategy = vec![value_strategy.clone(); nnz]; + + (Just(nrows), Just(ncols), row_indices, col_indices, values_strategy) + }).prop_map(|(nrows, ncols, row_indices, col_indices, values)| { + CooMatrix::try_from_triplets(nrows, ncols, row_indices, col_indices, values) + .expect("We should always generate valid COO data.") + }).boxed() +} \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit_tests/proptest.rs b/nalgebra-sparse/tests/unit_tests/proptest.rs new file mode 100644 index 00000000..e69de29b From 7260f05b07d25dcc6796577c8b9289b610ce6b53 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Wed, 18 Nov 2020 13:54:14 +0100 Subject: [PATCH 025/183] Improved CooMatrix proptest strategies --- nalgebra-sparse/Cargo.toml | 6 + nalgebra-sparse/src/coo.rs | 2 +- nalgebra-sparse/src/proptest.rs | 193 ++++++++++++++++--- nalgebra-sparse/tests/unit.rs | 3 + nalgebra-sparse/tests/unit_tests/mod.rs | 3 +- nalgebra-sparse/tests/unit_tests/proptest.rs | 134 +++++++++++++ 6 files changed, 312 insertions(+), 29 deletions(-) diff --git a/nalgebra-sparse/Cargo.toml b/nalgebra-sparse/Cargo.toml index bf0d3849..3a4f08c1 100644 --- a/nalgebra-sparse/Cargo.toml +++ b/nalgebra-sparse/Cargo.toml @@ -7,7 +7,13 @@ edition = "2018" [features] proptest-support = ["proptest", "nalgebra/proptest"] +# Enable to enable running some tests that take a lot of time to run +slow-tests = [] + [dependencies] nalgebra = { version="0.23", path = "../" } num-traits = { version = "0.2", default-features = false } proptest = { version = "0.10", optional = true } + +[dev-dependencies] +itertools = "0.9" diff --git a/nalgebra-sparse/src/coo.rs b/nalgebra-sparse/src/coo.rs index a22f65cd..9869d502 100644 --- a/nalgebra-sparse/src/coo.rs +++ b/nalgebra-sparse/src/coo.rs @@ -37,7 +37,7 @@ use num_traits::Zero; /// /// // TODO: Convert to CSR /// ``` -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct CooMatrix { nrows: usize, ncols: usize, diff --git a/nalgebra-sparse/src/proptest.rs b/nalgebra-sparse/src/proptest.rs index 59d832e7..fae49028 100644 --- a/nalgebra-sparse/src/proptest.rs +++ b/nalgebra-sparse/src/proptest.rs @@ -4,38 +4,177 @@ use crate::coo::CooMatrix; use proptest::prelude::*; -use proptest::collection::{SizeRange, vec}; +use proptest::collection::{vec, hash_map}; use nalgebra::Scalar; +use std::cmp::min; +use std::iter::repeat; +use proptest::sample::{Index}; -/// TODO -pub fn coo( - value_strategy: T, - rows: impl Strategy + 'static, - cols: impl Strategy + 'static, - max_nonzeros: usize) -> BoxedStrategy> +/// A strategy for generating `nnz` triplets. +/// +/// This strategy should generally only be used when `nnz` is close to `nrows * ncols`. +fn dense_triplet_strategy(value_strategy: T, + nrows: usize, + ncols: usize, + nnz: usize) + -> impl Strategy> where T: Strategy + Clone + 'static, T::Value: Scalar, { - (rows, cols, (0 ..= max_nonzeros)) - .prop_flat_map(move |(nrows, ncols, nnz)| { - // If the numbers of rows and columns are small in comparison with the - // max nnz, it will lead to small matrices essentially always turning out to be dense. - // To address this, we correct the nnz by computing the modulo with the - // maximum number of non-zeros (ignoring duplicates) we can have for - // the given dimensions. - // This way we can still generate very sparse matrices for small matrices. - let max_nnz = nrows * ncols; - let nnz = if max_nnz == 0 { 0 } else { nnz % max_nnz }; - let row_index_strategy = if nrows > 0 { 0 .. nrows } else { 0 .. 1 }; - let col_index_strategy = if ncols > 0 { 0 .. ncols } else { 0 .. 1 }; - let row_indices = vec![row_index_strategy.clone(); nnz]; - let col_indices = vec![col_index_strategy.clone(); nnz]; - let values_strategy = vec![value_strategy.clone(); nnz]; + assert!(nnz <= nrows * ncols); - (Just(nrows), Just(ncols), row_indices, col_indices, values_strategy) - }).prop_map(|(nrows, ncols, row_indices, col_indices, values)| { - CooMatrix::try_from_triplets(nrows, ncols, row_indices, col_indices, values) - .expect("We should always generate valid COO data.") - }).boxed() + // Construct a number of booleans of which exactly `nnz` are true. + let booleans: Vec<_> = repeat(true) + .take(nnz) + .chain(repeat(false)) + .take(nrows * ncols) + .collect(); + + Just(booleans) + // Shuffle the booleans so that they are randomly distributed + .prop_shuffle() + // Convert the booleans into a list of coordinate pairs + .prop_map(move |booleans| { + booleans + .into_iter() + .enumerate() + .filter_map(|(index, is_entry)| { + if is_entry { + // Convert linear index to row/col pair + let i = index / ncols; + let j = index % ncols; + Some((i, j)) + } else { + None + } + }) + .collect::>() + }) + // Assign values to each coordinate pair in order to generate a list of triplets + .prop_flat_map(move |coords| { + vec![value_strategy.clone(); coords.len()] + .prop_map(move |values| { + coords.clone().into_iter() + .zip(values) + .map(|((i, j), v)| { + (i, j, v) + }) + .collect::>() + }) + }) +} + +/// A strategy for generating `nnz` triplets. +/// +/// This strategy should generally only be used when `nnz << nrows * ncols`. If `nnz` is too +/// close to `nrows * ncols` it may fail due to excessive rejected samples. +fn sparse_triplet_strategy(value_strategy: T, + nrows: usize, + ncols: usize, + nnz: usize) + -> impl Strategy> + where + T: Strategy + Clone + 'static, + T::Value: Scalar, +{ + // Have to handle the zero case: proptest doesn't like empty ranges (i.e. 0 .. 0) + let row_index_strategy = if nrows > 0 { 0 .. nrows } else { 0 .. 1 }; + let col_index_strategy = if ncols > 0 { 0 .. ncols } else { 0 .. 1 }; + let coord_strategy = (row_index_strategy, col_index_strategy); + hash_map(coord_strategy, value_strategy.clone(), nnz) + .prop_map(|hash_map| { + let triplets: Vec<_> = hash_map + .into_iter() + .map(|((i, j), v)| (i, j, v)) + .collect(); + triplets + }) + // Although order in the hash map is unspecified, it's not necessarily *random* + // - or, in particular, it does not necessarily sample the whole space of possible outcomes - + // so we additionally shuffle the triplets + .prop_shuffle() +} + +/// TODO +pub fn coo_no_duplicates( + value_strategy: T, + rows: impl Strategy + 'static, + cols: impl Strategy + 'static, + max_nonzeros: usize) -> impl Strategy> +where + T: Strategy + Clone + 'static, + T::Value: Scalar, +{ + (rows, cols) + .prop_flat_map(move |(nrows, ncols)| { + let max_nonzeros = min(max_nonzeros, nrows * ncols); + let size_range = 0 ..= max_nonzeros; + let value_strategy = value_strategy.clone(); + + size_range.prop_flat_map(move |nnz| { + let value_strategy = value_strategy.clone(); + if nnz as f64 > 0.10 * (nrows as f64) * (ncols as f64) { + // If the number of nnz is sufficiently dense, then use the dense + // sample strategy + dense_triplet_strategy(value_strategy, nrows, ncols, nnz).boxed() + } else { + // Otherwise, use a hash map strategy so that we can get a sparse sampling + // (so that complexity is rather on the order of max_nnz than nrows * ncols) + sparse_triplet_strategy(value_strategy, nrows, ncols, nnz).boxed() + } + }) + .prop_map(move |triplets| { + let mut coo = CooMatrix::new(nrows, ncols); + for (i, j, v) in triplets { + coo.push(i, j, v); + } + coo + }) + }) +} + +/// TODO +/// +/// TODO: Write note on how this strategy only maintains the constraints on values +/// for each triplet, but does not consider the sum of triplets +pub fn coo_with_duplicates( + value_strategy: T, + rows: impl Strategy + 'static, + cols: impl Strategy + 'static, + max_nonzeros: usize, + max_duplicates: usize) + -> impl Strategy> +where + T: Strategy + Clone + 'static, + T::Value: Scalar, +{ + let coo_strategy = coo_no_duplicates(value_strategy.clone(), rows, cols, max_nonzeros); + let duplicate_strategy = vec((any::(), value_strategy.clone()), 0 ..= max_duplicates); + (coo_strategy, duplicate_strategy) + .prop_flat_map(|(coo, duplicates)| { + let mut triplets: Vec<(usize, usize, T::Value)> = coo.triplet_iter() + .map(|(i, j, v)| (i, j, v.clone())) + .collect(); + if !triplets.is_empty() { + let duplicates_iter: Vec<_> = duplicates + .into_iter() + .map(|(idx, val)| { + let (i, j, _) = idx.get(&triplets); + (*i, *j, val) + }) + .collect(); + triplets.extend(duplicates_iter); + } + // Make sure to shuffle so that the duplicates get mixed in with the non-duplicates + let shuffled = Just(triplets).prop_shuffle(); + (Just(coo.nrows()), Just(coo.ncols()), shuffled) + }) + .prop_map(move |(nrows, ncols, triplets)| { + let mut coo = CooMatrix::new(nrows, ncols); + for (i, j, v) in triplets { + coo.push(i, j, v); + } + coo + }) } \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit.rs b/nalgebra-sparse/tests/unit.rs index ee44e73b..b7badffd 100644 --- a/nalgebra-sparse/tests/unit.rs +++ b/nalgebra-sparse/tests/unit.rs @@ -1,4 +1,7 @@ //! Unit tests +#[cfg(not(feature = "proptest-support"))] +compile_error!("Tests must be run with feature proptest-support"); + mod unit_tests; #[macro_use] diff --git a/nalgebra-sparse/tests/unit_tests/mod.rs b/nalgebra-sparse/tests/unit_tests/mod.rs index 733357c9..f5b6d935 100644 --- a/nalgebra-sparse/tests/unit_tests/mod.rs +++ b/nalgebra-sparse/tests/unit_tests/mod.rs @@ -2,4 +2,5 @@ mod coo; mod ops; mod pattern; mod csr; -mod csc; \ No newline at end of file +mod csc; +mod proptest; \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit_tests/proptest.rs b/nalgebra-sparse/tests/unit_tests/proptest.rs index e69de29b..23afa741 100644 --- a/nalgebra-sparse/tests/unit_tests/proptest.rs +++ b/nalgebra-sparse/tests/unit_tests/proptest.rs @@ -0,0 +1,134 @@ +use nalgebra_sparse::proptest::{coo_with_duplicates, coo_no_duplicates}; +use nalgebra::DMatrix; + +use proptest::prelude::*; +use itertools::Itertools; + +use std::collections::HashSet; +use std::iter::repeat; + +#[cfg(feature = "slow-tests")] +use { + proptest::test_runner::TestRunner, + proptest::strategy::ValueTree +}; +use std::ops::RangeInclusive; + +#[cfg(feature = "slow-tests")] +fn generate_all_possible_matrices(value_range: RangeInclusive, + rows_range: RangeInclusive, + cols_range: RangeInclusive) + -> HashSet> +{ + // Enumerate all possible combinations + let mut all_combinations = HashSet::new(); + for nrows in rows_range { + for ncols in cols_range.clone() { + // For the given number of rows and columns + let n_values = nrows * ncols; + + if n_values == 0 { + // If we have zero rows or columns, the set of matrices with the given + // rows and columns is a single element: an empty matrix + all_combinations.insert(DMatrix::from_row_slice(nrows, ncols, &[])); + } else { + // Otherwise, we need to sample all possible matrices. + // To do this, we generate the values as the (multi) Cartesian product + // of the value sets. For example, for a 2x2 matrices, we consider + // all possible 4-element arrays that the matrices can take by + // considering all elements in the cartesian product + // V x V x V x V + // where V is the set of eligible values, e.g. V := -1 ..= 1 + let values_iter = repeat(value_range.clone()) + .take(n_values) + .multi_cartesian_product(); + for matrix_values in values_iter { + all_combinations.insert(DMatrix::from_row_slice(nrows, ncols, &matrix_values)); + } + } + } + } + all_combinations +} + +#[cfg(feature = "slow-tests")] +#[test] +fn coo_no_duplicates_samples_all_admissible_outputs() { + // Note: This test basically mirrors a similar test for `matrix` in the `nalgebra` repo. + + // Test that the proptest generation covers all possible outputs for a small space of inputs + // given enough samples. + + // We use a deterministic test runner to make the test "stable". + let mut runner = TestRunner::deterministic(); + + // This number needs to be high enough so that we with high probability sample + // all possible cases + let num_generated_matrices = 500000; + + let values = -1..=1; + let rows = 0..=2; + let cols = 0..=3; + let strategy = coo_no_duplicates(values.clone(), rows.clone(), cols.clone(), 2 * 3); + + // Enumerate all possible combinations + let all_combinations = generate_all_possible_matrices(values, rows, cols); + + let mut visited_combinations = HashSet::new(); + for _ in 0..num_generated_matrices { + let tree = strategy + .new_tree(&mut runner) + .expect("Tree generation should not fail"); + let matrix = tree.current(); + visited_combinations.insert(DMatrix::from(&matrix)); + } + + assert_eq!(visited_combinations.len(), all_combinations.len()); + assert_eq!(visited_combinations, all_combinations, "Did not sample all possible values."); +} + +#[cfg(feature = "slow-tests")] +#[test] +fn coo_with_duplicates_samples_all_admissible_outputs() { + // This is almost the same as the test for coo_no_duplicates, except that we need + // a different "success" criterion, since coo_with_duplicates is able to generate + // matrices with values outside of the value constraints. See below for details. + + // We use a deterministic test runner to make the test "stable". + let mut runner = TestRunner::deterministic(); + + // This number needs to be high enough so that we with high probability sample + // all possible cases + let num_generated_matrices = 500000; + + let values = -1..=1; + let rows = 0..=2; + let cols = 0..=3; + let strategy = coo_with_duplicates(values.clone(), rows.clone(), cols.clone(), 2 * 3, 2); + + // Enumerate all possible combinations that fit the constraints + // (note: this is only a subset of the matrices that can be generated by + // `coo_with_duplicates`) + let all_combinations = generate_all_possible_matrices(values, rows, cols); + + let mut visited_combinations = HashSet::new(); + for _ in 0..num_generated_matrices { + let tree = strategy + .new_tree(&mut runner) + .expect("Tree generation should not fail"); + let matrix = tree.current(); + visited_combinations.insert(DMatrix::from(&matrix)); + } + + // Here we cannot verify that the set of visited combinations is *equal* to + // all possible outcomes with the given constraints, however the + // strategy should be able to generate all matrices that fit the constraints. + // In other words, we need to determine that set of all admissible matrices + // is contained in the set of visited matrices + assert!(all_combinations.is_subset(&visited_combinations)); +} + +#[test] +fn coo_no_duplicates_generates_admissible_matrices() { + +} \ No newline at end of file From 54329146c98e801f8644fa47073d27130f2a396f Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Wed, 18 Nov 2020 14:14:38 +0100 Subject: [PATCH 026/183] Initial COO <-> Dense conversion routines --- nalgebra-sparse/src/convert/impl_std_ops.rs | 26 ++++++ nalgebra-sparse/src/convert/mod.rs | 5 ++ nalgebra-sparse/src/convert/serial.rs | 48 +++++++++++ nalgebra-sparse/src/coo.rs | 2 + nalgebra-sparse/src/lib.rs | 6 ++ .../tests/unit_tests/convert_serial.rs | 84 +++++++++++++++++++ nalgebra-sparse/tests/unit_tests/mod.rs | 1 + 7 files changed, 172 insertions(+) create mode 100644 nalgebra-sparse/src/convert/impl_std_ops.rs create mode 100644 nalgebra-sparse/src/convert/mod.rs create mode 100644 nalgebra-sparse/src/convert/serial.rs create mode 100644 nalgebra-sparse/tests/unit_tests/convert_serial.rs diff --git a/nalgebra-sparse/src/convert/impl_std_ops.rs b/nalgebra-sparse/src/convert/impl_std_ops.rs new file mode 100644 index 00000000..1ba52bd0 --- /dev/null +++ b/nalgebra-sparse/src/convert/impl_std_ops.rs @@ -0,0 +1,26 @@ +use crate::coo::CooMatrix; +use crate::convert::serial::{convert_dense_coo, convert_coo_dense}; +use nalgebra::{Matrix, Scalar, Dim, ClosedAdd, DMatrix}; +use nalgebra::storage::{Storage}; +use num_traits::Zero; + +impl<'a, T, R, C, S> From<&'a Matrix> for CooMatrix +where + T: Scalar + Zero, + R: Dim, + C: Dim, + S: Storage +{ + fn from(matrix: &'a Matrix) -> Self { + convert_dense_coo(matrix) + } +} + +impl<'a, T> From<&'a CooMatrix> for DMatrix +where + T: Scalar + Zero + ClosedAdd, +{ + fn from(coo: &'a CooMatrix) -> Self { + convert_coo_dense(coo) + } +} \ No newline at end of file diff --git a/nalgebra-sparse/src/convert/mod.rs b/nalgebra-sparse/src/convert/mod.rs new file mode 100644 index 00000000..cf627dfa --- /dev/null +++ b/nalgebra-sparse/src/convert/mod.rs @@ -0,0 +1,5 @@ +//! TODO + +pub mod serial; + +mod impl_std_ops; diff --git a/nalgebra-sparse/src/convert/serial.rs b/nalgebra-sparse/src/convert/serial.rs new file mode 100644 index 00000000..e8121b30 --- /dev/null +++ b/nalgebra-sparse/src/convert/serial.rs @@ -0,0 +1,48 @@ +//! TODO +use nalgebra::{DMatrix, Scalar, Matrix, Dim}; +use crate::coo::CooMatrix; +use crate::csr::CsrMatrix; +use num_traits::Zero; +use std::ops::{Add, AddAssign}; +use nalgebra::storage::Storage; + +/// TODO +pub fn convert_dense_coo(dense: &Matrix) -> CooMatrix +where + T: Scalar + Zero, + R: Dim, + C: Dim, + S: Storage +{ + let mut coo = CooMatrix::new(dense.nrows(), dense.ncols()); + + for (index, v) in dense.iter().enumerate() { + if v != &T::zero() { + // We use the fact that matrix iteration is guaranteed to be column-major + let i = index % dense.nrows(); + let j = index / dense.nrows(); + coo.push(i, j, v.inlined_clone()); + } + } + + coo +} + +/// TODO +/// +/// TODO: What should the actual trait bounds be? +pub fn convert_coo_dense(coo: &CooMatrix) -> DMatrix +where + T: Scalar + Zero + Add + AddAssign, +{ + let mut output = DMatrix::repeat(coo.nrows(), coo.ncols(), T::zero()); + for (i, j, v) in coo.triplet_iter() { + output[(i, j)] += v.inlined_clone(); + } + output +} + +/// TODO +pub fn convert_coo_csr(_coo: &CooMatrix) -> CsrMatrix { + todo!() +} \ No newline at end of file diff --git a/nalgebra-sparse/src/coo.rs b/nalgebra-sparse/src/coo.rs index 9869d502..8206cb09 100644 --- a/nalgebra-sparse/src/coo.rs +++ b/nalgebra-sparse/src/coo.rs @@ -186,6 +186,8 @@ where /// Construct the dense representation of the COO matrix. /// /// Duplicate entries are summed together. + /// + /// TODO: Remove? pub fn to_dense(&self) -> DMatrix where T: ClosedAdd + Zero, diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index 53bc846a..d1376b55 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -68,6 +68,11 @@ //! - Cholesky factorization (port existing factorization from nalgebra's sparse module) //! //! +//! TODO: Write docs on the following: +//! +//! - Overall design ("easy API" vs. "expert" API etc.) +//! - Conversions (From, explicit "expert" API etc.) +//! - Matrix ops design #![deny(non_camel_case_types)] #![deny(unused_parens)] #![deny(non_upper_case_globals)] @@ -80,6 +85,7 @@ pub mod csc; pub mod csr; pub mod pattern; pub mod ops; +pub mod convert; #[cfg(feature = "proptest-support")] pub mod proptest; diff --git a/nalgebra-sparse/tests/unit_tests/convert_serial.rs b/nalgebra-sparse/tests/unit_tests/convert_serial.rs new file mode 100644 index 00000000..21a93364 --- /dev/null +++ b/nalgebra-sparse/tests/unit_tests/convert_serial.rs @@ -0,0 +1,84 @@ +use nalgebra_sparse::coo::CooMatrix; +use nalgebra_sparse::convert::serial::{convert_coo_dense, convert_dense_coo}; +use nalgebra_sparse::proptest::coo_with_duplicates; +use nalgebra::proptest::matrix; +use proptest::prelude::*; +use nalgebra::DMatrix; + +#[test] +fn test_convert_dense_coo() { + // No duplicates + { + #[rustfmt::skip] + let entries = &[1, 0, 3, + 0, 5, 0]; + // The COO representation of a dense matrix is not unique. + // Here we implicitly test that the coo matrix is indeed constructed from column-major + // iteration of the dense matrix. + let dense = DMatrix::from_row_slice(2, 3, entries); + let coo = CooMatrix::try_from_triplets(2, 3, vec![0, 1, 0], vec![0, 1, 2], vec![1, 5, 3]) + .unwrap(); + + assert_eq!(CooMatrix::from(&dense), coo); + assert_eq!(DMatrix::from(&coo), dense); + } + + // Duplicates + // No duplicates + { + #[rustfmt::skip] + let entries = &[1, 0, 3, + 0, 5, 0]; + // The COO representation of a dense matrix is not unique. + // Here we implicitly test that the coo matrix is indeed constructed from column-major + // iteration of the dense matrix. + let dense = DMatrix::from_row_slice(2, 3, entries); + let coo_no_dup = CooMatrix::try_from_triplets(2, 3, + vec![0, 1, 0], + vec![0, 1, 2], + vec![1, 5, 3]) + .unwrap(); + let coo_dup = CooMatrix::try_from_triplets(2, 3, + vec![0, 1, 0, 1], + vec![0, 1, 2, 1], + vec![1, -2, 3, 7]) + .unwrap(); + + assert_eq!(CooMatrix::from(&dense), coo_no_dup); + assert_eq!(DMatrix::from(&coo_dup), dense); + } +} + +fn coo_strategy() -> impl Strategy> { + coo_with_duplicates(-5 ..= 5, 0..=6usize, 0..=6usize, 40, 2) +} + +proptest! { + + #[test] + fn convert_dense_coo_roundtrip(dense in matrix(-5 ..= 5, 0 ..=6, 0..=6)) { + let coo = convert_dense_coo(&dense); + let dense2 = convert_coo_dense(&coo); + prop_assert_eq!(&dense, &dense2); + } + + #[test] + fn convert_coo_dense_coo_roundtrip(coo in coo_strategy()) { + // We cannot compare the result of the roundtrip coo -> dense -> coo directly for + // two reasons: + // 1. the COO matrices will generally have different ordering of elements + // 2. explicitly stored zero entries in the original matrix will be discarded + // when converting back to COO + // Therefore we instead compare the results of converting the COO matrix + // at the end of the roundtrip with its dense representation + let dense = convert_coo_dense(&coo); + let coo2 = convert_dense_coo(&dense); + let dense2 = convert_coo_dense(&coo2); + prop_assert_eq!(dense, dense2); + } + + #[test] + fn from_dense_coo_roundtrip(dense in matrix(-5..=5, 0..=6, 0..=6)) { + prop_assert_eq!(&dense, &DMatrix::from(&CooMatrix::from(&dense))); + } +} \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit_tests/mod.rs b/nalgebra-sparse/tests/unit_tests/mod.rs index f5b6d935..cb32cd71 100644 --- a/nalgebra-sparse/tests/unit_tests/mod.rs +++ b/nalgebra-sparse/tests/unit_tests/mod.rs @@ -3,4 +3,5 @@ mod ops; mod pattern; mod csr; mod csc; +mod convert_serial; mod proptest; \ No newline at end of file From f20e764edc84eca9a97a3118f209c6a7b36192b9 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 23 Nov 2020 10:16:18 +0100 Subject: [PATCH 027/183] nalgebra-sparse: Move slow tests and imports into 'slow' child module This way it's easier to prevent accidental unused imports when running tests without the slow-tests feature. --- nalgebra-sparse/tests/unit_tests/proptest.rs | 228 +++++++++---------- 1 file changed, 114 insertions(+), 114 deletions(-) diff --git a/nalgebra-sparse/tests/unit_tests/proptest.rs b/nalgebra-sparse/tests/unit_tests/proptest.rs index 23afa741..72a3d40c 100644 --- a/nalgebra-sparse/tests/unit_tests/proptest.rs +++ b/nalgebra-sparse/tests/unit_tests/proptest.rs @@ -1,134 +1,134 @@ -use nalgebra_sparse::proptest::{coo_with_duplicates, coo_no_duplicates}; -use nalgebra::DMatrix; - -use proptest::prelude::*; -use itertools::Itertools; - -use std::collections::HashSet; -use std::iter::repeat; +#[test] +#[ignore] +fn coo_no_duplicates_generates_admissible_matrices() { + //TODO +} #[cfg(feature = "slow-tests")] -use { - proptest::test_runner::TestRunner, - proptest::strategy::ValueTree -}; -use std::ops::RangeInclusive; +mod slow { + use nalgebra_sparse::proptest::{coo_with_duplicates, coo_no_duplicates}; + use nalgebra::DMatrix; -#[cfg(feature = "slow-tests")] -fn generate_all_possible_matrices(value_range: RangeInclusive, - rows_range: RangeInclusive, - cols_range: RangeInclusive) - -> HashSet> -{ - // Enumerate all possible combinations - let mut all_combinations = HashSet::new(); - for nrows in rows_range { - for ncols in cols_range.clone() { - // For the given number of rows and columns - let n_values = nrows * ncols; + use proptest::test_runner::TestRunner; + use proptest::strategy::ValueTree; + use itertools::Itertools; - if n_values == 0 { - // If we have zero rows or columns, the set of matrices with the given - // rows and columns is a single element: an empty matrix - all_combinations.insert(DMatrix::from_row_slice(nrows, ncols, &[])); - } else { - // Otherwise, we need to sample all possible matrices. - // To do this, we generate the values as the (multi) Cartesian product - // of the value sets. For example, for a 2x2 matrices, we consider - // all possible 4-element arrays that the matrices can take by - // considering all elements in the cartesian product - // V x V x V x V - // where V is the set of eligible values, e.g. V := -1 ..= 1 - let values_iter = repeat(value_range.clone()) - .take(n_values) - .multi_cartesian_product(); - for matrix_values in values_iter { - all_combinations.insert(DMatrix::from_row_slice(nrows, ncols, &matrix_values)); + use proptest::prelude::*; + + use std::collections::HashSet; + use std::iter::repeat; + use std::ops::RangeInclusive; + + fn generate_all_possible_matrices(value_range: RangeInclusive, + rows_range: RangeInclusive, + cols_range: RangeInclusive) + -> HashSet> + { + // Enumerate all possible combinations + let mut all_combinations = HashSet::new(); + for nrows in rows_range { + for ncols in cols_range.clone() { + // For the given number of rows and columns + let n_values = nrows * ncols; + + if n_values == 0 { + // If we have zero rows or columns, the set of matrices with the given + // rows and columns is a single element: an empty matrix + all_combinations.insert(DMatrix::from_row_slice(nrows, ncols, &[])); + } else { + // Otherwise, we need to sample all possible matrices. + // To do this, we generate the values as the (multi) Cartesian product + // of the value sets. For example, for a 2x2 matrices, we consider + // all possible 4-element arrays that the matrices can take by + // considering all elements in the cartesian product + // V x V x V x V + // where V is the set of eligible values, e.g. V := -1 ..= 1 + let values_iter = repeat(value_range.clone()) + .take(n_values) + .multi_cartesian_product(); + for matrix_values in values_iter { + all_combinations.insert(DMatrix::from_row_slice(nrows, ncols, &matrix_values)); + } } } } - } - all_combinations -} - -#[cfg(feature = "slow-tests")] -#[test] -fn coo_no_duplicates_samples_all_admissible_outputs() { - // Note: This test basically mirrors a similar test for `matrix` in the `nalgebra` repo. - - // Test that the proptest generation covers all possible outputs for a small space of inputs - // given enough samples. - - // We use a deterministic test runner to make the test "stable". - let mut runner = TestRunner::deterministic(); - - // This number needs to be high enough so that we with high probability sample - // all possible cases - let num_generated_matrices = 500000; - - let values = -1..=1; - let rows = 0..=2; - let cols = 0..=3; - let strategy = coo_no_duplicates(values.clone(), rows.clone(), cols.clone(), 2 * 3); - - // Enumerate all possible combinations - let all_combinations = generate_all_possible_matrices(values, rows, cols); - - let mut visited_combinations = HashSet::new(); - for _ in 0..num_generated_matrices { - let tree = strategy - .new_tree(&mut runner) - .expect("Tree generation should not fail"); - let matrix = tree.current(); - visited_combinations.insert(DMatrix::from(&matrix)); + all_combinations } - assert_eq!(visited_combinations.len(), all_combinations.len()); - assert_eq!(visited_combinations, all_combinations, "Did not sample all possible values."); -} + #[cfg(feature = "slow-tests")] + #[test] + fn coo_no_duplicates_samples_all_admissible_outputs() { + // Note: This test basically mirrors a similar test for `matrix` in the `nalgebra` repo. -#[cfg(feature = "slow-tests")] -#[test] -fn coo_with_duplicates_samples_all_admissible_outputs() { - // This is almost the same as the test for coo_no_duplicates, except that we need - // a different "success" criterion, since coo_with_duplicates is able to generate - // matrices with values outside of the value constraints. See below for details. + // Test that the proptest generation covers all possible outputs for a small space of inputs + // given enough samples. - // We use a deterministic test runner to make the test "stable". - let mut runner = TestRunner::deterministic(); + // We use a deterministic test runner to make the test "stable". + let mut runner = TestRunner::deterministic(); - // This number needs to be high enough so that we with high probability sample - // all possible cases - let num_generated_matrices = 500000; + // This number needs to be high enough so that we with high probability sample + // all possible cases + let num_generated_matrices = 500000; - let values = -1..=1; - let rows = 0..=2; - let cols = 0..=3; - let strategy = coo_with_duplicates(values.clone(), rows.clone(), cols.clone(), 2 * 3, 2); + let values = -1..=1; + let rows = 0..=2; + let cols = 0..=3; + let strategy = coo_no_duplicates(values.clone(), rows.clone(), cols.clone(), 2 * 3); - // Enumerate all possible combinations that fit the constraints - // (note: this is only a subset of the matrices that can be generated by - // `coo_with_duplicates`) - let all_combinations = generate_all_possible_matrices(values, rows, cols); + // Enumerate all possible combinations + let all_combinations = generate_all_possible_matrices(values, rows, cols); - let mut visited_combinations = HashSet::new(); - for _ in 0..num_generated_matrices { - let tree = strategy - .new_tree(&mut runner) - .expect("Tree generation should not fail"); - let matrix = tree.current(); - visited_combinations.insert(DMatrix::from(&matrix)); + let mut visited_combinations = HashSet::new(); + for _ in 0..num_generated_matrices { + let tree = strategy + .new_tree(&mut runner) + .expect("Tree generation should not fail"); + let matrix = tree.current(); + visited_combinations.insert(DMatrix::from(&matrix)); + } + + assert_eq!(visited_combinations.len(), all_combinations.len()); + assert_eq!(visited_combinations, all_combinations, "Did not sample all possible values."); } - // Here we cannot verify that the set of visited combinations is *equal* to - // all possible outcomes with the given constraints, however the - // strategy should be able to generate all matrices that fit the constraints. - // In other words, we need to determine that set of all admissible matrices - // is contained in the set of visited matrices - assert!(all_combinations.is_subset(&visited_combinations)); -} + #[cfg(feature = "slow-tests")] + #[test] + fn coo_with_duplicates_samples_all_admissible_outputs() { + // This is almost the same as the test for coo_no_duplicates, except that we need + // a different "success" criterion, since coo_with_duplicates is able to generate + // matrices with values outside of the value constraints. See below for details. -#[test] -fn coo_no_duplicates_generates_admissible_matrices() { + // We use a deterministic test runner to make the test "stable". + let mut runner = TestRunner::deterministic(); + // This number needs to be high enough so that we with high probability sample + // all possible cases + let num_generated_matrices = 500000; + + let values = -1..=1; + let rows = 0..=2; + let cols = 0..=3; + let strategy = coo_with_duplicates(values.clone(), rows.clone(), cols.clone(), 2 * 3, 2); + + // Enumerate all possible combinations that fit the constraints + // (note: this is only a subset of the matrices that can be generated by + // `coo_with_duplicates`) + let all_combinations = generate_all_possible_matrices(values, rows, cols); + + let mut visited_combinations = HashSet::new(); + for _ in 0..num_generated_matrices { + let tree = strategy + .new_tree(&mut runner) + .expect("Tree generation should not fail"); + let matrix = tree.current(); + visited_combinations.insert(DMatrix::from(&matrix)); + } + + // Here we cannot verify that the set of visited combinations is *equal* to + // all possible outcomes with the given constraints, however the + // strategy should be able to generate all matrices that fit the constraints. + // In other words, we need to determine that set of all admissible matrices + // is contained in the set of visited matrices + assert!(all_combinations.is_subset(&visited_combinations)); + } } \ No newline at end of file From 41ce9a23dfc4321618322bc3c901b9203ebf67e2 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 23 Nov 2020 15:58:02 +0100 Subject: [PATCH 028/183] Remove CooMatrix::to_dense() and `Scalar` trait bound, add ::nnz() --- nalgebra-sparse/src/coo.rs | 32 ++++++++----------------- nalgebra-sparse/tests/unit_tests/coo.rs | 6 ++--- nalgebra-sparse/tests/unit_tests/ops.rs | 4 ++-- 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/nalgebra-sparse/src/coo.rs b/nalgebra-sparse/src/coo.rs index 8206cb09..43662da3 100644 --- a/nalgebra-sparse/src/coo.rs +++ b/nalgebra-sparse/src/coo.rs @@ -1,8 +1,6 @@ //! An implementation of the COO sparse matrix format. use crate::SparseFormatError; -use nalgebra::{ClosedAdd, DMatrix, Scalar}; -use num_traits::Zero; /// A COO representation of a sparse matrix. /// @@ -47,8 +45,6 @@ pub struct CooMatrix { } impl CooMatrix -where - T: Scalar, { /// Construct a zero COO matrix of the given dimensions. /// @@ -146,6 +142,16 @@ where self.ncols } + /// The number of explicitly stored entries in the matrix. + /// + /// This number *includes* duplicate entries. For example, if the `CooMatrix` contains duplicate + /// entries, then it may have a different number of non-zeros as reported by `nnz()` compared + /// to its CSR representation. + #[inline] + pub fn nnz(&self) -> usize { + self.values.len() + } + /// The row indices of the explicitly stored entries. pub fn row_indices(&self) -> &[usize] { &self.row_indices @@ -182,22 +188,4 @@ where pub fn disassemble(self) -> (Vec, Vec, Vec) { (self.row_indices, self.col_indices, self.values) } - - /// Construct the dense representation of the COO matrix. - /// - /// Duplicate entries are summed together. - /// - /// TODO: Remove? - pub fn to_dense(&self) -> DMatrix - where - T: ClosedAdd + Zero, - { - let mut result = DMatrix::zeros(self.nrows, self.ncols); - - for (i, j, v) in self.triplet_iter() { - result[(i, j)] += v.clone(); - } - - result - } } diff --git a/nalgebra-sparse/tests/unit_tests/coo.rs b/nalgebra-sparse/tests/unit_tests/coo.rs index 7bf0c05c..3373d89a 100644 --- a/nalgebra-sparse/tests/unit_tests/coo.rs +++ b/nalgebra-sparse/tests/unit_tests/coo.rs @@ -19,7 +19,7 @@ fn coo_construction_for_valid_data() { assert!(coo.col_indices().is_empty()); assert!(coo.values().is_empty()); - assert_eq!(coo.to_dense(), DMatrix::repeat(3, 2, 0)); + assert_eq!(DMatrix::from(&coo), DMatrix::repeat(3, 2, 0)); } { @@ -51,7 +51,7 @@ fn coo_construction_for_valid_data() { 0, 0, 3, 0, 0, 0, 0, 0, 1, 0 ]); - assert_eq!(coo.to_dense(), expected_dense); + assert_eq!(DMatrix::from(&coo), expected_dense); } { @@ -83,7 +83,7 @@ fn coo_construction_for_valid_data() { 0, 0, 8, 0, 0, 0, 0, 0, 1, 0 ]); - assert_eq!(coo.to_dense(), expected_dense); + assert_eq!(DMatrix::from(&coo), expected_dense); } } diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index a2c06b96..01df60b6 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -1,6 +1,6 @@ use nalgebra_sparse::coo::CooMatrix; use nalgebra_sparse::ops::spmv_coo; -use nalgebra::DVector; +use nalgebra::{DVector, DMatrix}; #[test] fn spmv_coo_agrees_with_dense_gemv() { @@ -20,7 +20,7 @@ fn spmv_coo_agrees_with_dense_gemv() { let mut y_dense = y.clone(); spmv_coo(beta, &mut y, alpha, &a, &x); - y_dense.gemv(alpha, &a.to_dense(), &x, beta); + y_dense.gemv(alpha, &DMatrix::from(&a), &x, beta); assert_eq!(y, y_dense); } From 6083d24dd6b4c580ea1f519a2e1624e00a8008ff Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 24 Nov 2020 17:34:19 +0100 Subject: [PATCH 029/183] Add csr, csc, sparsity_pattern proptest generators (untested) --- nalgebra-sparse/src/proptest.rs | 143 ++++++++++++++++++- nalgebra-sparse/tests/unit_tests/proptest.rs | 4 +- 2 files changed, 144 insertions(+), 3 deletions(-) diff --git a/nalgebra-sparse/src/proptest.rs b/nalgebra-sparse/src/proptest.rs index fae49028..80eb88a6 100644 --- a/nalgebra-sparse/src/proptest.rs +++ b/nalgebra-sparse/src/proptest.rs @@ -4,11 +4,42 @@ use crate::coo::CooMatrix; use proptest::prelude::*; -use proptest::collection::{vec, hash_map}; +use proptest::collection::{vec, hash_map, btree_set}; use nalgebra::Scalar; use std::cmp::min; -use std::iter::repeat; +use std::iter::{repeat}; use proptest::sample::{Index}; +use crate::csr::CsrMatrix; +use crate::pattern::SparsityPattern; +use std::sync::Arc; +use crate::csc::CscMatrix; + +fn dense_row_major_coord_strategy(nrows: usize, ncols: usize, nnz: usize) + -> impl Strategy> +{ + let mut booleans = vec![true; nnz]; + booleans.append(&mut vec![false; (nrows * ncols) - nnz]); + // Make sure that exactly `nnz` of the booleans are true + Just(booleans) + // Need to shuffle to make sure they are randomly distributed + .prop_shuffle() + .prop_map(move |booleans| { + booleans + .into_iter() + .enumerate() + .filter_map(|(index, is_entry)| { + if is_entry { + // Convert linear index to row/col pair + let i = index / ncols; + let j = index % ncols; + Some((i, j)) + } else { + None + } + }) + .collect::>() + }) +} /// A strategy for generating `nnz` triplets. /// @@ -177,4 +208,112 @@ where } coo }) +} + +fn sparsity_pattern_from_row_major_coords(nmajor: usize, nminor: usize, coords: I) -> SparsityPattern +where + I: Iterator + ExactSizeIterator, +{ + let mut minors = Vec::with_capacity(coords.len()); + let mut offsets = Vec::with_capacity(nmajor + 1); + let mut current_major = 0; + offsets.push(0); + for (idx, (i, j)) in coords.enumerate() { + assert!(i >= current_major); + assert!(i < nmajor && j < nminor, "Generated coords are out of bounds"); + while current_major < i{ + offsets.push(idx); + current_major += 1; + } + minors.push(j); + } + + while current_major < nmajor { + offsets.push(minors.len()); + current_major += 1; + } + + assert_eq!(offsets.first().unwrap(), &0); + assert_eq!(offsets.len(), nmajor + 1); + + SparsityPattern::try_from_offsets_and_indices(nmajor, + nminor, + offsets, + minors) + .expect("Internal error: Generated sparsity pattern is invalid") +} + +/// TODO +pub fn sparsity_pattern( + major_lanes: impl Strategy + 'static, + minor_lanes: impl Strategy + 'static, + max_nonzeros: usize) +-> impl Strategy +{ + (major_lanes, minor_lanes) + .prop_flat_map(move |(nmajor, nminor)| { + let max_nonzeros = min(nmajor * nminor, max_nonzeros); + (Just(nmajor), Just(nminor), 0 ..= max_nonzeros) + }).prop_flat_map(move |(nmajor, nminor, nnz)| { + if 10 * nnz < nmajor * nminor { + // If nnz is small compared to a dense matrix, then use a sparse sampling strategy + btree_set((0..nmajor, 0..nminor), nnz) + .prop_map(move |coords| { + sparsity_pattern_from_row_major_coords(nmajor, nminor, coords.into_iter()) + }) + .boxed() + } else { + // If the required number of nonzeros is sufficiently dense, + // we instead use a dense sampling + dense_row_major_coord_strategy(nmajor, nminor, nnz) + .prop_map(move |triplets| { + let coords = triplets.into_iter(); + sparsity_pattern_from_row_major_coords(nmajor, nminor, coords) + }).boxed() + } + }) +} + +/// TODO +pub fn csr(value_strategy: T, + rows: impl Strategy + 'static, + cols: impl Strategy + 'static, + max_nonzeros: usize) + -> impl Strategy> +where + T: Strategy + Clone + 'static, + T::Value: Scalar, +{ + sparsity_pattern(rows, cols, max_nonzeros) + .prop_flat_map(move |pattern| { + let nnz = pattern.nnz(); + let values = vec![value_strategy.clone(); nnz]; + (Just(pattern), values) + }) + .prop_map(|(pattern, values)| { + CsrMatrix::try_from_pattern_and_values(Arc::new(pattern), values) + .expect("Internal error: Generated CsrMatrix is invalid") + }) +} + +/// TODO +pub fn csc(value_strategy: T, + rows: impl Strategy + 'static, + cols: impl Strategy + 'static, + max_nonzeros: usize) + -> impl Strategy> + where + T: Strategy + Clone + 'static, + T::Value: Scalar, +{ + sparsity_pattern(cols, rows, max_nonzeros) + .prop_flat_map(move |pattern| { + let nnz = pattern.nnz(); + let values = vec![value_strategy.clone(); nnz]; + (Just(pattern), values) + }) + .prop_map(|(pattern, values)| { + CscMatrix::try_from_pattern_and_values(Arc::new(pattern), values) + .expect("Internal error: Generated CscMatrix is invalid") + }) } \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit_tests/proptest.rs b/nalgebra-sparse/tests/unit_tests/proptest.rs index 72a3d40c..8e5731e0 100644 --- a/nalgebra-sparse/tests/unit_tests/proptest.rs +++ b/nalgebra-sparse/tests/unit_tests/proptest.rs @@ -131,4 +131,6 @@ mod slow { // is contained in the set of visited matrices assert!(all_combinations.is_subset(&visited_combinations)); } -} \ No newline at end of file +} + +// TODO: Tests for csr, csc and sparsity_pattern strategies \ No newline at end of file From 95ee65fa8e01e5c6eb09e2beb309918a0664dfef Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 24 Nov 2020 17:36:03 +0100 Subject: [PATCH 030/183] Implement remaining conversions (missing docs) --- nalgebra-sparse/src/convert/impl_std_ops.rs | 100 +++- nalgebra-sparse/src/convert/serial.rs | 438 +++++++++++++++++- nalgebra-sparse/src/lib.rs | 19 +- .../tests/unit_tests/convert_serial.rs | 365 ++++++++++++++- 4 files changed, 904 insertions(+), 18 deletions(-) diff --git a/nalgebra-sparse/src/convert/impl_std_ops.rs b/nalgebra-sparse/src/convert/impl_std_ops.rs index 1ba52bd0..0a3fbdf6 100644 --- a/nalgebra-sparse/src/convert/impl_std_ops.rs +++ b/nalgebra-sparse/src/convert/impl_std_ops.rs @@ -1,8 +1,10 @@ use crate::coo::CooMatrix; -use crate::convert::serial::{convert_dense_coo, convert_coo_dense}; +use crate::convert::serial::*; use nalgebra::{Matrix, Scalar, Dim, ClosedAdd, DMatrix}; use nalgebra::storage::{Storage}; use num_traits::Zero; +use crate::csr::CsrMatrix; +use crate::csc::CscMatrix; impl<'a, T, R, C, S> From<&'a Matrix> for CooMatrix where @@ -23,4 +25,100 @@ where fn from(coo: &'a CooMatrix) -> Self { convert_coo_dense(coo) } +} + +impl<'a, T> From<&'a CooMatrix> for CsrMatrix +where + T: Scalar + Zero + ClosedAdd +{ + fn from(matrix: &'a CooMatrix) -> Self { + convert_coo_csr(matrix) + } +} + +impl<'a, T> From<&'a CsrMatrix> for CooMatrix +where + T: Scalar + Zero + ClosedAdd +{ + fn from(matrix: &'a CsrMatrix) -> Self { + convert_csr_coo(matrix) + } +} + +impl<'a, T, R, C, S> From<&'a Matrix> for CsrMatrix +where + T: Scalar + Zero, + R: Dim, + C: Dim, + S: Storage +{ + fn from(matrix: &'a Matrix) -> Self { + convert_dense_csr(matrix) + } +} + +impl<'a, T> From<&'a CsrMatrix> for DMatrix +where + T: Scalar + Zero + ClosedAdd +{ + fn from(matrix: &'a CsrMatrix) -> Self { + convert_csr_dense(matrix) + } +} + +impl<'a, T> From<&'a CooMatrix> for CscMatrix +where + T: Scalar + Zero + ClosedAdd +{ + fn from(matrix: &'a CooMatrix) -> Self { + convert_coo_csc(matrix) + } +} + +impl<'a, T> From<&'a CscMatrix> for CooMatrix +where + T: Scalar + Zero +{ + fn from(matrix: &'a CscMatrix) -> Self { + convert_csc_coo(matrix) + } +} + +impl<'a, T, R, C, S> From<&'a Matrix> for CscMatrix + where + T: Scalar + Zero, + R: Dim, + C: Dim, + S: Storage +{ + fn from(matrix: &'a Matrix) -> Self { + convert_dense_csc(matrix) + } +} + +impl<'a, T> From<&'a CscMatrix> for DMatrix + where + T: Scalar + Zero + ClosedAdd +{ + fn from(matrix: &'a CscMatrix) -> Self { + convert_csc_dense(matrix) + } +} + +impl<'a, T> From<&'a CscMatrix> for CsrMatrix + where + T: Scalar + Zero +{ + fn from(matrix: &'a CscMatrix) -> Self { + convert_csc_csr(matrix) + } +} + +impl<'a, T> From<&'a CsrMatrix> for CscMatrix +where + T: Scalar + Zero +{ + fn from(matrix: &'a CsrMatrix) -> Self { + convert_csr_csc(matrix) + } } \ No newline at end of file diff --git a/nalgebra-sparse/src/convert/serial.rs b/nalgebra-sparse/src/convert/serial.rs index e8121b30..f82020f4 100644 --- a/nalgebra-sparse/src/convert/serial.rs +++ b/nalgebra-sparse/src/convert/serial.rs @@ -1,10 +1,12 @@ //! TODO -use nalgebra::{DMatrix, Scalar, Matrix, Dim}; use crate::coo::CooMatrix; use crate::csr::CsrMatrix; -use num_traits::Zero; -use std::ops::{Add, AddAssign}; +use nalgebra::{DMatrix, Scalar, Matrix, Dim, ClosedAdd}; use nalgebra::storage::Storage; +use num_traits::Zero; + +use std::ops::{Add}; +use crate::csc::CscMatrix; /// TODO pub fn convert_dense_coo(dense: &Matrix) -> CooMatrix @@ -33,7 +35,7 @@ where /// TODO: What should the actual trait bounds be? pub fn convert_coo_dense(coo: &CooMatrix) -> DMatrix where - T: Scalar + Zero + Add + AddAssign, + T: Scalar + Zero + ClosedAdd, { let mut output = DMatrix::repeat(coo.nrows(), coo.ncols(), T::zero()); for (i, j, v) in coo.triplet_iter() { @@ -43,6 +45,430 @@ where } /// TODO -pub fn convert_coo_csr(_coo: &CooMatrix) -> CsrMatrix { - todo!() +pub fn convert_coo_csr(coo: &CooMatrix) -> CsrMatrix +where + T: Scalar + Zero +{ + let (offsets, indices, values) = convert_coo_cs(coo.nrows(), + coo.row_indices(), + coo.col_indices(), + coo.values()); + + // TODO: Avoid "try_from" since it validates the data? (requires unsafe, should benchmark + // to see if it can be justified for performance reasons) + CsrMatrix::try_from_csr_data(coo.nrows(), coo.ncols(), offsets, indices, values) + .expect("Internal error: Invalid CSR data during COO->CSR conversion") +} + +/// TODO +pub fn convert_csr_coo(csr: &CsrMatrix) -> CooMatrix +{ + let mut result = CooMatrix::new(csr.nrows(), csr.ncols()); + for (i, j, v) in csr.triplet_iter() { + result.push(i, j, v.inlined_clone()); + } + result +} + +/// TODO +pub fn convert_csr_dense(csr:& CsrMatrix) -> DMatrix +where + T: Scalar + ClosedAdd + Zero +{ + let mut output = DMatrix::zeros(csr.nrows(), csr.ncols()); + + for (i, j, v) in csr.triplet_iter() { + output[(i, j)] += v.inlined_clone(); + } + + output +} + +/// TODO +pub fn convert_dense_csr(dense: &Matrix) -> CsrMatrix +where + T: Scalar + Zero, + R: Dim, + C: Dim, + S: Storage +{ + let mut row_offsets = Vec::with_capacity(dense.nrows() + 1); + let mut col_idx = Vec::new(); + let mut values = Vec::new(); + + // We have to iterate row-by-row to build the CSR matrix, which is at odds with + // nalgebra's column-major storage. The alternative would be to perform an initial sweep + // to count number of non-zeros per row. + row_offsets.push(0); + for i in 0 .. dense.nrows() { + for j in 0 .. dense.ncols() { + let v = dense.index((i, j)); + if v != &T::zero() { + col_idx.push(j); + values.push(v.inlined_clone()); + } + } + row_offsets.push(col_idx.len()); + } + + // TODO: Consider circumventing the data validity check here + // (would require unsafe, should benchmark) + CsrMatrix::try_from_csr_data(dense.nrows(), dense.ncols(), row_offsets, col_idx, values) + .expect("Internal error: Invalid CsrMatrix format during dense-> CSR conversion") +} + +/// TODO +pub fn convert_coo_csc(coo: &CooMatrix) -> CscMatrix +where + T: Scalar + Zero +{ + let (offsets, indices, values) = convert_coo_cs(coo.ncols(), + coo.col_indices(), + coo.row_indices(), + coo.values()); + + // TODO: Avoid "try_from" since it validates the data? (requires unsafe, should benchmark + // to see if it can be justified for performance reasons) + CscMatrix::try_from_csc_data(coo.nrows(), coo.ncols(), offsets, indices, values) + .expect("Internal error: Invalid CSC data during COO->CSC conversion") +} + +/// TODO +pub fn convert_csc_coo(csc: &CscMatrix) -> CooMatrix +where + T: Scalar +{ + let mut coo = CooMatrix::new(csc.nrows(), csc.ncols()); + for (i, j, v) in csc.triplet_iter() { + coo.push(i, j, v.inlined_clone()); + } + coo +} + +/// TODO +pub fn convert_csc_dense(csc: &CscMatrix) -> DMatrix +where + T: Scalar + ClosedAdd + Zero +{ + let mut output = DMatrix::zeros(csc.nrows(), csc.ncols()); + + for (i, j, v) in csc.triplet_iter() { + output[(i, j)] += v.inlined_clone(); + } + + output +} + +/// TODO +pub fn convert_dense_csc(dense: &Matrix) -> CscMatrix + where + T: Scalar + Zero, + R: Dim, + C: Dim, + S: Storage +{ + let mut col_offsets = Vec::with_capacity(dense.ncols() + 1); + let mut row_idx = Vec::new(); + let mut values = Vec::new(); + + col_offsets.push(0); + for j in 0 .. dense.ncols() { + for i in 0 .. dense.nrows() { + let v = dense.index((i, j)); + if v != &T::zero() { + row_idx.push(i); + values.push(v.inlined_clone()); + } + } + col_offsets.push(row_idx.len()); + } + + // TODO: Consider circumventing the data validity check here + // (would require unsafe, should benchmark) + CscMatrix::try_from_csc_data(dense.nrows(), dense.ncols(), col_offsets, row_idx, values) + .expect("Internal error: Invalid CscMatrix format during dense-> CSC conversion") +} + +/// TODO +pub fn convert_csr_csc(csr: &CsrMatrix) -> CscMatrix +where + T: Scalar + Zero +{ + let (offsets, indices, values) = transpose_cs(csr.nrows(), + csr.ncols(), + csr.row_offsets(), + csr.col_indices(), + csr.values()); + + // TODO: Avoid data validity check? + CscMatrix::try_from_csc_data(csr.nrows(), csr.ncols(), offsets, indices, values) + .expect("Internal error: Invalid CSC data during CSR->CSC conversion") +} + +/// TODO +pub fn convert_csc_csr(csc: &CscMatrix) -> CsrMatrix + where + T: Scalar + Zero +{ + let (offsets, indices, values) = transpose_cs(csc.ncols(), + csc.nrows(), + csc.col_offsets(), + csc.row_indices(), + csc.values()); + + // TODO: Avoid data validity check? + CsrMatrix::try_from_csr_data(csc.nrows(), csc.ncols(), offsets, indices, values) + .expect("Internal error: Invalid CSR data during CSC->CSR conversion") +} + +fn convert_coo_cs(major_dim: usize, + major_indices: &[usize], + minor_indices: &[usize], + values: &[T]) + -> (Vec, Vec, Vec) +where + T: Scalar + Zero +{ + assert_eq!(major_indices.len(), minor_indices.len()); + assert_eq!(minor_indices.len(), values.len()); + let nnz = major_indices.len(); + + let (unsorted_major_offsets, unsorted_minor_idx, unsorted_vals) = { + let mut offsets = vec![0usize; major_dim + 1]; + let mut minor_idx = vec![0usize; nnz]; + let mut vals = vec![T::zero(); nnz]; + coo_to_unsorted_cs( + &mut offsets, + &mut minor_idx, + &mut vals, + major_dim, + major_indices, + minor_indices, + values, + ); + (offsets, minor_idx, vals) + }; + + // TODO: If input is sorted and/or without duplicates, we can avoid additional allocations + // and work. Might want to take advantage of this. + + // At this point, assembly is essentially complete. However, we must ensure + // that minor indices are sorted within each lane and without duplicates. + let mut sorted_major_offsets = Vec::new(); + let mut sorted_minor_idx = Vec::new(); + let mut sorted_vals = Vec::new(); + + sorted_major_offsets.push(0); + + // We need some temporary storage when working with each lane. Since lanes often have a + // very small number of non-zero entries, we try to amortize allocations across + // lanes by reusing workspace vectors + let mut idx_workspace = Vec::new(); + let mut perm_workspace = Vec::new(); + let mut values_workspace = Vec::new(); + + for lane in 0..major_dim { + let begin = unsorted_major_offsets[lane]; + let end = unsorted_major_offsets[lane + 1]; + let count = end - begin; + let range = begin..end; + + // Ensure that workspaces can hold enough data + perm_workspace.resize(count, 0); + idx_workspace.resize(count, 0); + values_workspace.resize(count, T::zero()); + sort_lane( + &mut idx_workspace[..count], + &mut values_workspace[..count], + &unsorted_minor_idx[range.clone()], + &unsorted_vals[range.clone()], + &mut perm_workspace[..count], + ); + + let sorted_ja_current_len = sorted_minor_idx.len(); + + combine_duplicates( + |idx| sorted_minor_idx.push(idx), + |val| sorted_vals.push(val), + &idx_workspace[..count], + &values_workspace[..count], + &Add::add, + ); + + let new_col_count = sorted_minor_idx.len() - sorted_ja_current_len; + sorted_major_offsets.push(sorted_major_offsets.last().unwrap() + new_col_count); + } + + (sorted_major_offsets, sorted_minor_idx, sorted_vals) +} + +/// Converts matrix data given in triplet format to unsorted CSR/CSC, retaining any duplicated +/// indices. +/// +/// Here `major/minor` is `row/col` for CSR and `col/row` for CSC. +fn coo_to_unsorted_cs( + major_offsets: &mut [usize], + cs_minor_idx: &mut [usize], + cs_values: &mut [T], + major_dim: usize, + major_indices: &[usize], + minor_indices: &[usize], + coo_values: &[T], +) { + assert_eq!(major_offsets.len(), major_dim + 1); + assert_eq!(cs_minor_idx.len(), cs_values.len()); + assert_eq!(cs_values.len(), major_indices.len()); + assert_eq!(major_indices.len(), minor_indices.len()); + assert_eq!(minor_indices.len(), coo_values.len()); + + // Count the number of occurrences of each row + for major_idx in major_indices { + major_offsets[*major_idx] += 1; + } + + convert_counts_to_offsets(major_offsets); + + { + // TODO: Instead of allocating a whole new vector storing the current counts, + // I think it's possible to be a bit more clever by storing each count + // in the last of the column indices for each row + let mut current_counts = vec![0usize; major_dim + 1]; + let triplet_iter = major_indices.iter().zip(minor_indices).zip(coo_values); + for ((i, j), value) in triplet_iter { + let current_offset = major_offsets[*i] + current_counts[*i]; + cs_minor_idx[current_offset] = *j; + cs_values[current_offset] = value.clone(); + current_counts[*i] += 1; + } + } +} + +/// Sort the indices of the given lane. +/// +/// The indices and values in `minor_idx` and `values` are sorted according to the +/// minor indices and stored in `minor_idx_result` and `values_result` respectively. +/// +/// All input slices are expected to be of the same length. The contents of mutable slices +/// can be arbitrary, as they are anyway overwritten. +fn sort_lane( + minor_idx_result: &mut [usize], + values_result: &mut [T], + minor_idx: &[usize], + values: &[T], + workspace: &mut [usize], +) { + assert_eq!(minor_idx_result.len(), values_result.len()); + assert_eq!(values_result.len(), minor_idx.len()); + assert_eq!(minor_idx.len(), values.len()); + assert_eq!(values.len(), workspace.len()); + + let permutation = workspace; + // Set permutation to identity + for (i, p) in permutation.iter_mut().enumerate() { + *p = i; + } + + // Compute permutation needed to bring minor indices into sorted order + // Note: Using sort_unstable here avoids internal allocations, which is crucial since + // each lane might have a small number of elements + permutation.sort_unstable_by_key(|idx| minor_idx[*idx]); + + apply_permutation(minor_idx_result, minor_idx, permutation); + apply_permutation(values_result, values, permutation); +} + +/// Transposes the compressed format. +/// +/// This means that major and minor roles are switched. This is used for converting between CSR +/// and CSC formats. +fn transpose_cs(major_dim: usize, + minor_dim: usize, + source_major_offsets: &[usize], + source_minor_indices: &[usize], + values: &[T]) + -> (Vec, Vec, Vec) +where + T: Scalar + Zero +{ + assert_eq!(source_major_offsets.len(), major_dim + 1); + assert_eq!(source_minor_indices.len(), values.len()); + let nnz = values.len(); + + // Count the number of occurences of each minor index + let mut minor_counts = vec![0; minor_dim]; + for minor_idx in source_minor_indices { + minor_counts[*minor_idx] += 1; + } + convert_counts_to_offsets(&mut minor_counts); + let mut target_offsets = minor_counts; + target_offsets.push(nnz); + let mut target_indices = vec![usize::MAX; nnz]; + let mut target_values = vec![T::zero(); nnz]; + + // Keep track of how many entries we have placed in each target major lane + let mut current_target_major_counts = vec![0; minor_dim]; + + for source_major_idx in 0 .. major_dim { + let source_lane_begin = source_major_offsets[source_major_idx]; + let source_lane_end = source_major_offsets[source_major_idx + 1]; + let source_lane_indices = &source_minor_indices[source_lane_begin .. source_lane_end]; + let source_lane_values = &values[source_lane_begin .. source_lane_end]; + + for (&source_minor_idx, val) in source_lane_indices.iter().zip(source_lane_values) { + // Compute the offset in the target data for this particular source entry + let target_lane_count = &mut current_target_major_counts[source_minor_idx]; + let entry_offset = target_offsets[source_minor_idx] + *target_lane_count; + target_indices[entry_offset] = source_major_idx; + target_values[entry_offset] = val.inlined_clone(); + *target_lane_count += 1; + } + } + + (target_offsets, target_indices, target_values) +} + +fn convert_counts_to_offsets(counts: &mut [usize]) { + // Convert the counts to an offset + let mut offset = 0; + for i_offset in counts.iter_mut() { + let count = *i_offset; + *i_offset = offset; + offset += count; + } +} + +// TODO: Move this into `utils` or something? +fn apply_permutation(out_slice: &mut [T], in_slice: &[T], permutation: &[usize]) { + assert_eq!(out_slice.len(), in_slice.len()); + assert_eq!(out_slice.len(), permutation.len()); + for (out_element, old_pos) in out_slice.iter_mut().zip(permutation) { + *out_element = in_slice[*old_pos].clone(); + } +} + +/// Given *sorted* indices and corresponding scalar values, combines duplicates with the given +/// associative combiner and calls the provided produce methods with combined indices and values. +fn combine_duplicates( + mut produce_idx: impl FnMut(usize), + mut produce_value: impl FnMut(T), + idx_array: &[usize], + values: &[T], + combiner: impl Fn(T, T) -> T, +) { + assert_eq!(idx_array.len(), values.len()); + + let mut i = 0; + while i < idx_array.len() { + let idx = idx_array[i]; + let mut combined_value = values[i].clone(); + let mut j = i + 1; + while j < idx_array.len() && idx_array[j] == idx { + let j_val = values[j].clone(); + combined_value = combiner(combined_value, j_val); + j += 1; + } + produce_idx(idx); + produce_value(combined_value); + i = j; + } } \ No newline at end of file diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index d1376b55..688362f9 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -45,14 +45,17 @@ //! - [x] "Disassemble" the COO matrix into its underlying triplet arrays. //! - Format conversion: //! - [x] COO -> Dense -//! - [ ] CSR -> Dense -//! - [ ] CSC -> Dense -//! - [ ] COO -> CSR -//! - [ ] COO -> CSC -//! - [ ] CSR -> CSC -//! - [ ] CSC -> CSR -//! - [ ] CSR -> COO -//! - [ ] CSC -> COO +//! - [x] CSR -> Dense +//! - [x] CSC -> Dense +//! - [x] COO -> CSR +//! - [x] COO -> CSC +//! - [x] CSR -> CSC +//! - [x] CSC -> CSR +//! - [x] CSR -> COO +//! - [x] CSC -> COO +//! - [x] Dense -> COO +//! - [x] Dense -> CSR +//! - [x] Dense -> CSC //! - Arithmetic. In general arithmetic is only implemented between instances of the same format, //! or between dense and instances of a given format. For example, we do not implement //! CSR * CSC, only CSR * CSR and CSC * CSC. diff --git a/nalgebra-sparse/tests/unit_tests/convert_serial.rs b/nalgebra-sparse/tests/unit_tests/convert_serial.rs index 21a93364..5975966d 100644 --- a/nalgebra-sparse/tests/unit_tests/convert_serial.rs +++ b/nalgebra-sparse/tests/unit_tests/convert_serial.rs @@ -1,9 +1,16 @@ use nalgebra_sparse::coo::CooMatrix; -use nalgebra_sparse::convert::serial::{convert_coo_dense, convert_dense_coo}; -use nalgebra_sparse::proptest::coo_with_duplicates; +use nalgebra_sparse::convert::serial::{convert_coo_dense, convert_coo_csr, + convert_dense_coo, convert_csr_dense, + convert_csr_coo, convert_dense_csr, + convert_csc_coo, convert_coo_csc, + convert_csc_dense, convert_dense_csc, + convert_csr_csc, convert_csc_csr}; +use nalgebra_sparse::proptest::{coo_with_duplicates, coo_no_duplicates, csr, csc}; use nalgebra::proptest::matrix; use proptest::prelude::*; use nalgebra::DMatrix; +use nalgebra_sparse::csr::CsrMatrix; +use nalgebra_sparse::csc::CscMatrix; #[test] fn test_convert_dense_coo() { @@ -49,10 +56,244 @@ fn test_convert_dense_coo() { } } +#[test] +fn test_convert_coo_csr() { + // No duplicates + { + let coo = { + let mut coo = CooMatrix::new(3, 4); + coo.push(1, 3, 4); + coo.push(0, 1, 2); + coo.push(2, 0, 1); + coo.push(2, 3, 2); + coo.push(2, 2, 1); + coo + }; + + let expected_csr = CsrMatrix::try_from_csr_data( + 3, + 4, + vec![0, 1, 2, 5], + vec![1, 3, 0, 2, 3], + vec![2, 4, 1, 1, 2] + ).unwrap(); + + assert_eq!(convert_coo_csr(&coo), expected_csr); + } + + // Duplicates + { + let coo = { + let mut coo = CooMatrix::new(3, 4); + coo.push(1, 3, 4); + coo.push(2, 3, 2); + coo.push(0, 1, 2); + coo.push(2, 0, 1); + coo.push(2, 3, 2); + coo.push(0, 1, 3); + coo.push(2, 2, 1); + coo + }; + + let expected_csr = CsrMatrix::try_from_csr_data( + 3, + 4, + vec![0, 1, 2, 5], + vec![1, 3, 0, 2, 3], + vec![5, 4, 1, 1, 4] + ).unwrap(); + + assert_eq!(convert_coo_csr(&coo), expected_csr); + } +} + +#[test] +fn test_convert_csr_coo() { + let csr = CsrMatrix::try_from_csr_data( + 3, + 4, + vec![0, 1, 2, 5], + vec![1, 3, 0, 2, 3], + vec![5, 4, 1, 1, 4] + ).unwrap(); + + let expected_coo = CooMatrix::try_from_triplets( + 3, + 4, + vec![0, 1, 2, 2, 2], + vec![1, 3, 0, 2, 3], + vec![5, 4, 1, 1, 4] + ).unwrap(); + + assert_eq!(convert_csr_coo(&csr), expected_coo); +} + +#[test] +fn test_convert_coo_csc() { + // No duplicates + { + let coo = { + let mut coo = CooMatrix::new(3, 4); + coo.push(1, 3, 4); + coo.push(0, 1, 2); + coo.push(2, 0, 1); + coo.push(2, 3, 2); + coo.push(2, 2, 1); + coo + }; + + let expected_csc = CscMatrix::try_from_csc_data( + 3, + 4, + vec![0, 1, 2, 3, 5], + vec![2, 0, 2, 1, 2], + vec![1, 2, 1, 4, 2] + ).unwrap(); + + assert_eq!(convert_coo_csc(&coo), expected_csc); + } + + // Duplicates + { + let coo = { + let mut coo = CooMatrix::new(3, 4); + coo.push(1, 3, 4); + coo.push(2, 3, 2); + coo.push(0, 1, 2); + coo.push(2, 0, 1); + coo.push(2, 3, 2); + coo.push(0, 1, 3); + coo.push(2, 2, 1); + coo + }; + + let expected_csc = CscMatrix::try_from_csc_data( + 3, + 4, + vec![0, 1, 2, 3, 5], + vec![2, 0, 2, 1, 2], + vec![1, 5, 1, 4, 4] + ).unwrap(); + + assert_eq!(convert_coo_csc(&coo), expected_csc); + } +} + +#[test] +fn test_convert_csc_coo() { + let csc = CscMatrix::try_from_csc_data( + 3, + 4, + vec![0, 1, 2, 3, 5], + vec![2, 0, 2, 1, 2], + vec![1, 2, 1, 4, 2] + ).unwrap(); + + let expected_coo = CooMatrix::try_from_triplets( + 3, + 4, + vec![2, 0, 2, 1, 2], + vec![0, 1, 2, 3, 3], + vec![1, 2, 1, 4, 2] + ).unwrap(); + + assert_eq!(convert_csc_coo(&csc), expected_coo); +} + +#[test] +fn test_convert_csr_csc_bidirectional() { + let csr = CsrMatrix::try_from_csr_data( + 3, + 4, + vec![0, 3, 4, 6], + vec![1, 2, 3, 0, 1, 3], + vec![5, 3, 2, 2, 1, 4], + ).unwrap(); + + let csc = CscMatrix::try_from_csc_data( + 3, + 4, + vec![0, 1, 3, 4, 6], + vec![1, 0, 2, 0, 0, 2], + vec![2, 5, 1, 3, 2, 4], + ).unwrap(); + + assert_eq!(convert_csr_csc(&csr), csc); + assert_eq!(convert_csc_csr(&csc), csr); +} + +#[test] +fn test_convert_csr_dense_bidirectional() { + let csr = CsrMatrix::try_from_csr_data( + 3, + 4, + vec![0, 3, 4, 6], + vec![1, 2, 3, 0, 1, 3], + vec![5, 3, 2, 2, 1, 4], + ).unwrap(); + + #[rustfmt::skip] + let dense = DMatrix::from_row_slice(3, 4, &[ + 0, 5, 3, 2, + 2, 0, 0, 0, + 0, 1, 0, 4 + ]); + + assert_eq!(convert_csr_dense(&csr), dense); + assert_eq!(convert_dense_csr(&dense), csr); +} + +#[test] +fn test_convert_csc_dense_bidirectional() { + let csc = CscMatrix::try_from_csc_data( + 3, + 4, + vec![0, 1, 3, 4, 6], + vec![1, 0, 2, 0, 0, 2], + vec![2, 5, 1, 3, 2, 4], + ).unwrap(); + + #[rustfmt::skip] + let dense = DMatrix::from_row_slice(3, 4, &[ + 0, 5, 3, 2, + 2, 0, 0, 0, + 0, 1, 0, 4 + ]); + + assert_eq!(convert_csc_dense(&csc), dense); + assert_eq!(convert_dense_csc(&dense), csc); +} + fn coo_strategy() -> impl Strategy> { coo_with_duplicates(-5 ..= 5, 0..=6usize, 0..=6usize, 40, 2) } +fn coo_no_duplicates_strategy() -> impl Strategy> { + coo_no_duplicates(-5 ..= 5, 0..=6usize, 0..=6usize, 40) +} + +fn csr_strategy() -> impl Strategy> { + csr(-5 ..= 5, 0..=6usize, 0..=6usize, 40) +} + +fn csc_strategy() -> impl Strategy> { + csc(-5 ..= 5, 0..=6usize, 0..=6usize, 40) +} + +/// Avoid generating explicit zero values so that it is possible to reason about sparsity patterns +fn non_zero_csr_strategy() -> impl Strategy> { + csr(1 ..= 5, 0..=6usize, 0..=6usize, 40) +} + +/// Avoid generating explicit zero values so that it is possible to reason about sparsity patterns +fn non_zero_csc_strategy() -> impl Strategy> { + csc(1 ..= 5, 0..=6usize, 0..=6usize, 40) +} + +fn dense_strategy() -> impl Strategy> { + matrix(-5..=5, 0..=6, 0..=6) +} + proptest! { #[test] @@ -78,7 +319,125 @@ proptest! { } #[test] - fn from_dense_coo_roundtrip(dense in matrix(-5..=5, 0..=6, 0..=6)) { + fn coo_from_dense_roundtrip(dense in dense_strategy()) { prop_assert_eq!(&dense, &DMatrix::from(&CooMatrix::from(&dense))); } + + #[test] + fn convert_coo_csr_agrees_with_csr_dense(coo in coo_strategy()) { + let coo_dense = convert_coo_dense(&coo); + let csr = convert_coo_csr(&coo); + let csr_dense = convert_csr_dense(&csr); + prop_assert_eq!(csr_dense, coo_dense); + + // It might be that COO matrices have a higher nnz due to duplicates, + // so we can only check that the CSR matrix has no more than the original COO matrix + prop_assert!(csr.nnz() <= coo.nnz()); + } + + #[test] + fn convert_coo_csr_nnz(coo in coo_no_duplicates_strategy()) { + // Check that the NNZ are equal when converting from a CooMatrix without + // duplicates to a CSR matrix + let csr = convert_coo_csr(&coo); + prop_assert_eq!(csr.nnz(), coo.nnz()); + } + + #[test] + fn convert_csr_coo_roundtrip(csr in csr_strategy()) { + let coo = convert_csr_coo(&csr); + let csr2 = convert_coo_csr(&coo); + prop_assert_eq!(csr2, csr); + } + + #[test] + fn coo_from_csr_roundtrip(csr in csr_strategy()) { + prop_assert_eq!(&csr, &CsrMatrix::from(&CooMatrix::from(&csr))); + } + + #[test] + fn csr_from_dense_roundtrip(dense in dense_strategy()) { + prop_assert_eq!(&dense, &DMatrix::from(&CsrMatrix::from(&dense))); + } + + #[test] + fn convert_csr_dense_roundtrip(csr in non_zero_csr_strategy()) { + // Since we only generate CSR matrices with non-zero values, we know that the + // number of explicitly stored entries when converting CSR->Dense->CSR should be + // unchanged, so that we can verify that the result is the same as the input + let dense = convert_csr_dense(&csr); + let csr2 = convert_dense_csr(&dense); + prop_assert_eq!(csr2, csr); + } + + #[test] + fn convert_csc_coo_roundtrip(csc in csc_strategy()) { + let coo = convert_csc_coo(&csc); + let csc2 = convert_coo_csc(&coo); + prop_assert_eq!(csc2, csc); + } + + #[test] + fn coo_from_csc_roundtrip(csc in csc_strategy()) { + prop_assert_eq!(&csc, &CscMatrix::from(&CooMatrix::from(&csc))); + } + + #[test] + fn convert_csc_dense_roundtrip(csc in non_zero_csc_strategy()) { + // Since we only generate CSC matrices with non-zero values, we know that the + // number of explicitly stored entries when converting CSC->Dense->CSC should be + // unchanged, so that we can verify that the result is the same as the input + let dense = convert_csc_dense(&csc); + let csc2 = convert_dense_csc(&dense); + prop_assert_eq!(csc2, csc); + } + + #[test] + fn csc_from_dense_roundtrip(dense in dense_strategy()) { + prop_assert_eq!(&dense, &DMatrix::from(&CscMatrix::from(&dense))); + } + + #[test] + fn convert_coo_csc_agrees_with_csc_dense(coo in coo_strategy()) { + let coo_dense = convert_coo_dense(&coo); + let csc = convert_coo_csc(&coo); + let csc_dense = convert_csc_dense(&csc); + prop_assert_eq!(csc_dense, coo_dense); + + // It might be that COO matrices have a higher nnz due to duplicates, + // so we can only check that the CSR matrix has no more than the original COO matrix + prop_assert!(csc.nnz() <= coo.nnz()); + } + + #[test] + fn convert_coo_csc_nnz(coo in coo_no_duplicates_strategy()) { + // Check that the NNZ are equal when converting from a CooMatrix without + // duplicates to a CSR matrix + let csc = convert_coo_csc(&coo); + prop_assert_eq!(csc.nnz(), coo.nnz()); + } + + #[test] + fn convert_csc_csr_roundtrip(csc in csc_strategy()) { + let csr = convert_csc_csr(&csc); + let csc2 = convert_csr_csc(&csr); + prop_assert_eq!(csc2, csc); + } + + #[test] + fn convert_csr_csc_roundtrip(csr in csr_strategy()) { + let csc = convert_csr_csc(&csr); + let csr2 = convert_csc_csr(&csc); + prop_assert_eq!(csr2, csr); + } + + #[test] + fn csc_from_csr_roundtrip(csr in csr_strategy()) { + prop_assert_eq!(&csr, &CsrMatrix::from(&CscMatrix::from(&csr))); + } + + #[test] + fn csr_from_csc_roundtrip(csc in csc_strategy()) { + prop_assert_eq!(&csc, &CscMatrix::from(&CsrMatrix::from(&csc))); + } } \ No newline at end of file From 1ae03d9ebbef551f414db250f767e9affef3c5bd Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Wed, 2 Dec 2020 16:56:22 +0100 Subject: [PATCH 031/183] Implement spmm_csr_dense --- nalgebra-sparse/src/ops/mod.rs | 34 ++++ .../src/{ops.rs => ops/serial/coo.rs} | 2 + nalgebra-sparse/src/ops/serial/csr.rs | 68 ++++++++ nalgebra-sparse/src/ops/serial/mod.rs | 37 +++++ nalgebra-sparse/tests/unit_tests/ops.rs | 147 +++++++++++++++++- 5 files changed, 286 insertions(+), 2 deletions(-) create mode 100644 nalgebra-sparse/src/ops/mod.rs rename nalgebra-sparse/src/{ops.rs => ops/serial/coo.rs} (98%) create mode 100644 nalgebra-sparse/src/ops/serial/csr.rs create mode 100644 nalgebra-sparse/src/ops/serial/mod.rs diff --git a/nalgebra-sparse/src/ops/mod.rs b/nalgebra-sparse/src/ops/mod.rs new file mode 100644 index 00000000..eea08495 --- /dev/null +++ b/nalgebra-sparse/src/ops/mod.rs @@ -0,0 +1,34 @@ +//! TODO + +pub mod serial; + +/// TODO +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Transposition { + /// TODO + Transpose, + /// TODO + NoTranspose, +} + +impl Transposition { + /// TODO + pub fn is_transpose(&self) -> bool { + self == &Self::Transpose + } + + /// TODO + pub fn from_bool(transpose: bool) -> Self { + if transpose { Self::Transpose } else { Self::NoTranspose } + } +} + +/// TODO +pub fn transpose() -> Transposition { + Transposition::Transpose +} + +/// TODO +pub fn no_transpose() -> Transposition { + Transposition::NoTranspose +} \ No newline at end of file diff --git a/nalgebra-sparse/src/ops.rs b/nalgebra-sparse/src/ops/serial/coo.rs similarity index 98% rename from nalgebra-sparse/src/ops.rs rename to nalgebra-sparse/src/ops/serial/coo.rs index bc3b9399..322c6914 100644 --- a/nalgebra-sparse/src/ops.rs +++ b/nalgebra-sparse/src/ops/serial/coo.rs @@ -12,6 +12,8 @@ use num_traits::{One, Zero}; /// /// If `beta == 0`, the elements in `y` are never read. /// +/// TODO: Rethink this function +/// /// Panics /// ------ /// diff --git a/nalgebra-sparse/src/ops/serial/csr.rs b/nalgebra-sparse/src/ops/serial/csr.rs new file mode 100644 index 00000000..fa7ff30f --- /dev/null +++ b/nalgebra-sparse/src/ops/serial/csr.rs @@ -0,0 +1,68 @@ +use crate::csr::CsrMatrix; +use crate::ops::Transposition; +use nalgebra::{DVectorSlice, Scalar, DMatrixSlice, DVectorSliceMut, ClosedAdd, ClosedMul, DMatrixSliceMut}; +use num_traits::{Zero, One}; + +/// Sparse-dense matrix-matrix multiplication `C = beta * C + alpha * trans(A) * trans(B)`. +pub fn spmm_csr_dense<'a, T>(c: impl Into>, + beta: T, + alpha: T, + trans_a: Transposition, + a: &CsrMatrix, + trans_b: Transposition, + b: impl Into>) + where + T: Scalar + ClosedAdd + ClosedMul + Zero + One +{ + spmm_csr_dense_(c.into(), beta, alpha, trans_a, a, trans_b, b.into()) +} + +fn spmm_csr_dense_(mut c: DMatrixSliceMut, + beta: T, + alpha: T, + trans_a: Transposition, + a: &CsrMatrix, + trans_b: Transposition, + b: DMatrixSlice) +where + T: Scalar + ClosedAdd + ClosedMul + Zero + One +{ + assert_compatible_spmm_dims!(c, a, b, trans_a, trans_b); + + if trans_a.is_transpose() { + // In this case, we have to pre-multiply C by beta + c *= beta; + + for k in 0..a.nrows() { + let a_row_k = a.row(k); + for (&i, a_ki) in a_row_k.col_indices().iter().zip(a_row_k.values()) { + let gamma_ki = alpha.inlined_clone() * a_ki.inlined_clone(); + let mut c_row_i = c.row_mut(i); + if trans_b.is_transpose() { + let b_col_k = b.column(k); + for (c_ij, b_jk) in c_row_i.iter_mut().zip(b_col_k.iter()) { + *c_ij += gamma_ki.inlined_clone() * b_jk.inlined_clone(); + } + } else { + let b_row_k = b.row(k); + for (c_ij, b_kj) in c_row_i.iter_mut().zip(b_row_k.iter()) { + *c_ij += gamma_ki.inlined_clone() * b_kj.inlined_clone(); + } + } + } + } + } else { + for j in 0..c.ncols() { + let mut c_col_j = c.column_mut(j); + for (c_ij, a_row_i) in c_col_j.iter_mut().zip(a.row_iter()) { + let mut dot_ij = T::zero(); + for (&k, a_ik) in a_row_i.col_indices().iter().zip(a_row_i.values()) { + let b_contrib = + if trans_b.is_transpose() { b.index((j, k)) } else { b.index((k, j)) }; + dot_ij += a_ik.inlined_clone() * b_contrib.inlined_clone(); + } + *c_ij = beta.inlined_clone() * c_ij.inlined_clone() + alpha.inlined_clone() * dot_ij; + } + } + } +} \ No newline at end of file diff --git a/nalgebra-sparse/src/ops/serial/mod.rs b/nalgebra-sparse/src/ops/serial/mod.rs new file mode 100644 index 00000000..02e15210 --- /dev/null +++ b/nalgebra-sparse/src/ops/serial/mod.rs @@ -0,0 +1,37 @@ +//! TODO + +#[macro_use] +macro_rules! assert_compatible_spmm_dims { + ($c:expr, $a:expr, $b:expr, $trans_a:expr, $trans_b:expr) => { + use crate::ops::Transposition::{Transpose, NoTranspose}; + match ($trans_a, $trans_b) { + (NoTranspose, NoTranspose) => { + assert_eq!($c.nrows(), $a.nrows(), "C.nrows() != A.nrows()"); + assert_eq!($c.ncols(), $b.ncols(), "C.ncols() != B.ncols()"); + assert_eq!($a.ncols(), $b.nrows(), "A.ncols() != B.nrows()"); + }, + (Transpose, NoTranspose) => { + assert_eq!($c.nrows(), $a.ncols(), "C.nrows() != A.ncols()"); + assert_eq!($c.ncols(), $b.ncols(), "C.ncols() != B.ncols()"); + assert_eq!($a.nrows(), $b.nrows(), "A.nrows() != B.nrows()"); + }, + (NoTranspose, Transpose) => { + assert_eq!($c.nrows(), $a.nrows(), "C.nrows() != A.nrows()"); + assert_eq!($c.ncols(), $b.nrows(), "C.ncols() != B.nrows()"); + assert_eq!($a.ncols(), $b.ncols(), "A.ncols() != B.ncols()"); + }, + (Transpose, Transpose) => { + assert_eq!($c.nrows(), $a.ncols(), "C.nrows() != A.ncols()"); + assert_eq!($c.ncols(), $b.nrows(), "C.ncols() != B.nrows()"); + assert_eq!($a.nrows(), $b.ncols(), "A.nrows() != B.ncols()"); + } + } + + } +} + +mod coo; +mod csr; + +pub use coo::*; +pub use csr::*; \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index 01df60b6..19add876 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -1,6 +1,15 @@ use nalgebra_sparse::coo::CooMatrix; -use nalgebra_sparse::ops::spmv_coo; -use nalgebra::{DVector, DMatrix}; +use nalgebra_sparse::ops::serial::{spmv_coo, spmm_csr_dense}; +use nalgebra_sparse::ops::{no_transpose, Transposition}; +use nalgebra_sparse::csr::CsrMatrix; +use nalgebra_sparse::proptest::csr; + +use nalgebra::{DVector, DMatrix, Scalar, DMatrixSliceMut, DMatrixSlice}; +use nalgebra::proptest::matrix; + +use proptest::prelude::*; + +use std::panic::catch_unwind; #[test] fn spmv_coo_agrees_with_dense_gemv() { @@ -25,4 +34,138 @@ fn spmv_coo_agrees_with_dense_gemv() { assert_eq!(y, y_dense); } } +} + +#[derive(Debug)] +struct SpmmCsrDenseArgs { + c: DMatrix, + beta: T, + alpha: T, + trans_a: Transposition, + a: CsrMatrix, + trans_b: Transposition, + b: DMatrix, +} + +/// Returns matrices C, A and B with compatible dimensions such that it can be used +/// in an `spmm` operation `C = beta * C + alpha * trans(A) * trans(B)`. +fn spmm_csr_dense_args_strategy() -> impl Strategy> { + let max_nnz = 40; + let value_strategy = -5 ..= 5; + let c_rows = 0 ..= 6usize; + let c_cols = 0 ..= 6usize; + let common_dim = 0 ..= 6usize; + let trans_strategy = trans_strategy(); + let c_matrix_strategy = matrix(value_strategy.clone(), c_rows, c_cols); + + (c_matrix_strategy, common_dim, trans_strategy.clone(), trans_strategy.clone()) + .prop_flat_map(move |(c, common_dim, trans_a, trans_b)| { + let a_shape = + if trans_a.is_transpose() { (common_dim, c.nrows()) } + else { (c.nrows(), common_dim) }; + let b_shape = + if trans_b.is_transpose() { (c.ncols(), common_dim) } + else { (common_dim, c.ncols()) }; + let a = csr(value_strategy.clone(), Just(a_shape.0), Just(a_shape.1), max_nnz); + let b = matrix(value_strategy.clone(), b_shape.0, b_shape.1); + + // We use the same values for alpha, beta parameters as for matrix elements + let alpha = value_strategy.clone(); + let beta = value_strategy.clone(); + + (Just(c), beta, alpha, Just(trans_a), a, Just(trans_b), b) + }).prop_map(|(c, beta, alpha, trans_a, a, trans_b, b)| { + SpmmCsrDenseArgs { + c, + beta, + alpha, + trans_a, + a, + trans_b, + b, + } + }) +} + +fn csr_strategy() -> impl Strategy> { + csr(-5 ..= 5, 0 ..= 6usize, 0 ..= 6usize, 40) +} + +fn dense_strategy() -> impl Strategy> { + matrix(-5 ..= 5, 0 ..= 6, 0 ..= 6) +} + +fn trans_strategy() -> impl Strategy + Clone { + proptest::bool::ANY.prop_map(Transposition::from_bool) +} + +/// Helper function to help us call dense GEMM with our transposition parameters +fn dense_gemm<'a>(c: impl Into>, + beta: i32, + alpha: i32, + trans_a: Transposition, + a: impl Into>, + trans_b: Transposition, + b: impl Into>) +{ + let mut c = c.into(); + let a = a.into(); + let b = b.into(); + + use Transposition::{Transpose, NoTranspose}; + match (trans_a, trans_b) { + (NoTranspose, NoTranspose) => c.gemm(alpha, &a, &b, beta), + (Transpose, NoTranspose) => c.gemm(alpha, &a.transpose(), &b, beta), + (NoTranspose, Transpose) => c.gemm(alpha, &a, &b.transpose(), beta), + (Transpose, Transpose) => c.gemm(alpha, &a.transpose(), &b.transpose(), beta) + }; +} + +proptest! { + + #[test] + fn spmm_csr_dense_agrees_with_dense_result( + SpmmCsrDenseArgs { c, beta, alpha, trans_a, a, trans_b, b } + in spmm_csr_dense_args_strategy() + ) { + let mut spmm_result = c.clone(); + spmm_csr_dense(&mut spmm_result, beta, alpha, trans_a, &a, trans_b, &b); + + let mut gemm_result = c.clone(); + dense_gemm(&mut gemm_result, beta, alpha, trans_a, &DMatrix::from(&a), trans_b, &b); + + prop_assert_eq!(spmm_result, gemm_result); + } + + #[test] + fn spmm_csr_dense_panics_on_dim_mismatch( + (alpha, beta, c, a, b, trans_a, trans_b) + in (-5 ..= 5, -5 ..= 5, dense_strategy(), csr_strategy(), + dense_strategy(), trans_strategy(), trans_strategy()) + ) { + // We refer to `A * B` as the "product" + let product_rows = if trans_a.is_transpose() { a.ncols() } else { a.nrows() }; + let product_cols = if trans_b.is_transpose() { b.nrows() } else { b.ncols() }; + // Determine the common dimension in the product + // from the perspective of a and b, respectively + let product_a_common = if trans_a.is_transpose() { a.nrows() } else { a.ncols() }; + let product_b_common = if trans_b.is_transpose() { b.ncols() } else { b.nrows() }; + + let dims_are_compatible = product_rows == c.nrows() + && product_cols == c.ncols() + && product_a_common == product_b_common; + + // If the dimensions randomly happen to be compatible, then of course we need to + // skip the test, so we assume that they are not. + prop_assume!(!dims_are_compatible); + + let result = catch_unwind(|| { + let mut spmm_result = c.clone(); + spmm_csr_dense(&mut spmm_result, beta, alpha, trans_a, &a, trans_b, &b); + }); + + prop_assert!(result.is_err(), + "The SPMM kernel executed successfully despite mismatch dimensions"); + } + } \ No newline at end of file From 7c6895061476a21d3a3086e7e044cb1ffaf9f09c Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Wed, 2 Dec 2020 17:04:19 +0100 Subject: [PATCH 032/183] Simplify transposition API in spmm_csr_dense --- nalgebra-sparse/src/ops/mod.rs | 28 +++----------------- nalgebra-sparse/src/ops/serial/csr.rs | 18 ++++++------- nalgebra-sparse/src/ops/serial/mod.rs | 10 +++---- nalgebra-sparse/tests/unit_tests/ops.rs | 35 ++++++++++++------------- 4 files changed, 35 insertions(+), 56 deletions(-) diff --git a/nalgebra-sparse/src/ops/mod.rs b/nalgebra-sparse/src/ops/mod.rs index eea08495..bf1698ec 100644 --- a/nalgebra-sparse/src/ops/mod.rs +++ b/nalgebra-sparse/src/ops/mod.rs @@ -4,31 +4,11 @@ pub mod serial; /// TODO #[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum Transposition { - /// TODO - Transpose, - /// TODO - NoTranspose, -} +pub struct Transpose(pub bool); -impl Transposition { +impl Transpose { /// TODO - pub fn is_transpose(&self) -> bool { - self == &Self::Transpose + pub fn to_bool(&self) -> bool { + self.0 } - - /// TODO - pub fn from_bool(transpose: bool) -> Self { - if transpose { Self::Transpose } else { Self::NoTranspose } - } -} - -/// TODO -pub fn transpose() -> Transposition { - Transposition::Transpose -} - -/// TODO -pub fn no_transpose() -> Transposition { - Transposition::NoTranspose } \ No newline at end of file diff --git a/nalgebra-sparse/src/ops/serial/csr.rs b/nalgebra-sparse/src/ops/serial/csr.rs index fa7ff30f..3b28ac74 100644 --- a/nalgebra-sparse/src/ops/serial/csr.rs +++ b/nalgebra-sparse/src/ops/serial/csr.rs @@ -1,15 +1,15 @@ use crate::csr::CsrMatrix; -use crate::ops::Transposition; -use nalgebra::{DVectorSlice, Scalar, DMatrixSlice, DVectorSliceMut, ClosedAdd, ClosedMul, DMatrixSliceMut}; +use crate::ops::{Transpose}; +use nalgebra::{Scalar, DMatrixSlice, ClosedAdd, ClosedMul, DMatrixSliceMut}; use num_traits::{Zero, One}; /// Sparse-dense matrix-matrix multiplication `C = beta * C + alpha * trans(A) * trans(B)`. pub fn spmm_csr_dense<'a, T>(c: impl Into>, beta: T, alpha: T, - trans_a: Transposition, + trans_a: Transpose, a: &CsrMatrix, - trans_b: Transposition, + trans_b: Transpose, b: impl Into>) where T: Scalar + ClosedAdd + ClosedMul + Zero + One @@ -20,16 +20,16 @@ pub fn spmm_csr_dense<'a, T>(c: impl Into>, fn spmm_csr_dense_(mut c: DMatrixSliceMut, beta: T, alpha: T, - trans_a: Transposition, + trans_a: Transpose, a: &CsrMatrix, - trans_b: Transposition, + trans_b: Transpose, b: DMatrixSlice) where T: Scalar + ClosedAdd + ClosedMul + Zero + One { assert_compatible_spmm_dims!(c, a, b, trans_a, trans_b); - if trans_a.is_transpose() { + if trans_a.to_bool() { // In this case, we have to pre-multiply C by beta c *= beta; @@ -38,7 +38,7 @@ where for (&i, a_ki) in a_row_k.col_indices().iter().zip(a_row_k.values()) { let gamma_ki = alpha.inlined_clone() * a_ki.inlined_clone(); let mut c_row_i = c.row_mut(i); - if trans_b.is_transpose() { + if trans_b.to_bool() { let b_col_k = b.column(k); for (c_ij, b_jk) in c_row_i.iter_mut().zip(b_col_k.iter()) { *c_ij += gamma_ki.inlined_clone() * b_jk.inlined_clone(); @@ -58,7 +58,7 @@ where let mut dot_ij = T::zero(); for (&k, a_ik) in a_row_i.col_indices().iter().zip(a_row_i.values()) { let b_contrib = - if trans_b.is_transpose() { b.index((j, k)) } else { b.index((k, j)) }; + if trans_b.to_bool() { b.index((j, k)) } else { b.index((k, j)) }; dot_ij += a_ik.inlined_clone() * b_contrib.inlined_clone(); } *c_ij = beta.inlined_clone() * c_ij.inlined_clone() + alpha.inlined_clone() * dot_ij; diff --git a/nalgebra-sparse/src/ops/serial/mod.rs b/nalgebra-sparse/src/ops/serial/mod.rs index 02e15210..a7615ec4 100644 --- a/nalgebra-sparse/src/ops/serial/mod.rs +++ b/nalgebra-sparse/src/ops/serial/mod.rs @@ -3,24 +3,24 @@ #[macro_use] macro_rules! assert_compatible_spmm_dims { ($c:expr, $a:expr, $b:expr, $trans_a:expr, $trans_b:expr) => { - use crate::ops::Transposition::{Transpose, NoTranspose}; + use crate::ops::Transpose; match ($trans_a, $trans_b) { - (NoTranspose, NoTranspose) => { + (Transpose(false), Transpose(false)) => { assert_eq!($c.nrows(), $a.nrows(), "C.nrows() != A.nrows()"); assert_eq!($c.ncols(), $b.ncols(), "C.ncols() != B.ncols()"); assert_eq!($a.ncols(), $b.nrows(), "A.ncols() != B.nrows()"); }, - (Transpose, NoTranspose) => { + (Transpose(true), Transpose(false)) => { assert_eq!($c.nrows(), $a.ncols(), "C.nrows() != A.ncols()"); assert_eq!($c.ncols(), $b.ncols(), "C.ncols() != B.ncols()"); assert_eq!($a.nrows(), $b.nrows(), "A.nrows() != B.nrows()"); }, - (NoTranspose, Transpose) => { + (Transpose(false), Transpose(true)) => { assert_eq!($c.nrows(), $a.nrows(), "C.nrows() != A.nrows()"); assert_eq!($c.ncols(), $b.nrows(), "C.ncols() != B.nrows()"); assert_eq!($a.ncols(), $b.ncols(), "A.ncols() != B.ncols()"); }, - (Transpose, Transpose) => { + (Transpose(true), Transpose(true)) => { assert_eq!($c.nrows(), $a.ncols(), "C.nrows() != A.ncols()"); assert_eq!($c.ncols(), $b.nrows(), "C.ncols() != B.nrows()"); assert_eq!($a.nrows(), $b.ncols(), "A.nrows() != B.ncols()"); diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index 19add876..add03a98 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -1,6 +1,6 @@ use nalgebra_sparse::coo::CooMatrix; use nalgebra_sparse::ops::serial::{spmv_coo, spmm_csr_dense}; -use nalgebra_sparse::ops::{no_transpose, Transposition}; +use nalgebra_sparse::ops::{Transpose}; use nalgebra_sparse::csr::CsrMatrix; use nalgebra_sparse::proptest::csr; @@ -41,9 +41,9 @@ struct SpmmCsrDenseArgs { c: DMatrix, beta: T, alpha: T, - trans_a: Transposition, + trans_a: Transpose, a: CsrMatrix, - trans_b: Transposition, + trans_b: Transpose, b: DMatrix, } @@ -61,10 +61,10 @@ fn spmm_csr_dense_args_strategy() -> impl Strategy> (c_matrix_strategy, common_dim, trans_strategy.clone(), trans_strategy.clone()) .prop_flat_map(move |(c, common_dim, trans_a, trans_b)| { let a_shape = - if trans_a.is_transpose() { (common_dim, c.nrows()) } + if trans_a.to_bool() { (common_dim, c.nrows()) } else { (c.nrows(), common_dim) }; let b_shape = - if trans_b.is_transpose() { (c.ncols(), common_dim) } + if trans_b.to_bool() { (c.ncols(), common_dim) } else { (common_dim, c.ncols()) }; let a = csr(value_strategy.clone(), Just(a_shape.0), Just(a_shape.1), max_nnz); let b = matrix(value_strategy.clone(), b_shape.0, b_shape.1); @@ -95,29 +95,28 @@ fn dense_strategy() -> impl Strategy> { matrix(-5 ..= 5, 0 ..= 6, 0 ..= 6) } -fn trans_strategy() -> impl Strategy + Clone { - proptest::bool::ANY.prop_map(Transposition::from_bool) +fn trans_strategy() -> impl Strategy + Clone { + proptest::bool::ANY.prop_map(Transpose) } /// Helper function to help us call dense GEMM with our transposition parameters fn dense_gemm<'a>(c: impl Into>, beta: i32, alpha: i32, - trans_a: Transposition, + trans_a: Transpose, a: impl Into>, - trans_b: Transposition, + trans_b: Transpose, b: impl Into>) { let mut c = c.into(); let a = a.into(); let b = b.into(); - use Transposition::{Transpose, NoTranspose}; match (trans_a, trans_b) { - (NoTranspose, NoTranspose) => c.gemm(alpha, &a, &b, beta), - (Transpose, NoTranspose) => c.gemm(alpha, &a.transpose(), &b, beta), - (NoTranspose, Transpose) => c.gemm(alpha, &a, &b.transpose(), beta), - (Transpose, Transpose) => c.gemm(alpha, &a.transpose(), &b.transpose(), beta) + (Transpose(false), Transpose(false)) => c.gemm(alpha, &a, &b, beta), + (Transpose(true), Transpose(false)) => c.gemm(alpha, &a.transpose(), &b, beta), + (Transpose(false), Transpose(true)) => c.gemm(alpha, &a, &b.transpose(), beta), + (Transpose(true), Transpose(true)) => c.gemm(alpha, &a.transpose(), &b.transpose(), beta) }; } @@ -144,12 +143,12 @@ proptest! { dense_strategy(), trans_strategy(), trans_strategy()) ) { // We refer to `A * B` as the "product" - let product_rows = if trans_a.is_transpose() { a.ncols() } else { a.nrows() }; - let product_cols = if trans_b.is_transpose() { b.nrows() } else { b.ncols() }; + let product_rows = if trans_a.to_bool() { a.ncols() } else { a.nrows() }; + let product_cols = if trans_b.to_bool() { b.nrows() } else { b.ncols() }; // Determine the common dimension in the product // from the perspective of a and b, respectively - let product_a_common = if trans_a.is_transpose() { a.nrows() } else { a.ncols() }; - let product_b_common = if trans_b.is_transpose() { b.ncols() } else { b.nrows() }; + let product_a_common = if trans_a.to_bool() { a.nrows() } else { a.ncols() }; + let product_b_common = if trans_b.to_bool() { b.ncols() } else { b.nrows() }; let dims_are_compatible = product_rows == c.nrows() && product_cols == c.ncols() From 4420237edefd65d00d8a05fca723de8f4c64f026 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Fri, 4 Dec 2020 14:13:07 +0100 Subject: [PATCH 033/183] Implement spadd_build_pattern --- nalgebra-sparse/src/ops/impl_std_ops.rs | 0 nalgebra-sparse/src/ops/serial/csr.rs | 2 +- nalgebra-sparse/src/ops/serial/mod.rs | 4 +- nalgebra-sparse/src/ops/serial/pattern.rs | 77 +++++++++++++++++++++++ nalgebra-sparse/tests/unit_tests/ops.rs | 41 +++++++++++- 5 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 nalgebra-sparse/src/ops/impl_std_ops.rs create mode 100644 nalgebra-sparse/src/ops/serial/pattern.rs diff --git a/nalgebra-sparse/src/ops/impl_std_ops.rs b/nalgebra-sparse/src/ops/impl_std_ops.rs new file mode 100644 index 00000000..e69de29b diff --git a/nalgebra-sparse/src/ops/serial/csr.rs b/nalgebra-sparse/src/ops/serial/csr.rs index 3b28ac74..b77d1112 100644 --- a/nalgebra-sparse/src/ops/serial/csr.rs +++ b/nalgebra-sparse/src/ops/serial/csr.rs @@ -3,7 +3,7 @@ use crate::ops::{Transpose}; use nalgebra::{Scalar, DMatrixSlice, ClosedAdd, ClosedMul, DMatrixSliceMut}; use num_traits::{Zero, One}; -/// Sparse-dense matrix-matrix multiplication `C = beta * C + alpha * trans(A) * trans(B)`. +/// Sparse-dense matrix-matrix multiplication `C <- beta * C + alpha * trans(A) * trans(B)`. pub fn spmm_csr_dense<'a, T>(c: impl Into>, beta: T, alpha: T, diff --git a/nalgebra-sparse/src/ops/serial/mod.rs b/nalgebra-sparse/src/ops/serial/mod.rs index a7615ec4..bb40419f 100644 --- a/nalgebra-sparse/src/ops/serial/mod.rs +++ b/nalgebra-sparse/src/ops/serial/mod.rs @@ -32,6 +32,8 @@ macro_rules! assert_compatible_spmm_dims { mod coo; mod csr; +mod pattern; pub use coo::*; -pub use csr::*; \ No newline at end of file +pub use csr::*; +pub use pattern::*; \ No newline at end of file diff --git a/nalgebra-sparse/src/ops/serial/pattern.rs b/nalgebra-sparse/src/ops/serial/pattern.rs new file mode 100644 index 00000000..6507ad1b --- /dev/null +++ b/nalgebra-sparse/src/ops/serial/pattern.rs @@ -0,0 +1,77 @@ +use crate::pattern::SparsityPattern; + +use std::mem::swap; +use std::iter; + +/// Sparse matrix addition pattern construction, `C <- A + B`. +/// +/// Builds the pattern for `C`, which is able to hold the result of the sum `A + B`. +/// The patterns are assumed to have the same major and minor dimensions. In other words, +/// both patterns `A` and `B` must both stem from the same kind of compressed matrix: +/// CSR or CSC. +/// TODO: Explain that output pattern is only used to avoid allocations +pub fn spadd_build_pattern(pattern: &mut SparsityPattern, + a: &SparsityPattern, + b: &SparsityPattern) +{ + // TODO: Proper error messages + assert_eq!(a.major_dim(), b.major_dim()); + assert_eq!(a.minor_dim(), b.minor_dim()); + + let input_pattern = pattern; + let mut temp_pattern = SparsityPattern::new(a.major_dim(), b.minor_dim()); + swap(input_pattern, &mut temp_pattern); + let (mut offsets, mut indices) = temp_pattern.disassemble(); + + offsets.clear(); + offsets.reserve(a.major_dim() + 1); + indices.clear(); + + offsets.push(0); + + for lane_idx in 0 .. a.major_dim() { + let lane_a = a.lane(lane_idx); + let lane_b = b.lane(lane_idx); + indices.extend(iterate_intersection(lane_a, lane_b)); + offsets.push(indices.len()); + } + + // TODO: Consider circumventing format checks? (requires unsafe, should benchmark first) + let mut new_pattern = SparsityPattern::try_from_offsets_and_indices( + a.major_dim(), a.minor_dim(), offsets, indices) + .expect("Pattern must be valid by definition"); + swap(input_pattern, &mut new_pattern); +} + +/// Iterate over the intersection of the two sets represented by sorted slices +/// (with unique elements) +fn iterate_intersection<'a>(mut sorted_a: &'a [usize], + mut sorted_b: &'a [usize]) -> impl Iterator + 'a { + // TODO: Can use a kind of simultaneous exponential search to speed things up here + iter::from_fn(move || { + if let (Some(a_item), Some(b_item)) = (sorted_a.first(), sorted_b.first()) { + let item = if a_item < b_item { + sorted_a = &sorted_a[1 ..]; + a_item + } else if b_item < a_item { + sorted_b = &sorted_b[1 ..]; + b_item + } else { + // Both lists contain the same element, advance both slices to avoid + // duplicate entries in the result + sorted_a = &sorted_a[1 ..]; + sorted_b = &sorted_b[1 ..]; + a_item + }; + Some(*item) + } else if let Some(a_item) = sorted_a.first() { + sorted_a = &sorted_a[1..]; + Some(*a_item) + } else if let Some(b_item) = sorted_b.first() { + sorted_b = &sorted_b[1..]; + Some(*b_item) + } else { + None + } + }) +} \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index add03a98..4fb9c232 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -1,8 +1,9 @@ use nalgebra_sparse::coo::CooMatrix; -use nalgebra_sparse::ops::serial::{spmv_coo, spmm_csr_dense}; +use nalgebra_sparse::ops::serial::{spmv_coo, spmm_csr_dense, spadd_build_pattern}; use nalgebra_sparse::ops::{Transpose}; use nalgebra_sparse::csr::CsrMatrix; -use nalgebra_sparse::proptest::csr; +use nalgebra_sparse::proptest::{csr, sparsity_pattern}; +use nalgebra_sparse::pattern::SparsityPattern; use nalgebra::{DVector, DMatrix, Scalar, DMatrixSliceMut, DMatrixSlice}; use nalgebra::proptest::matrix; @@ -10,6 +11,7 @@ use nalgebra::proptest::matrix; use proptest::prelude::*; use std::panic::catch_unwind; +use std::sync::Arc; #[test] fn spmv_coo_agrees_with_dense_gemv() { @@ -99,6 +101,19 @@ fn trans_strategy() -> impl Strategy + Clone { proptest::bool::ANY.prop_map(Transpose) } +fn pattern_strategy() -> impl Strategy { + sparsity_pattern(0 ..= 6usize, 0..= 6usize, 40) +} + +/// Constructs pairs (a, b) where a and b have the same dimensions +fn spadd_build_pattern_strategy() -> impl Strategy { + pattern_strategy() + .prop_flat_map(|a| { + let b = sparsity_pattern(Just(a.major_dim()), Just(a.minor_dim()), 40); + (Just(a), b) + }) +} + /// Helper function to help us call dense GEMM with our transposition parameters fn dense_gemm<'a>(c: impl Into>, beta: i32, @@ -167,4 +182,26 @@ proptest! { "The SPMM kernel executed successfully despite mismatch dimensions"); } + #[test] + fn spadd_build_pattern_test((c, (a, b)) in (pattern_strategy(), spadd_build_pattern_strategy())) + { + // (a, b) are dimensionally compatible patterns, whereas c is an *arbitrary* pattern + let mut pattern_result = c.clone(); + spadd_build_pattern(&mut pattern_result, &a, &b); + + // To verify the pattern, we construct CSR matrices with positive integer entries + // corresponding to a and b, and convert them to dense matrices. + // The sum of these dense matrices will then have non-zeros in exactly the same locations + // as the result of "adding" the sparsity patterns + let a_csr = CsrMatrix::try_from_pattern_and_values(Arc::new(a.clone()), vec![1; a.nnz()]) + .unwrap(); + let a_dense = DMatrix::from(&a_csr); + let b_csr = CsrMatrix::try_from_pattern_and_values(Arc::new(b.clone()), vec![1; b.nnz()]) + .unwrap(); + let b_dense = DMatrix::from(&b_csr); + let c_dense = a_dense + b_dense; + let c_csr = CsrMatrix::from(&c_dense); + + prop_assert_eq!(&pattern_result, &*c_csr.pattern()); + } } \ No newline at end of file From 8b7b836a37504e1769b4f26c6daf38239da1e430 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Wed, 9 Dec 2020 14:16:27 +0100 Subject: [PATCH 034/183] Make CsrMatrix/CscMatrix::pattern() return reference --- nalgebra-sparse/src/csc.rs | 8 ++++---- nalgebra-sparse/src/csr.rs | 8 ++++---- nalgebra-sparse/tests/unit_tests/ops.rs | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/nalgebra-sparse/src/csc.rs b/nalgebra-sparse/src/csc.rs index 1a3f0639..941fb4c9 100644 --- a/nalgebra-sparse/src/csc.rs +++ b/nalgebra-sparse/src/csc.rs @@ -275,8 +275,8 @@ impl CscMatrix { // Take an Arc to the pattern, which might be the sole reference to the data after // taking the values. This is important, because it might let us avoid cloning the data // further below. - let pattern = self.pattern(); - let values = self.take_values(); + let pattern = self.sparsity_pattern; + let values = self.values; // Try to take the pattern out of the `Arc` if possible, // otherwise clone the pattern. @@ -292,8 +292,8 @@ impl CscMatrix { /// The sparsity pattern is stored internally inside an `Arc`. This allows users to re-use /// the same sparsity pattern for multiple matrices without storing the same pattern multiple /// times in memory. - pub fn pattern(&self) -> Arc { - Arc::clone(&self.sparsity_pattern) + pub fn pattern(&self) -> &Arc { + &self.sparsity_pattern } } diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index 3adc25bd..01d7533e 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -275,8 +275,8 @@ impl CsrMatrix { // Take an Arc to the pattern, which might be the sole reference to the data after // taking the values. This is important, because it might let us avoid cloning the data // further below. - let pattern = self.pattern(); - let values = self.take_values(); + let pattern = self.sparsity_pattern; + let values = self.values; // Try to take the pattern out of the `Arc` if possible, // otherwise clone the pattern. @@ -292,8 +292,8 @@ impl CsrMatrix { /// The sparsity pattern is stored internally inside an `Arc`. This allows users to re-use /// the same sparsity pattern for multiple matrices without storing the same pattern multiple /// times in memory. - pub fn pattern(&self) -> Arc { - Arc::clone(&self.sparsity_pattern) + pub fn pattern(&self) -> &Arc { + &self.sparsity_pattern } } diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index 4fb9c232..14eb4aec 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -202,6 +202,6 @@ proptest! { let c_dense = a_dense + b_dense; let c_csr = CsrMatrix::from(&c_dense); - prop_assert_eq!(&pattern_result, &*c_csr.pattern()); + prop_assert_eq!(&pattern_result, c_csr.pattern().as_ref()); } } \ No newline at end of file From 830df6d07b81ac24e52fb1c83f24ed80213871ab Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Wed, 9 Dec 2020 14:42:31 +0100 Subject: [PATCH 035/183] Implement Csr/CscMatrix::transpose() --- nalgebra-sparse/src/csc.rs | 21 +++++++++++++++++ nalgebra-sparse/src/csr.rs | 23 ++++++++++++++++++- nalgebra-sparse/tests/common/mod.rs | 15 +++++++++++- .../tests/unit_tests/convert_serial.rs | 5 +--- nalgebra-sparse/tests/unit_tests/csc.rs | 20 ++++++++++++++++ nalgebra-sparse/tests/unit_tests/csr.rs | 18 +++++++++++++++ nalgebra-sparse/tests/unit_tests/ops.rs | 5 ++-- 7 files changed, 98 insertions(+), 9 deletions(-) diff --git a/nalgebra-sparse/src/csc.rs b/nalgebra-sparse/src/csc.rs index 941fb4c9..a94a5fdc 100644 --- a/nalgebra-sparse/src/csc.rs +++ b/nalgebra-sparse/src/csc.rs @@ -8,6 +8,8 @@ use std::slice::{IterMut, Iter}; use std::ops::Range; use num_traits::Zero; use std::ptr::slice_from_raw_parts_mut; +use crate::csr::CsrMatrix; +use nalgebra::Scalar; /// A CSC representation of a sparse matrix. /// @@ -295,6 +297,15 @@ impl CscMatrix { pub fn pattern(&self) -> &Arc { &self.sparsity_pattern } + + /// Reinterprets the CSC matrix as its transpose represented by a CSR matrix. + /// + /// This operation does not touch the CSC data, and is effectively a no-op. + pub fn transpose_as_csr(self) -> CsrMatrix { + let pattern = self.sparsity_pattern; + let values = self.values; + CsrMatrix::try_from_pattern_and_values(pattern, values).unwrap() + } } impl CscMatrix { @@ -323,6 +334,16 @@ impl CscMatrix { } } +impl CscMatrix + where + T: Scalar + Zero +{ + /// Compute the transpose of the matrix. + pub fn transpose(&self) -> CscMatrix { + CsrMatrix::from(self).transpose_as_csc() + } +} + /// Convert pattern format errors into more meaningful CSC-specific errors. /// /// This ensures that the terminology is consistent: we are talking about rows and columns, diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index 01d7533e..33348bd5 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -2,11 +2,14 @@ use crate::{SparseFormatError, SparseFormatErrorKind}; use crate::pattern::{SparsityPattern, SparsityPatternFormatError, SparsityPatternIter}; +use crate::csc::CscMatrix; + +use nalgebra::Scalar; +use num_traits::Zero; use std::sync::Arc; use std::slice::{IterMut, Iter}; use std::ops::Range; -use num_traits::Zero; use std::ptr::slice_from_raw_parts_mut; /// A CSR representation of a sparse matrix. @@ -321,6 +324,24 @@ impl CsrMatrix { pub fn index(&self, row_index: usize, col_index: usize) -> T { self.get(row_index, col_index).unwrap() } + + /// Reinterprets the CSR matrix as its transpose represented by a CSC matrix. + /// This operation does not touch the CSR data, and is effectively a no-op. + pub fn transpose_as_csc(self) -> CscMatrix { + let pattern = self.sparsity_pattern; + let values = self.values; + CscMatrix::try_from_pattern_and_values(pattern, values).unwrap() + } +} + +impl CsrMatrix +where + T: Scalar + Zero +{ + /// Compute the transpose of the matrix. + pub fn transpose(&self) -> CsrMatrix { + CscMatrix::from(self).transpose_as_csr() + } } /// Convert pattern format errors into more meaningful CSR-specific errors. diff --git a/nalgebra-sparse/tests/common/mod.rs b/nalgebra-sparse/tests/common/mod.rs index bb77a10a..6e730b7d 100644 --- a/nalgebra-sparse/tests/common/mod.rs +++ b/nalgebra-sparse/tests/common/mod.rs @@ -1,3 +1,8 @@ +use proptest::strategy::Strategy; +use nalgebra_sparse::csr::CsrMatrix; +use nalgebra_sparse::proptest::{csr, csc}; +use nalgebra_sparse::csc::CscMatrix; + #[macro_export] macro_rules! assert_panics { ($e:expr) => {{ @@ -17,4 +22,12 @@ macro_rules! assert_panics { panic!("assert_panics!({}) failed: the expression did not panic.", expr_string); } }}; -} \ No newline at end of file +} + +pub fn csr_strategy() -> impl Strategy> { + csr(-5 ..= 5, 0 ..= 6usize, 0 ..= 6usize, 40) +} + +pub fn csc_strategy() -> impl Strategy> { + csc(-5 ..= 5, 0..=6usize, 0..=6usize, 40) +} diff --git a/nalgebra-sparse/tests/unit_tests/convert_serial.rs b/nalgebra-sparse/tests/unit_tests/convert_serial.rs index 5975966d..9dc13c71 100644 --- a/nalgebra-sparse/tests/unit_tests/convert_serial.rs +++ b/nalgebra-sparse/tests/unit_tests/convert_serial.rs @@ -11,6 +11,7 @@ use proptest::prelude::*; use nalgebra::DMatrix; use nalgebra_sparse::csr::CsrMatrix; use nalgebra_sparse::csc::CscMatrix; +use crate::common::csc_strategy; #[test] fn test_convert_dense_coo() { @@ -276,10 +277,6 @@ fn csr_strategy() -> impl Strategy> { csr(-5 ..= 5, 0..=6usize, 0..=6usize, 40) } -fn csc_strategy() -> impl Strategy> { - csc(-5 ..= 5, 0..=6usize, 0..=6usize, 40) -} - /// Avoid generating explicit zero values so that it is possible to reason about sparsity patterns fn non_zero_csr_strategy() -> impl Strategy> { csr(1 ..= 5, 0..=6usize, 0..=6usize, 40) diff --git a/nalgebra-sparse/tests/unit_tests/csc.rs b/nalgebra-sparse/tests/unit_tests/csc.rs index 140a5db2..a16e1686 100644 --- a/nalgebra-sparse/tests/unit_tests/csc.rs +++ b/nalgebra-sparse/tests/unit_tests/csc.rs @@ -1,5 +1,10 @@ use nalgebra_sparse::csc::CscMatrix; use nalgebra_sparse::SparseFormatErrorKind; +use nalgebra::DMatrix; + +use proptest::prelude::*; + +use crate::common::csc_strategy; #[test] fn csc_matrix_valid_data() { @@ -251,4 +256,19 @@ fn csc_matrix_get_index() { #[test] fn csc_matrix_col_iter() { // TODO +} + +proptest! { + #[test] + fn csc_double_transpose_is_identity(csc in csc_strategy()) { + prop_assert_eq!(csc.transpose().transpose(), csc); + } + + #[test] + fn csc_transpose_agrees_with_dense(csc in csc_strategy()) { + let dense_transpose = DMatrix::from(&csc).transpose(); + let csc_transpose = csc.transpose(); + prop_assert_eq!(dense_transpose, DMatrix::from(&csc_transpose)); + prop_assert_eq!(csc.nnz(), csc_transpose.nnz()); + } } \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit_tests/csr.rs b/nalgebra-sparse/tests/unit_tests/csr.rs index ab6f698e..424bc2c1 100644 --- a/nalgebra-sparse/tests/unit_tests/csr.rs +++ b/nalgebra-sparse/tests/unit_tests/csr.rs @@ -1,5 +1,8 @@ use nalgebra_sparse::csr::CsrMatrix; use nalgebra_sparse::SparseFormatErrorKind; +use nalgebra::DMatrix; +use proptest::prelude::*; +use crate::common::csr_strategy; #[test] fn csr_matrix_valid_data() { @@ -250,5 +253,20 @@ fn csr_matrix_get_index() { #[test] fn csr_matrix_row_iter() { + // TODO +} +proptest! { + #[test] + fn csr_double_transpose_is_identity(csr in csr_strategy()) { + prop_assert_eq!(csr.transpose().transpose(), csr); + } + + #[test] + fn csr_transpose_agrees_with_dense(csr in csr_strategy()) { + let dense_transpose = DMatrix::from(&csr).transpose(); + let csr_transpose = csr.transpose(); + prop_assert_eq!(dense_transpose, DMatrix::from(&csr_transpose)); + prop_assert_eq!(csr.nnz(), csr_transpose.nnz()); + } } \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index 14eb4aec..cc416790 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -13,6 +13,8 @@ use proptest::prelude::*; use std::panic::catch_unwind; use std::sync::Arc; +use crate::common::csr_strategy; + #[test] fn spmv_coo_agrees_with_dense_gemv() { let x = DVector::from_column_slice(&[2, 3, 4, 5]); @@ -89,9 +91,6 @@ fn spmm_csr_dense_args_strategy() -> impl Strategy> }) } -fn csr_strategy() -> impl Strategy> { - csr(-5 ..= 5, 0 ..= 6usize, 0 ..= 6usize, 40) -} fn dense_strategy() -> impl Strategy> { matrix(-5 ..= 5, 0 ..= 6, 0 ..= 6) From 921686c49058b78bc5fa4951a5827309189a7475 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Wed, 9 Dec 2020 15:25:16 +0100 Subject: [PATCH 036/183] Rename CsrMatrix::get(_mut) to get_entry(_mut) and change semantics --- nalgebra-sparse/src/csr.rs | 149 +++++++++++++++++++++++++------------ nalgebra-sparse/src/lib.rs | 41 +++++++++- 2 files changed, 143 insertions(+), 47 deletions(-) diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index 33348bd5..20db241b 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -1,6 +1,5 @@ //! An implementation of the CSR sparse matrix format. - -use crate::{SparseFormatError, SparseFormatErrorKind}; +use crate::{SparseFormatError, SparseFormatErrorKind, SparseEntry, SparseEntryMut}; use crate::pattern::{SparsityPattern, SparsityPatternFormatError, SparsityPatternIter}; use crate::csc::CscMatrix; @@ -298,40 +297,79 @@ impl CsrMatrix { pub fn pattern(&self) -> &Arc { &self.sparsity_pattern } -} - -impl CsrMatrix { - /// Return the value in the matrix at the given global row/col indices, or `None` if out of - /// bounds. - /// - /// If the indices are in bounds, but no explicitly stored entry is associated with it, - /// `T::zero()` is returned. Note that this method offers no way of distinguishing - /// explicitly stored zero entries from zero values that are only implicitly represented. - /// - /// Each call to this function incurs the cost of a binary search among the explicitly - /// stored column entries for the given row. - #[inline] - pub fn get(&self, row_index: usize, col_index: usize) -> Option { - self.get_row(row_index)?.get(col_index) - } - - /// Same as `get`, but panics if indices are out of bounds. - /// - /// Panics - /// ------ - /// Panics if either index is out of bounds. - #[inline] - pub fn index(&self, row_index: usize, col_index: usize) -> T { - self.get(row_index, col_index).unwrap() - } /// Reinterprets the CSR matrix as its transpose represented by a CSC matrix. + /// /// This operation does not touch the CSR data, and is effectively a no-op. pub fn transpose_as_csc(self) -> CscMatrix { let pattern = self.sparsity_pattern; let values = self.values; CscMatrix::try_from_pattern_and_values(pattern, values).unwrap() } + + /// Returns an entry for the given row/col indices, or `None` if the indices are out of bounds. + /// + /// Each call to this function incurs the cost of a binary search among the explicitly + /// stored column entries for the given row. + pub fn get_entry(&self, row_index: usize, col_index: usize) -> Option> { + let row_range = self.get_index_range(row_index)?; + let col_indices = &self.col_indices()[row_range.clone()]; + let values = &self.values()[row_range]; + get_entry_from_slices(self.ncols(), col_indices, values, col_index) + } + + /// Returns a mutable entry for the given row/col indices, or `None` if the indices are out + /// of bounds. + /// + /// Each call to this function incurs the cost of a binary search among the explicitly + /// stored column entries for the given row. + pub fn get_entry_mut(&mut self, row_index: usize, col_index: usize) + -> Option> { + let row_range = self.get_index_range(row_index)?; + let ncols = self.ncols(); + let (_, col_indices, values) = self.csr_data_mut(); + let col_indices = &col_indices[row_range.clone()]; + let values = &mut values[row_range]; + get_mut_entry_from_slices(ncols, col_indices, values, col_index) + } + + /// Returns an entry for the given row/col indices. + /// + /// Same as `get_entry`, except that it directly panics upon encountering row/col indices + /// out of bounds. + /// + /// Panics + /// ------ + /// Panics if `row_index` or `col_index` is out of bounds. + pub fn index_entry(&self, row_index: usize, col_index: usize) -> SparseEntry { + self.get_entry(row_index, col_index) + .expect("Out of bounds matrix indices encountered") + } + + /// Returns a mutable entry for the given row/col indices. + /// + /// Same as `get_entry_mut`, except that it directly panics upon encountering row/col indices + /// out of bounds. + /// + /// Panics + /// ------ + /// Panics if `row_index` or `col_index` is out of bounds. + pub fn index_entry_mut(&mut self, row_index: usize, col_index: usize) -> SparseEntryMut { + self.get_entry_mut(row_index, col_index) + .expect("Out of bounds matrix indices encountered") + } + + /// Returns a triplet of slices `(row_offsets, col_indices, values)` that make up the CSR data. + pub fn csr_data(&self) -> (&[usize], &[usize], &[T]) { + (self.row_offsets(), self.col_indices(), self.values()) + } + + /// Returns a triplet of slices `(row_offsets, col_indices, values)` that make up the CSR data, + /// where the `values` array is mutable. + pub fn csr_data_mut(&mut self) -> (&[usize], &[usize], &mut [T]) { + let pattern = self.sparsity_pattern.as_ref(); + (pattern.major_offsets(), pattern.minor_indices(), &mut self.values) + } } impl CsrMatrix @@ -465,32 +503,46 @@ macro_rules! impl_csr_row_common_methods { pub fn values(&self) -> &[T] { self.values } - } - impl<'a, T: Clone + Zero> $name { - /// Return the value in the matrix at the given global column index, or `None` if out of - /// bounds. - /// - /// If the index is in bounds, but no explicitly stored entry is associated with it, - /// `T::zero()` is returned. Note that this method offers no way of distinguishing - /// explicitly stored zero entries from zero values that are only implicitly represented. + /// Returns an entry for the given global column index. /// /// Each call to this function incurs the cost of a binary search among the explicitly - /// stored column entries for the current row. - pub fn get(&self, global_col_index: usize) -> Option { - let local_index = self.col_indices().binary_search(&global_col_index); - if let Ok(local_index) = local_index { - Some(self.values[local_index].clone()) - } else if global_col_index < self.ncols { - Some(T::zero()) - } else { - None - } + /// stored column entries. + pub fn get_entry(&self, global_col_index: usize) -> Option> { + get_entry_from_slices(self.ncols, self.col_indices, self.values, global_col_index) } } } } +fn get_entry_from_slices<'a, T>(ncols: usize, + col_indices: &'a [usize], + values: &'a [T], + global_col_index: usize) -> Option> { + let local_index = col_indices.binary_search(&global_col_index); + if let Ok(local_index) = local_index { + Some(SparseEntry::NonZero(&values[local_index])) + } else if global_col_index < ncols { + Some(SparseEntry::Zero) + } else { + None + } +} + +fn get_mut_entry_from_slices<'a, T>(ncols: usize, + col_indices: &'a [usize], + values: &'a mut [T], + global_col_index: usize) -> Option> { + let local_index = col_indices.binary_search(&global_col_index); + if let Ok(local_index) = local_index { + Some(SparseEntryMut::NonZero(&mut values[local_index])) + } else if global_col_index < ncols { + Some(SparseEntryMut::Zero) + } else { + None + } +} + impl_csr_row_common_methods!(CsrRow<'a, T>); impl_csr_row_common_methods!(CsrRowMut<'a, T>); @@ -508,6 +560,11 @@ impl<'a, T> CsrRowMut<'a, T> { pub fn cols_and_values_mut(&mut self) -> (&[usize], &mut [T]) { (self.col_indices, self.values) } + + /// Returns a mutable entry for the given global column index. + pub fn get_entry_mut(&mut self, global_col_index: usize) -> Option> { + get_mut_entry_from_slices(self.ncols, self.col_indices, self.values, global_col_index) + } } /// Row iterator for [CsrMatrix](struct.CsrMatrix.html). diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index 688362f9..536f4b75 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -95,6 +95,7 @@ pub mod proptest; use std::error::Error; use std::fmt; +use num_traits::Zero; /// Errors produced by functions that expect well-formed sparse format data. #[derive(Debug)] @@ -148,4 +149,42 @@ impl fmt::Display for SparseFormatError { } } -impl Error for SparseFormatError {} \ No newline at end of file +impl Error for SparseFormatError {} + +/// TODO +#[derive(Debug, PartialEq, Eq)] +pub enum SparseEntry<'a, T> { + /// TODO + NonZero(&'a T), + /// TODO + Zero +} + +impl<'a, T: Clone + Zero> SparseEntry<'a, T> { + /// TODO + pub fn to_value(self) -> T { + match self { + SparseEntry::NonZero(value) => value.clone(), + SparseEntry::Zero => T::zero() + } + } +} + +/// TODO +#[derive(Debug, PartialEq, Eq)] +pub enum SparseEntryMut<'a, T> { + /// TODO + NonZero(&'a mut T), + /// TODO + Zero +} + +impl<'a, T: Clone + Zero> SparseEntryMut<'a, T> { + /// TODO + pub fn to_value(self) -> T { + match self { + SparseEntryMut::NonZero(value) => value.clone(), + SparseEntryMut::Zero => T::zero() + } + } +} \ No newline at end of file From 41941e62c88ce427fc07740de154eebde6e4994a Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Thu, 10 Dec 2020 13:30:37 +0100 Subject: [PATCH 037/183] Implement CSR-CSR addition --- nalgebra-sparse/src/ops/impl_std_ops.rs | 68 ++++++++++++++++++ nalgebra-sparse/src/ops/mod.rs | 5 +- nalgebra-sparse/src/ops/serial/csr.rs | 91 +++++++++++++++++++++++- nalgebra-sparse/src/ops/serial/mod.rs | 24 ++++++- nalgebra-sparse/tests/unit_tests/ops.rs | 94 ++++++++++++++++++++++++- 5 files changed, 278 insertions(+), 4 deletions(-) diff --git a/nalgebra-sparse/src/ops/impl_std_ops.rs b/nalgebra-sparse/src/ops/impl_std_ops.rs index e69de29b..c8e9e800 100644 --- a/nalgebra-sparse/src/ops/impl_std_ops.rs +++ b/nalgebra-sparse/src/ops/impl_std_ops.rs @@ -0,0 +1,68 @@ +use crate::csr::CsrMatrix; + +use std::ops::Add; +use crate::ops::serial::{spadd_csr, spadd_build_pattern}; +use nalgebra::{ClosedAdd, ClosedMul, Scalar}; +use num_traits::{Zero, One}; +use std::sync::Arc; +use crate::ops::Transpose; +use crate::pattern::SparsityPattern; + +impl<'a, T> Add<&'a CsrMatrix> for &'a CsrMatrix +where + // TODO: Consider introducing wrapper trait for these things? It's technically a "Ring", + // I guess... + T: Scalar + ClosedAdd + ClosedMul + Zero + One +{ + type Output = CsrMatrix; + + fn add(self, rhs: &'a CsrMatrix) -> Self::Output { + let mut pattern = SparsityPattern::new(self.nrows(), self.ncols()); + spadd_build_pattern(&mut pattern, self.pattern(), rhs.pattern()); + let values = vec![T::zero(); pattern.nnz()]; + // We are giving data that is valid by definition, so it is safe to unwrap below + let mut result = CsrMatrix::try_from_pattern_and_values(Arc::new(pattern), values) + .unwrap(); + spadd_csr(&mut result, T::zero(), T::one(), Transpose(false), &self).unwrap(); + spadd_csr(&mut result, T::one(), T::one(), Transpose(false), &rhs).unwrap(); + result + } +} + +impl<'a, T> Add<&'a CsrMatrix> for CsrMatrix +where + T: Scalar + ClosedAdd + ClosedMul + Zero + One +{ + type Output = CsrMatrix; + + fn add(mut self, rhs: &'a CsrMatrix) -> Self::Output { + if Arc::ptr_eq(self.pattern(), rhs.pattern()) { + spadd_csr(&mut self, T::one(), T::one(), Transpose(false), &rhs).unwrap(); + self + } else { + &self + rhs + } + } +} + +impl<'a, T> Add> for &'a CsrMatrix + where + T: Scalar + ClosedAdd + ClosedMul + Zero + One +{ + type Output = CsrMatrix; + + fn add(self, rhs: CsrMatrix) -> Self::Output { + rhs + self + } +} + +impl Add> for CsrMatrix +where + T: Scalar + ClosedAdd + ClosedMul + Zero + One +{ + type Output = Self; + + fn add(self, rhs: CsrMatrix) -> Self::Output { + self + &rhs + } +} \ No newline at end of file diff --git a/nalgebra-sparse/src/ops/mod.rs b/nalgebra-sparse/src/ops/mod.rs index bf1698ec..08939d8a 100644 --- a/nalgebra-sparse/src/ops/mod.rs +++ b/nalgebra-sparse/src/ops/mod.rs @@ -1,5 +1,6 @@ //! TODO +mod impl_std_ops; pub mod serial; /// TODO @@ -11,4 +12,6 @@ impl Transpose { pub fn to_bool(&self) -> bool { self.0 } -} \ No newline at end of file +} + + diff --git a/nalgebra-sparse/src/ops/serial/csr.rs b/nalgebra-sparse/src/ops/serial/csr.rs index b77d1112..42ef6121 100644 --- a/nalgebra-sparse/src/ops/serial/csr.rs +++ b/nalgebra-sparse/src/ops/serial/csr.rs @@ -2,6 +2,9 @@ use crate::csr::CsrMatrix; use crate::ops::{Transpose}; use nalgebra::{Scalar, DMatrixSlice, ClosedAdd, ClosedMul, DMatrixSliceMut}; use num_traits::{Zero, One}; +use crate::ops::serial::{OperationError, OperationErrorType}; +use std::sync::Arc; +use crate::SparseEntryMut; /// Sparse-dense matrix-matrix multiplication `C <- beta * C + alpha * trans(A) * trans(B)`. pub fn spmm_csr_dense<'a, T>(c: impl Into>, @@ -65,4 +68,90 @@ where } } } -} \ No newline at end of file +} + +fn spadd_csr_unexpected_entry() -> OperationError { + OperationError::from_type_and_message( + OperationErrorType::InvalidPattern, + String::from("Found entry in `a` that is not present in `c`.")) +} + +/// Sparse matrix addition `C <- beta * C + alpha * trans(A)`. +/// +/// If the pattern of `c` does not accommodate all the non-zero entries in `a`, an error is +/// returned. +pub fn spadd_csr(c: &mut CsrMatrix, + beta: T, + alpha: T, + trans_a: Transpose, + a: &CsrMatrix) + -> Result<(), OperationError> +where + T: Scalar + ClosedAdd + ClosedMul + Zero + One +{ + // TODO: Proper error messages + if trans_a.to_bool() { + assert_eq!(c.nrows(), a.ncols()); + assert_eq!(c.ncols(), a.nrows()); + } else { + assert_eq!(c.nrows(), a.nrows()); + assert_eq!(c.ncols(), a.ncols()); + } + + // TODO: Change CsrMatrix::pattern() to return `&Arc` instead of `Arc` + if Arc::ptr_eq(&c.pattern(), &a.pattern()) { + // Special fast path: The two matrices have *exactly* the same sparsity pattern, + // so we only need to sum the value arrays + for (c_ij, a_ij) in c.values_mut().iter_mut().zip(a.values()) { + let (alpha, beta) = (alpha.inlined_clone(), beta.inlined_clone()); + *c_ij = beta * c_ij.inlined_clone() + alpha * a_ij.inlined_clone(); + } + Ok(()) + } else { + if trans_a.to_bool() + { + if beta != T::one() { + for c_ij in c.values_mut() { + *c_ij *= beta.inlined_clone(); + } + } + + for (i, a_row_i) in a.row_iter().enumerate() { + for (&j, a_val) in a_row_i.col_indices().iter().zip(a_row_i.values()) { + let a_val = a_val.inlined_clone(); + let alpha = alpha.inlined_clone(); + match c.index_entry_mut(j, i) { + SparseEntryMut::NonZero(c_ji) => { *c_ji += alpha * a_val } + SparseEntryMut::Zero => return Err(spadd_csr_unexpected_entry()), + } + } + } + } else { + for (mut c_row_i, a_row_i) in c.row_iter_mut().zip(a.row_iter()) { + if beta != T::one() { + for c_ij in c_row_i.values_mut() { + *c_ij *= beta.inlined_clone(); + } + } + + let (mut c_cols, mut c_vals) = c_row_i.cols_and_values_mut(); + let (a_cols, a_vals) = (a_row_i.col_indices(), a_row_i.values()); + + for (a_col, a_val) in a_cols.iter().zip(a_vals) { + // TODO: Use exponential search instead of linear search. + // If C has substantially more entries in the row than A, then a line search + // will needlessly visit many entries in C. + let (c_idx, _) = c_cols.iter() + .enumerate() + .find(|(_, c_col)| *c_col == a_col) + .ok_or_else(spadd_csr_unexpected_entry)?; + c_vals[c_idx] += alpha.inlined_clone() * a_val.inlined_clone(); + c_cols = &c_cols[c_idx ..]; + c_vals = &mut c_vals[c_idx ..]; + } + } + } + Ok(()) + } +} + diff --git a/nalgebra-sparse/src/ops/serial/mod.rs b/nalgebra-sparse/src/ops/serial/mod.rs index bb40419f..cd0dc09c 100644 --- a/nalgebra-sparse/src/ops/serial/mod.rs +++ b/nalgebra-sparse/src/ops/serial/mod.rs @@ -36,4 +36,26 @@ mod pattern; pub use coo::*; pub use csr::*; -pub use pattern::*; \ No newline at end of file +pub use pattern::*; + +/// TODO +#[derive(Clone, Debug)] +pub struct OperationError { + error_type: OperationErrorType, + message: String +} + +/// TODO +#[non_exhaustive] +#[derive(Clone, Debug)] +pub enum OperationErrorType { + /// TODO + InvalidPattern, +} + +impl OperationError { + /// TODO + pub fn from_type_and_message(error_type: OperationErrorType, message: String) -> Self { + Self { error_type, message } + } +} \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index cc416790..6c4f2006 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -1,5 +1,5 @@ use nalgebra_sparse::coo::CooMatrix; -use nalgebra_sparse::ops::serial::{spmv_coo, spmm_csr_dense, spadd_build_pattern}; +use nalgebra_sparse::ops::serial::{spmv_coo, spmm_csr_dense, spadd_build_pattern, spadd_csr}; use nalgebra_sparse::ops::{Transpose}; use nalgebra_sparse::csr::CsrMatrix; use nalgebra_sparse::proptest::{csr, sparsity_pattern}; @@ -15,6 +15,15 @@ use std::sync::Arc; use crate::common::csr_strategy; +/// Represents the sparsity pattern of a CSR matrix as a dense matrix with 0/1 +fn dense_csr_pattern(pattern: &SparsityPattern) -> DMatrix { + let boolean_csr = CsrMatrix::try_from_pattern_and_values( + Arc::new(pattern.clone()), + vec![1; pattern.nnz()]) + .unwrap(); + DMatrix::from(&boolean_csr) +} + #[test] fn spmv_coo_agrees_with_dense_gemv() { let x = DVector::from_column_slice(&[2, 3, 4, 5]); @@ -91,6 +100,37 @@ fn spmm_csr_dense_args_strategy() -> impl Strategy> }) } +#[derive(Debug)] +struct SpaddCsrArgs { + c: CsrMatrix, + beta: T, + alpha: T, + trans_a: Transpose, + a: CsrMatrix, +} + +fn spadd_csr_args_strategy() -> impl Strategy> { + let value_strategy = -5 ..= 5; + + // TODO :Support transposition + spadd_build_pattern_strategy() + .prop_flat_map(move |(a_pattern, b_pattern)| { + let mut c_pattern = SparsityPattern::new(a_pattern.major_dim(), b_pattern.major_dim()); + spadd_build_pattern(&mut c_pattern, &a_pattern, &b_pattern); + + let a_values = vec![value_strategy.clone(); a_pattern.nnz()]; + let c_values = vec![value_strategy.clone(); c_pattern.nnz()]; + let alpha = value_strategy.clone(); + let beta = value_strategy.clone(); + (Just(c_pattern), Just(a_pattern), c_values, a_values, alpha, beta, trans_strategy()) + }).prop_map(|(c_pattern, a_pattern, c_values, a_values, alpha, beta, trans_a)| { + let c = CsrMatrix::try_from_pattern_and_values(Arc::new(c_pattern), c_values).unwrap(); + let a = CsrMatrix::try_from_pattern_and_values(Arc::new(a_pattern), a_values).unwrap(); + + let a = if trans_a.to_bool() { a.transpose() } else { a }; + SpaddCsrArgs { c, beta, alpha, trans_a, a } + }) +} fn dense_strategy() -> impl Strategy> { matrix(-5 ..= 5, 0 ..= 6, 0 ..= 6) @@ -203,4 +243,56 @@ proptest! { prop_assert_eq!(&pattern_result, c_csr.pattern().as_ref()); } + + #[test] + fn spadd_csr_test(SpaddCsrArgs { c, beta, alpha, trans_a, a } in spadd_csr_args_strategy()) { + // Test that we get the expected result by comparing to an equivalent dense operation + // (here we give in the C matrix, so the sparsity pattern is essentially fixed) + + let mut c_sparse = c.clone(); + spadd_csr(&mut c_sparse, beta, alpha, trans_a, &a).unwrap(); + + let mut c_dense = DMatrix::from(&c); + let op_a_dense = DMatrix::from(&a); + let op_a_dense = if trans_a.to_bool() { op_a_dense.transpose() } else { op_a_dense }; + c_dense = beta * c_dense + alpha * &op_a_dense; + + prop_assert_eq!(&DMatrix::from(&c_sparse), &c_dense); + } + + #[test] + fn csr_add_csr( + // a and b have the same dimensions + (a, b) + in csr_strategy() + .prop_flat_map(|a| { + let b = csr(-5 ..= 5, Just(a.nrows()), Just(a.ncols()), 40); + (Just(a), b) + })) + { + // We use the dense result as the ground truth for the arithmetic result + let c_dense = DMatrix::from(&a) + DMatrix::from(&b); + // However, it's not enough only to cover the dense result, we also need to verify the + // sparsity pattern. We can determine the exact sparsity pattern by using + // dense arithmetic with positive integer values and extracting positive entries. + let c_dense_pattern = dense_csr_pattern(a.pattern()) + dense_csr_pattern(b.pattern()); + let c_pattern = CsrMatrix::from(&c_dense_pattern).pattern().clone(); + + // Check each combination of owned matrices and references + let c_owned_owned = a.clone() + b.clone(); + prop_assert_eq!(&DMatrix::from(&c_owned_owned), &c_dense); + prop_assert_eq!(c_owned_owned.pattern(), &c_pattern); + + let c_owned_ref = a.clone() + &b; + prop_assert_eq!(&DMatrix::from(&c_owned_ref), &c_dense); + prop_assert_eq!(c_owned_ref.pattern(), &c_pattern); + + let c_ref_owned = &a + b.clone(); + prop_assert_eq!(&DMatrix::from(&c_ref_owned), &c_dense); + prop_assert_eq!(c_ref_owned.pattern(), &c_pattern); + + let c_ref_ref = &a + &b; + prop_assert_eq!(&DMatrix::from(&c_ref_ref), &c_dense); + prop_assert_eq!(c_ref_ref.pattern(), &c_pattern); + } } \ No newline at end of file From c4285d9fb3f0f80f466d936ccbd23a5c5d7828a0 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 14 Dec 2020 11:18:16 +0100 Subject: [PATCH 038/183] remove spmv_coo --- nalgebra-sparse/src/ops/serial/coo.rs | 72 ------------------------- nalgebra-sparse/src/ops/serial/mod.rs | 2 - nalgebra-sparse/tests/unit_tests/ops.rs | 30 +---------- 3 files changed, 2 insertions(+), 102 deletions(-) delete mode 100644 nalgebra-sparse/src/ops/serial/coo.rs diff --git a/nalgebra-sparse/src/ops/serial/coo.rs b/nalgebra-sparse/src/ops/serial/coo.rs deleted file mode 100644 index 322c6914..00000000 --- a/nalgebra-sparse/src/ops/serial/coo.rs +++ /dev/null @@ -1,72 +0,0 @@ -//! Matrix operations involving sparse matrices. - -use crate::coo::CooMatrix; -use nalgebra::base::storage::{Storage, StorageMut}; -use nalgebra::{ClosedAdd, ClosedMul, Dim, Scalar, Vector}; -use num_traits::{One, Zero}; - -/// Sparse matrix-vector multiplication `y = beta * y + alpha * A * x`. -/// -/// Computes a matrix-vector product with the COO matrix "A" and the vector `x`, storing the -/// result in `y`. -/// -/// If `beta == 0`, the elements in `y` are never read. -/// -/// TODO: Rethink this function -/// -/// Panics -/// ------ -/// -/// Panics if `y`, `a` and `x` do not have compatible dimensions. -pub fn spmv_coo( - beta: T, - y: &mut Vector, - alpha: T, - a: &CooMatrix, - x: &Vector, -) where - T: Scalar + ClosedAdd + ClosedMul + Zero + One, - YDim: Dim, - XDim: Dim, - Y: StorageMut, - X: Storage, -{ - assert_eq!( - y.len(), - a.nrows(), - "y and a must be dimensionally compatible" - ); - assert_eq!( - a.ncols(), - x.len(), - "a and x must be dimensionally compatible" - ); - - if beta == T::zero() { - // If `y` is constructed through `new_uninitialized()`, we must make sure to not read - // any of the elements in order to avoid UB, so we special case beta == 0 - // in order to ensure that we only write, not read, the elements in y. - for y_i in y.iter_mut() { - *y_i = T::zero(); - } - } else if beta != T::one() { - // Since the COO triplets have no particular structure, we cannot combine initialization - // of y with the triplet loop below, and instead have to do it in a pre-pass. - for y_i in y.iter_mut() { - *y_i *= beta.inlined_clone(); - } - } - - for (i, j, v) in a.triplet_iter() { - // TODO: We could skip bounds checks with unsafe here, since COO ensures that all indices - // are in bounds and we assert on dimensions up-front. - // The compiler will not be able to elide the checks, since we're doing - // random/unpredictable access to elements in `x` and `y`. - let (alpha, v, x_j) = ( - alpha.inlined_clone(), - v.inlined_clone(), - x[j].inlined_clone(), - ); - y[i] += alpha * v * x_j; - } -} diff --git a/nalgebra-sparse/src/ops/serial/mod.rs b/nalgebra-sparse/src/ops/serial/mod.rs index cd0dc09c..01c31d93 100644 --- a/nalgebra-sparse/src/ops/serial/mod.rs +++ b/nalgebra-sparse/src/ops/serial/mod.rs @@ -30,11 +30,9 @@ macro_rules! assert_compatible_spmm_dims { } } -mod coo; mod csr; mod pattern; -pub use coo::*; pub use csr::*; pub use pattern::*; diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index 6c4f2006..a7f82b9a 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -1,11 +1,10 @@ -use nalgebra_sparse::coo::CooMatrix; -use nalgebra_sparse::ops::serial::{spmv_coo, spmm_csr_dense, spadd_build_pattern, spadd_csr}; +use nalgebra_sparse::ops::serial::{spmm_csr_dense, spadd_build_pattern, spadd_csr}; use nalgebra_sparse::ops::{Transpose}; use nalgebra_sparse::csr::CsrMatrix; use nalgebra_sparse::proptest::{csr, sparsity_pattern}; use nalgebra_sparse::pattern::SparsityPattern; -use nalgebra::{DVector, DMatrix, Scalar, DMatrixSliceMut, DMatrixSlice}; +use nalgebra::{DMatrix, Scalar, DMatrixSliceMut, DMatrixSlice}; use nalgebra::proptest::matrix; use proptest::prelude::*; @@ -24,31 +23,6 @@ fn dense_csr_pattern(pattern: &SparsityPattern) -> DMatrix { DMatrix::from(&boolean_csr) } -#[test] -fn spmv_coo_agrees_with_dense_gemv() { - let x = DVector::from_column_slice(&[2, 3, 4, 5]); - - let i = vec![0, 0, 1, 1, 2, 2]; - let j = vec![0, 3, 0, 1, 1, 3]; - let v = vec![3, 2, 1, 2, 3, 1]; - let a = CooMatrix::try_from_triplets(3, 4, i, j, v).unwrap(); - - let betas = [0, 1, 2]; - let alphas = [0, 1, 2]; - - for &beta in &betas { - for &alpha in &alphas { - let mut y = DVector::from_column_slice(&[2, 5, 3]); - let mut y_dense = y.clone(); - spmv_coo(beta, &mut y, alpha, &a, &x); - - y_dense.gemv(alpha, &DMatrix::from(&a), &x, beta); - - assert_eq!(y, y_dense); - } - } -} - #[derive(Debug)] struct SpmmCsrDenseArgs { c: DMatrix, From 9db17f00e76ad901cc89bcfc01f31f57a5dc1845 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 14 Dec 2020 16:55:06 +0100 Subject: [PATCH 039/183] Implement spmm_pattern --- nalgebra-sparse/src/ops/serial/csr.rs | 5 +-- nalgebra-sparse/src/ops/serial/pattern.rs | 43 ++++++++++++++++++++--- nalgebra-sparse/tests/common/mod.rs | 8 +++-- nalgebra-sparse/tests/unit_tests/ops.rs | 35 ++++++++++++++++-- 4 files changed, 80 insertions(+), 11 deletions(-) diff --git a/nalgebra-sparse/src/ops/serial/csr.rs b/nalgebra-sparse/src/ops/serial/csr.rs index 42ef6121..16ef3b04 100644 --- a/nalgebra-sparse/src/ops/serial/csr.rs +++ b/nalgebra-sparse/src/ops/serial/csr.rs @@ -1,10 +1,11 @@ use crate::csr::CsrMatrix; use crate::ops::{Transpose}; +use crate::SparseEntryMut; +use crate::ops::serial::{OperationError, OperationErrorType}; use nalgebra::{Scalar, DMatrixSlice, ClosedAdd, ClosedMul, DMatrixSliceMut}; use num_traits::{Zero, One}; -use crate::ops::serial::{OperationError, OperationErrorType}; use std::sync::Arc; -use crate::SparseEntryMut; +use std::borrow::Cow; /// Sparse-dense matrix-matrix multiplication `C <- beta * C + alpha * trans(A) * trans(B)`. pub fn spmm_csr_dense<'a, T>(c: impl Into>, diff --git a/nalgebra-sparse/src/ops/serial/pattern.rs b/nalgebra-sparse/src/ops/serial/pattern.rs index 6507ad1b..c8d4a586 100644 --- a/nalgebra-sparse/src/ops/serial/pattern.rs +++ b/nalgebra-sparse/src/ops/serial/pattern.rs @@ -32,7 +32,7 @@ pub fn spadd_build_pattern(pattern: &mut SparsityPattern, for lane_idx in 0 .. a.major_dim() { let lane_a = a.lane(lane_idx); let lane_b = b.lane(lane_idx); - indices.extend(iterate_intersection(lane_a, lane_b)); + indices.extend(iterate_union(lane_a, lane_b)); offsets.push(indices.len()); } @@ -43,11 +43,44 @@ pub fn spadd_build_pattern(pattern: &mut SparsityPattern, swap(input_pattern, &mut new_pattern); } -/// Iterate over the intersection of the two sets represented by sorted slices +/// Sparse matrix multiplication pattern construction, `C <- A * B`. +pub fn spmm_pattern(a: &SparsityPattern, b: &SparsityPattern) -> SparsityPattern { + // TODO: Proper error message + assert_eq!(a.minor_dim(), b.major_dim()); + + let mut offsets = Vec::new(); + let mut indices = Vec::new(); + offsets.push(0); + + let mut c_lane_workspace = Vec::new(); + for i in 0 .. a.major_dim() { + let a_lane_i = a.lane(i); + let c_lane_i_offset = *offsets.last().unwrap(); + for &k in a_lane_i { + // We have that the set of elements in lane i in C is given by the union of all + // B_k, where B_k is the set of indices in lane k of B. More precisely, let C_i + // denote the set of indices in lane i in C, and similarly for A_i and B_k. Then + // C_i = union B_k for all k in A_i + // We incrementally compute C_i by incrementally computing the union of C_i with + // B_k until we're through all k in A_i. + let b_lane_k = b.lane(k); + let c_lane_i = &indices[c_lane_i_offset..]; + c_lane_workspace.clear(); + c_lane_workspace.extend(iterate_union(c_lane_i, b_lane_k)); + indices.truncate(c_lane_i_offset); + indices.append(&mut c_lane_workspace); + } + offsets.push(indices.len()); + } + + SparsityPattern::try_from_offsets_and_indices(a.major_dim(), b.minor_dim(), offsets, indices) + .expect("Internal error: Invalid pattern during matrix multiplication pattern construction") +} + +/// Iterate over the union of the two sets represented by sorted slices /// (with unique elements) -fn iterate_intersection<'a>(mut sorted_a: &'a [usize], - mut sorted_b: &'a [usize]) -> impl Iterator + 'a { - // TODO: Can use a kind of simultaneous exponential search to speed things up here +fn iterate_union<'a>(mut sorted_a: &'a [usize], + mut sorted_b: &'a [usize]) -> impl Iterator + 'a { iter::from_fn(move || { if let (Some(a_item), Some(b_item)) = (sorted_a.first(), sorted_b.first()) { let item = if a_item < b_item { diff --git a/nalgebra-sparse/tests/common/mod.rs b/nalgebra-sparse/tests/common/mod.rs index 6e730b7d..21751e50 100644 --- a/nalgebra-sparse/tests/common/mod.rs +++ b/nalgebra-sparse/tests/common/mod.rs @@ -2,6 +2,7 @@ use proptest::strategy::Strategy; use nalgebra_sparse::csr::CsrMatrix; use nalgebra_sparse::proptest::{csr, csc}; use nalgebra_sparse::csc::CscMatrix; +use std::ops::RangeInclusive; #[macro_export] macro_rules! assert_panics { @@ -24,10 +25,13 @@ macro_rules! assert_panics { }}; } +pub const PROPTEST_MATRIX_DIM: RangeInclusive = 0..=6; +pub const PROPTEST_MAX_NNZ: usize = 40; + pub fn csr_strategy() -> impl Strategy> { - csr(-5 ..= 5, 0 ..= 6usize, 0 ..= 6usize, 40) + csr(-5 ..= 5, PROPTEST_MATRIX_DIM, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ) } pub fn csc_strategy() -> impl Strategy> { - csc(-5 ..= 5, 0..=6usize, 0..=6usize, 40) + csc(-5 ..= 5, PROPTEST_MATRIX_DIM, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ) } diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index a7f82b9a..2a46ee32 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -1,4 +1,4 @@ -use nalgebra_sparse::ops::serial::{spmm_csr_dense, spadd_build_pattern, spadd_csr}; +use nalgebra_sparse::ops::serial::{spmm_csr_dense, spadd_build_pattern, spmm_pattern, spadd_csr}; use nalgebra_sparse::ops::{Transpose}; use nalgebra_sparse::csr::CsrMatrix; use nalgebra_sparse::proptest::{csr, sparsity_pattern}; @@ -12,7 +12,7 @@ use proptest::prelude::*; use std::panic::catch_unwind; use std::sync::Arc; -use crate::common::csr_strategy; +use crate::common::{csr_strategy, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ}; /// Represents the sparsity pattern of a CSR matrix as a dense matrix with 0/1 fn dense_csr_pattern(pattern: &SparsityPattern) -> DMatrix { @@ -127,6 +127,15 @@ fn spadd_build_pattern_strategy() -> impl Strategy impl Strategy { + pattern_strategy() + .prop_flat_map(|a| { + let b = sparsity_pattern(Just(a.minor_dim()), PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ); + (Just(a), b) + }) +} + /// Helper function to help us call dense GEMM with our transposition parameters fn dense_gemm<'a>(c: impl Into>, beta: i32, @@ -269,4 +278,26 @@ proptest! { prop_assert_eq!(&DMatrix::from(&c_ref_ref), &c_dense); prop_assert_eq!(c_ref_ref.pattern(), &c_pattern); } + + #[test] + fn spmm_pattern_test((a, b) in spmm_pattern_strategy()) + { + // (a, b) are multiplication-wise dimensionally compatible patterns + let c_pattern = spmm_pattern(&a, &b); + + // To verify the pattern, we construct CSR matrices with positive integer entries + // corresponding to a and b, and convert them to dense matrices. + // The product of these dense matrices will then have non-zeros in exactly the same locations + // as the result of "multiplying" the sparsity patterns + let a_csr = CsrMatrix::try_from_pattern_and_values(Arc::new(a.clone()), vec![1; a.nnz()]) + .unwrap(); + let a_dense = DMatrix::from(&a_csr); + let b_csr = CsrMatrix::try_from_pattern_and_values(Arc::new(b.clone()), vec![1; b.nnz()]) + .unwrap(); + let b_dense = DMatrix::from(&b_csr); + let c_dense = a_dense * b_dense; + let c_csr = CsrMatrix::from(&c_dense); + + prop_assert_eq!(&c_pattern, c_csr.pattern().as_ref()); + } } \ No newline at end of file From 2d534a613332fc265de70b6a17f3905d6f44aa24 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Wed, 16 Dec 2020 14:06:12 +0100 Subject: [PATCH 040/183] Implement spmm_csr --- nalgebra-sparse/src/ops/serial/csr.rs | 64 +++++++++++ nalgebra-sparse/src/ops/serial/pattern.rs | 38 ++++--- nalgebra-sparse/tests/common/mod.rs | 1 + nalgebra-sparse/tests/unit_tests/ops.rs | 124 +++++++++++++++++++--- 4 files changed, 200 insertions(+), 27 deletions(-) diff --git a/nalgebra-sparse/src/ops/serial/csr.rs b/nalgebra-sparse/src/ops/serial/csr.rs index 16ef3b04..9f14d8f1 100644 --- a/nalgebra-sparse/src/ops/serial/csr.rs +++ b/nalgebra-sparse/src/ops/serial/csr.rs @@ -156,3 +156,67 @@ where } } +fn spmm_csr_unexpected_entry() -> OperationError { + OperationError::from_type_and_message( + OperationErrorType::InvalidPattern, + String::from("Found unexpected entry that is not present in `c`.")) +} + +/// Sparse-sparse matrix multiplication, `C <- beta * C + alpha * op(A) * op(B)`. +pub fn spmm_csr<'a, T>( + c: &mut CsrMatrix, + beta: T, + alpha: T, + trans_a: Transpose, + a: &CsrMatrix, + trans_b: Transpose, + b: &CsrMatrix) +-> Result<(), OperationError> +where + T: Scalar + ClosedAdd + ClosedMul + Zero + One +{ + assert_compatible_spmm_dims!(c, a, b, trans_a, trans_b); + + if !trans_a.to_bool() && !trans_b.to_bool() { + for (mut c_row_i, a_row_i) in c.row_iter_mut().zip(a.row_iter()) { + for c_ij in c_row_i.values_mut() { + *c_ij = beta.inlined_clone() * c_ij.inlined_clone(); + } + + for (&k, a_ik) in a_row_i.col_indices().iter().zip(a_row_i.values()) { + let b_row_k = b.row(k); + let (mut c_row_i_cols, mut c_row_i_values) = c_row_i.cols_and_values_mut(); + let alpha_aik = alpha.inlined_clone() * a_ik.inlined_clone(); + for (j, b_kj) in b_row_k.col_indices().iter().zip(b_row_k.values()) { + // Determine the location in C to append the value + let (c_local_idx, _) = c_row_i_cols.iter() + .enumerate() + .find(|(_, c_col)| *c_col == j) + .ok_or_else(spmm_csr_unexpected_entry)?; + + c_row_i_values[c_local_idx] += alpha_aik.inlined_clone() * b_kj.inlined_clone(); + c_row_i_cols = &c_row_i_cols[c_local_idx ..]; + c_row_i_values = &mut c_row_i_values[c_local_idx ..]; + } + } + } + Ok(()) + } else { + // Currently we handle transposition by explicitly precomputing transposed matrices + // and calling the operation again without transposition + // TODO: At least use workspaces to allow control of allocations. Maybe + // consider implementing certain patterns (like A^T * B) explicitly + let (a, b) = { + use Cow::*; + match (trans_a, trans_b) { + (Transpose(false), Transpose(false)) => unreachable!(), + (Transpose(true), Transpose(false)) => (Owned(a.transpose()), Borrowed(b)), + (Transpose(false), Transpose(true)) => (Borrowed(a), Owned(b.transpose())), + (Transpose(true), Transpose(true)) => (Owned(a.transpose()), Owned(b.transpose())) + } + }; + + spmm_csr(c, beta, alpha, Transpose(false), a.as_ref(), Transpose(false), b.as_ref()) + } +} + diff --git a/nalgebra-sparse/src/ops/serial/pattern.rs b/nalgebra-sparse/src/ops/serial/pattern.rs index c8d4a586..2e442cc0 100644 --- a/nalgebra-sparse/src/ops/serial/pattern.rs +++ b/nalgebra-sparse/src/ops/serial/pattern.rs @@ -45,31 +45,41 @@ pub fn spadd_build_pattern(pattern: &mut SparsityPattern, /// Sparse matrix multiplication pattern construction, `C <- A * B`. pub fn spmm_pattern(a: &SparsityPattern, b: &SparsityPattern) -> SparsityPattern { - // TODO: Proper error message - assert_eq!(a.minor_dim(), b.major_dim()); + assert_eq!(a.minor_dim(), b.major_dim(), "a and b must have compatible dimensions"); let mut offsets = Vec::new(); let mut indices = Vec::new(); offsets.push(0); - let mut c_lane_workspace = Vec::new(); + // Keep a vector of whether we have visited a particular minor index when working + // on a major lane + // TODO: Consider using a bitvec or similar here to reduce pressure on memory + // (would cut memory use to 1/8, which might help reduce cache misses) + let mut visited = vec![false; b.minor_dim()]; + for i in 0 .. a.major_dim() { let a_lane_i = a.lane(i); let c_lane_i_offset = *offsets.last().unwrap(); for &k in a_lane_i { - // We have that the set of elements in lane i in C is given by the union of all - // B_k, where B_k is the set of indices in lane k of B. More precisely, let C_i - // denote the set of indices in lane i in C, and similarly for A_i and B_k. Then - // C_i = union B_k for all k in A_i - // We incrementally compute C_i by incrementally computing the union of C_i with - // B_k until we're through all k in A_i. let b_lane_k = b.lane(k); - let c_lane_i = &indices[c_lane_i_offset..]; - c_lane_workspace.clear(); - c_lane_workspace.extend(iterate_union(c_lane_i, b_lane_k)); - indices.truncate(c_lane_i_offset); - indices.append(&mut c_lane_workspace); + + for &j in b_lane_k { + let have_visited_j = &mut visited[j]; + if !*have_visited_j { + indices.push(j); + *have_visited_j = true; + } + } } + + let c_lane_i = &mut indices[c_lane_i_offset ..]; + c_lane_i.sort_unstable(); + + // Reset visits so that visited[j] == false for all j for the next major lane + for j in c_lane_i { + visited[*j] = false; + } + offsets.push(indices.len()); } diff --git a/nalgebra-sparse/tests/common/mod.rs b/nalgebra-sparse/tests/common/mod.rs index 21751e50..0a6e31a1 100644 --- a/nalgebra-sparse/tests/common/mod.rs +++ b/nalgebra-sparse/tests/common/mod.rs @@ -27,6 +27,7 @@ macro_rules! assert_panics { pub const PROPTEST_MATRIX_DIM: RangeInclusive = 0..=6; pub const PROPTEST_MAX_NNZ: usize = 40; +pub const PROPTEST_I32_VALUE_STRATEGY: RangeInclusive = -5 ..= 5; pub fn csr_strategy() -> impl Strategy> { csr(-5 ..= 5, PROPTEST_MATRIX_DIM, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ) diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index 2a46ee32..60e6cbcd 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -1,4 +1,6 @@ -use nalgebra_sparse::ops::serial::{spmm_csr_dense, spadd_build_pattern, spmm_pattern, spadd_csr}; +use crate::common::{csr_strategy, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ, + PROPTEST_I32_VALUE_STRATEGY}; +use nalgebra_sparse::ops::serial::{spmm_csr_dense, spadd_build_pattern, spmm_pattern, spadd_csr, spmm_csr}; use nalgebra_sparse::ops::{Transpose}; use nalgebra_sparse::csr::CsrMatrix; use nalgebra_sparse::proptest::{csr, sparsity_pattern}; @@ -12,8 +14,6 @@ use proptest::prelude::*; use std::panic::catch_unwind; use std::sync::Arc; -use crate::common::{csr_strategy, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ}; - /// Represents the sparsity pattern of a CSR matrix as a dense matrix with 0/1 fn dense_csr_pattern(pattern: &SparsityPattern) -> DMatrix { let boolean_csr = CsrMatrix::try_from_pattern_and_values( @@ -37,11 +37,11 @@ struct SpmmCsrDenseArgs { /// Returns matrices C, A and B with compatible dimensions such that it can be used /// in an `spmm` operation `C = beta * C + alpha * trans(A) * trans(B)`. fn spmm_csr_dense_args_strategy() -> impl Strategy> { - let max_nnz = 40; - let value_strategy = -5 ..= 5; - let c_rows = 0 ..= 6usize; - let c_cols = 0 ..= 6usize; - let common_dim = 0 ..= 6usize; + let max_nnz = PROPTEST_MAX_NNZ; + let value_strategy = PROPTEST_I32_VALUE_STRATEGY; + let c_rows = PROPTEST_MATRIX_DIM; + let c_cols = PROPTEST_MATRIX_DIM; + let common_dim = PROPTEST_MATRIX_DIM; let trans_strategy = trans_strategy(); let c_matrix_strategy = matrix(value_strategy.clone(), c_rows, c_cols); @@ -84,9 +84,8 @@ struct SpaddCsrArgs { } fn spadd_csr_args_strategy() -> impl Strategy> { - let value_strategy = -5 ..= 5; + let value_strategy = PROPTEST_I32_VALUE_STRATEGY; - // TODO :Support transposition spadd_build_pattern_strategy() .prop_flat_map(move |(a_pattern, b_pattern)| { let mut c_pattern = SparsityPattern::new(a_pattern.major_dim(), b_pattern.major_dim()); @@ -107,7 +106,7 @@ fn spadd_csr_args_strategy() -> impl Strategy> { } fn dense_strategy() -> impl Strategy> { - matrix(-5 ..= 5, 0 ..= 6, 0 ..= 6) + matrix(PROPTEST_I32_VALUE_STRATEGY, PROPTEST_MATRIX_DIM, PROPTEST_MATRIX_DIM) } fn trans_strategy() -> impl Strategy + Clone { @@ -115,14 +114,14 @@ fn trans_strategy() -> impl Strategy + Clone { } fn pattern_strategy() -> impl Strategy { - sparsity_pattern(0 ..= 6usize, 0..= 6usize, 40) + sparsity_pattern(PROPTEST_MATRIX_DIM, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ) } /// Constructs pairs (a, b) where a and b have the same dimensions fn spadd_build_pattern_strategy() -> impl Strategy { pattern_strategy() .prop_flat_map(|a| { - let b = sparsity_pattern(Just(a.major_dim()), Just(a.minor_dim()), 40); + let b = sparsity_pattern(Just(a.major_dim()), Just(a.minor_dim()), PROPTEST_MAX_NNZ); (Just(a), b) }) } @@ -136,6 +135,50 @@ fn spmm_pattern_strategy() -> impl Strategy { + c: CsrMatrix, + beta: T, + alpha: T, + trans_a: Transpose, + a: CsrMatrix, + trans_b: Transpose, + b: CsrMatrix +} + +fn spmm_csr_args_strategy() -> impl Strategy> { + spmm_pattern_strategy() + .prop_flat_map(|(a_pattern, b_pattern)| { + let a_values = vec![PROPTEST_I32_VALUE_STRATEGY; a_pattern.nnz()]; + let b_values = vec![PROPTEST_I32_VALUE_STRATEGY; b_pattern.nnz()]; + let c_pattern = spmm_pattern(&a_pattern, &b_pattern); + let c_values = vec![PROPTEST_I32_VALUE_STRATEGY; c_pattern.nnz()]; + let a_pattern = Arc::new(a_pattern); + let b_pattern = Arc::new(b_pattern); + let c_pattern = Arc::new(c_pattern); + let a = a_values.prop_map(move |values| + CsrMatrix::try_from_pattern_and_values(Arc::clone(&a_pattern), values).unwrap()); + let b = b_values.prop_map(move |values| + CsrMatrix::try_from_pattern_and_values(Arc::clone(&b_pattern), values).unwrap()); + let c = c_values.prop_map(move |values| + CsrMatrix::try_from_pattern_and_values(Arc::clone(&c_pattern), values).unwrap()); + let alpha = PROPTEST_I32_VALUE_STRATEGY; + let beta = PROPTEST_I32_VALUE_STRATEGY; + (c, beta, alpha, trans_strategy(), a, trans_strategy(), b) + }) + .prop_map(|(c, beta, alpha, trans_a, a, trans_b, b)| { + SpmmCsrArgs:: { + c, + beta, + alpha, + trans_a, + a: if trans_a.to_bool() { a.transpose() } else { a }, + trans_b, + b: if trans_b.to_bool() { b.transpose() } else { b } + } + }) +} + /// Helper function to help us call dense GEMM with our transposition parameters fn dense_gemm<'a>(c: impl Into>, beta: i32, @@ -300,4 +343,59 @@ proptest! { prop_assert_eq!(&c_pattern, c_csr.pattern().as_ref()); } + + #[test] + fn spmm_csr_test(SpmmCsrArgs { c, beta, alpha, trans_a, a, trans_b, b } + in spmm_csr_args_strategy() + ) { + // Test that we get the expected result by comparing to an equivalent dense operation + // (here we give in the C matrix, so the sparsity pattern is essentially fixed) + let mut c_sparse = c.clone(); + spmm_csr(&mut c_sparse, beta, alpha, trans_a, &a, trans_b, &b).unwrap(); + + let mut c_dense = DMatrix::from(&c); + let op_a_dense = DMatrix::from(&a); + let op_a_dense = if trans_a.to_bool() { op_a_dense.transpose() } else { op_a_dense }; + let op_b_dense = DMatrix::from(&b); + let op_b_dense = if trans_b.to_bool() { op_b_dense.transpose() } else { op_b_dense }; + c_dense = beta * c_dense + alpha * &op_a_dense * op_b_dense; + + prop_assert_eq!(&DMatrix::from(&c_sparse), &c_dense); + } + + #[test] + fn spmm_csr_panics_on_dim_mismatch( + (alpha, beta, c, a, b, trans_a, trans_b) + in (PROPTEST_I32_VALUE_STRATEGY, + PROPTEST_I32_VALUE_STRATEGY, + csr_strategy(), + csr_strategy(), + csr_strategy(), + trans_strategy(), + trans_strategy()) + ) { + // We refer to `A * B` as the "product" + let product_rows = if trans_a.to_bool() { a.ncols() } else { a.nrows() }; + let product_cols = if trans_b.to_bool() { b.nrows() } else { b.ncols() }; + // Determine the common dimension in the product + // from the perspective of a and b, respectively + let product_a_common = if trans_a.to_bool() { a.nrows() } else { a.ncols() }; + let product_b_common = if trans_b.to_bool() { b.ncols() } else { b.nrows() }; + + let dims_are_compatible = product_rows == c.nrows() + && product_cols == c.ncols() + && product_a_common == product_b_common; + + // If the dimensions randomly happen to be compatible, then of course we need to + // skip the test, so we assume that they are not. + prop_assume!(!dims_are_compatible); + + let result = catch_unwind(|| { + let mut spmm_result = c.clone(); + spmm_csr(&mut spmm_result, beta, alpha, trans_a, &a, trans_b, &b).unwrap(); + }); + + prop_assert!(result.is_err(), + "The SPMM kernel executed successfully despite mismatch dimensions"); + } } \ No newline at end of file From d9cfe5cb3effc501d9d680a4912376ea02a0cc43 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Wed, 16 Dec 2020 14:21:35 +0100 Subject: [PATCH 041/183] Improve dimension assertions for spadd_csr --- nalgebra-sparse/src/ops/serial/csr.rs | 9 +-------- nalgebra-sparse/src/ops/serial/mod.rs | 18 +++++++++++++++++ nalgebra-sparse/tests/unit_tests/ops.rs | 27 +++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/nalgebra-sparse/src/ops/serial/csr.rs b/nalgebra-sparse/src/ops/serial/csr.rs index 9f14d8f1..670711cd 100644 --- a/nalgebra-sparse/src/ops/serial/csr.rs +++ b/nalgebra-sparse/src/ops/serial/csr.rs @@ -90,14 +90,7 @@ pub fn spadd_csr(c: &mut CsrMatrix, where T: Scalar + ClosedAdd + ClosedMul + Zero + One { - // TODO: Proper error messages - if trans_a.to_bool() { - assert_eq!(c.nrows(), a.ncols()); - assert_eq!(c.ncols(), a.nrows()); - } else { - assert_eq!(c.nrows(), a.nrows()); - assert_eq!(c.ncols(), a.ncols()); - } + assert_compatible_spadd_dims!(c, a, trans_a); // TODO: Change CsrMatrix::pattern() to return `&Arc` instead of `Arc` if Arc::ptr_eq(&c.pattern(), &a.pattern()) { diff --git a/nalgebra-sparse/src/ops/serial/mod.rs b/nalgebra-sparse/src/ops/serial/mod.rs index 01c31d93..8ac22ac8 100644 --- a/nalgebra-sparse/src/ops/serial/mod.rs +++ b/nalgebra-sparse/src/ops/serial/mod.rs @@ -30,6 +30,24 @@ macro_rules! assert_compatible_spmm_dims { } } +#[macro_use] +macro_rules! assert_compatible_spadd_dims { + ($c:expr, $a:expr, $trans_a:expr) => { + use crate::ops::Transpose; + match $trans_a { + Transpose(false) => { + assert_eq!($c.nrows(), $a.nrows(), "C.nrows() != A.nrows()"); + assert_eq!($c.ncols(), $a.ncols(), "C.ncols() != A.ncols()"); + }, + Transpose(true) => { + assert_eq!($c.nrows(), $a.ncols(), "C.nrows() != A.ncols()"); + assert_eq!($c.ncols(), $a.nrows(), "C.ncols() != A.nrows()"); + } + } + + } +} + mod csr; mod pattern; diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index 60e6cbcd..c1df496f 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -398,4 +398,31 @@ proptest! { prop_assert!(result.is_err(), "The SPMM kernel executed successfully despite mismatch dimensions"); } + + #[test] + fn spadd_csr_panics_on_dim_mismatch( + (alpha, beta, c, a, trans_a) + in (PROPTEST_I32_VALUE_STRATEGY, + PROPTEST_I32_VALUE_STRATEGY, + csr_strategy(), + csr_strategy(), + trans_strategy()) + ) { + let op_a_rows = if trans_a.to_bool() { a.ncols() } else { a.nrows() }; + let op_a_cols = if trans_a.to_bool() { a.nrows() } else { a.ncols() }; + + let dims_are_compatible = c.nrows() == op_a_rows && c.ncols() == op_a_cols; + + // If the dimensions randomly happen to be compatible, then of course we need to + // skip the test, so we assume that they are not. + prop_assume!(!dims_are_compatible); + + let result = catch_unwind(|| { + let mut spmm_result = c.clone(); + spadd_csr(&mut spmm_result, beta, alpha, trans_a, &a).unwrap(); + }); + + prop_assert!(result.is_err(), + "The SPMM kernel executed successfully despite mismatch dimensions"); + } } \ No newline at end of file From b25848838b141d01c16a790dad88b85c72d04d8a Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Wed, 16 Dec 2020 16:17:42 +0100 Subject: [PATCH 042/183] Implement CSR-CSR matrix multiplication --- nalgebra-sparse/src/ops/impl_std_ops.rs | 45 +++++++++++++++++++++++-- nalgebra-sparse/tests/unit_tests/ops.rs | 40 +++++++++++++++++++++- 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/nalgebra-sparse/src/ops/impl_std_ops.rs b/nalgebra-sparse/src/ops/impl_std_ops.rs index c8e9e800..17f357a6 100644 --- a/nalgebra-sparse/src/ops/impl_std_ops.rs +++ b/nalgebra-sparse/src/ops/impl_std_ops.rs @@ -1,7 +1,7 @@ use crate::csr::CsrMatrix; -use std::ops::Add; -use crate::ops::serial::{spadd_csr, spadd_build_pattern}; +use std::ops::{Add, Mul}; +use crate::ops::serial::{spadd_csr, spadd_build_pattern, spmm_pattern, spmm_csr}; use nalgebra::{ClosedAdd, ClosedMul, Scalar}; use num_traits::{Zero, One}; use std::sync::Arc; @@ -65,4 +65,43 @@ where fn add(self, rhs: CsrMatrix) -> Self::Output { self + &rhs } -} \ No newline at end of file +} + +/// Helper macro for implementing matrix multiplication for different matrix types +/// See below for usage. +macro_rules! impl_matrix_mul { + (<$($life:lifetime),*>($a_name:ident : $a:ty, $b_name:ident : $b:ty) -> $ret:ty $body:block) + => + { + impl<$($life,)* T> Mul<$b> for $a + where + T: Scalar + ClosedAdd + ClosedMul + Zero + One + { + type Output = $ret; + fn mul(self, rhs: $b) -> Self::Output { + let $a_name = self; + let $b_name = rhs; + $body + } + } + } +} + +impl_matrix_mul!(<'a>(a: &'a CsrMatrix, b: &'a CsrMatrix) -> CsrMatrix { + let pattern = spmm_pattern(a.pattern(), b.pattern()); + let values = vec![T::zero(); pattern.nnz()]; + let mut result = CsrMatrix::try_from_pattern_and_values(Arc::new(pattern), values) + .unwrap(); + spmm_csr(&mut result, + T::zero(), + T::one(), + Transpose(false), + a, + Transpose(false), + b) + .expect("Internal error: spmm failed (please debug)."); + result +}); +impl_matrix_mul!(<'a>(a: &'a CsrMatrix, b: CsrMatrix) -> CsrMatrix { a * &b}); +impl_matrix_mul!(<'a>(a: CsrMatrix, b: &'a CsrMatrix) -> CsrMatrix { &a * b}); +impl_matrix_mul!(<>(a: CsrMatrix, b: CsrMatrix) -> CsrMatrix { &a * &b}); \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index c1df496f..d5f2bba8 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -292,7 +292,7 @@ proptest! { (a, b) in csr_strategy() .prop_flat_map(|a| { - let b = csr(-5 ..= 5, Just(a.nrows()), Just(a.ncols()), 40); + let b = csr(PROPTEST_I32_VALUE_STRATEGY, Just(a.nrows()), Just(a.ncols()), 40); (Just(a), b) })) { @@ -425,4 +425,42 @@ proptest! { prop_assert!(result.is_err(), "The SPMM kernel executed successfully despite mismatch dimensions"); } + + #[test] + fn csr_mul_csr( + // a and b have dimensions compatible for multiplication + (a, b) + in csr_strategy() + .prop_flat_map(|a| { + let max_nnz = PROPTEST_MAX_NNZ; + let cols = PROPTEST_MATRIX_DIM; + let b = csr(PROPTEST_I32_VALUE_STRATEGY, Just(a.ncols()), cols, max_nnz); + (Just(a), b) + })) + { + // We use the dense result as the ground truth for the arithmetic result + let c_dense = DMatrix::from(&a) * DMatrix::from(&b); + // However, it's not enough only to cover the dense result, we also need to verify the + // sparsity pattern. We can determine the exact sparsity pattern by using + // dense arithmetic with positive integer values and extracting positive entries. + let c_dense_pattern = dense_csr_pattern(a.pattern()) * dense_csr_pattern(b.pattern()); + let c_pattern = CsrMatrix::from(&c_dense_pattern).pattern().clone(); + + // Check each combination of owned matrices and references + let c_owned_owned = a.clone() * b.clone(); + prop_assert_eq!(&DMatrix::from(&c_owned_owned), &c_dense); + prop_assert_eq!(c_owned_owned.pattern(), &c_pattern); + + let c_owned_ref = a.clone() * &b; + prop_assert_eq!(&DMatrix::from(&c_owned_ref), &c_dense); + prop_assert_eq!(c_owned_ref.pattern(), &c_pattern); + + let c_ref_owned = &a * b.clone(); + prop_assert_eq!(&DMatrix::from(&c_ref_owned), &c_dense); + prop_assert_eq!(c_ref_owned.pattern(), &c_pattern); + + let c_ref_ref = &a * &b; + prop_assert_eq!(&DMatrix::from(&c_ref_ref), &c_dense); + prop_assert_eq!(c_ref_ref.pattern(), &c_pattern); + } } \ No newline at end of file From 6a100c085a413a66e00133c74935692c3bef4b24 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Wed, 16 Dec 2020 16:50:50 +0100 Subject: [PATCH 043/183] Add proptest regressions --- .../unit_tests/convert_serial.proptest-regressions | 10 ++++++++++ .../tests/unit_tests/ops.proptest-regressions | 12 ++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 nalgebra-sparse/tests/unit_tests/convert_serial.proptest-regressions create mode 100644 nalgebra-sparse/tests/unit_tests/ops.proptest-regressions diff --git a/nalgebra-sparse/tests/unit_tests/convert_serial.proptest-regressions b/nalgebra-sparse/tests/unit_tests/convert_serial.proptest-regressions new file mode 100644 index 00000000..bfd41ead --- /dev/null +++ b/nalgebra-sparse/tests/unit_tests/convert_serial.proptest-regressions @@ -0,0 +1,10 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 07cb95127d2700ff2000157938e351ce2b43f3e6419d69b00726abfc03e682bd # shrinks to coo = CooMatrix { nrows: 4, ncols: 5, row_indices: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0], col_indices: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 4, 3], values: [1, -5, -4, -5, 1, 2, 4, -4, -4, -5, 2, -2, 4, -4] } +cc 8fdaf70d6091d89a6617573547745e9802bb9c1ce7c6ec7ad4f301cd05d54c5d # shrinks to dense = Matrix { data: VecStorage { data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1], nrows: Dynamic { value: 4 }, ncols: Dynamic { value: 5 } } } +cc 6961760ac7915b57a28230524cea7e9bfcea4f31790e3c0569ea74af904c2d79 # shrinks to coo = CooMatrix { nrows: 6, ncols: 6, row_indices: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0], col_indices: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0], values: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0] } +cc c9a1af218f7a974f1fda7b8909c2635d735eedbfe953082ef6b0b92702bf6d1b # shrinks to dense = Matrix { data: VecStorage { data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], nrows: Dynamic { value: 6 }, ncols: Dynamic { value: 5 } } } diff --git a/nalgebra-sparse/tests/unit_tests/ops.proptest-regressions b/nalgebra-sparse/tests/unit_tests/ops.proptest-regressions new file mode 100644 index 00000000..4ea85f39 --- /dev/null +++ b/nalgebra-sparse/tests/unit_tests/ops.proptest-regressions @@ -0,0 +1,12 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 6748ea4ac9523fcc4dd8327b27c6818f8df10eb2042774f59a6e3fa3205dbcbd # shrinks to (beta, alpha, (c, a, b)) = (0, -1, (Matrix { data: VecStorage { data: [0, 0, 0, 0, 0, 1, 5, -4, 2], nrows: Dynamic { value: 3 }, ncols: Dynamic { value: 3 } } }, CsrMatrix { sparsity_pattern: SparsityPattern { major_offsets: [0, 2, 2, 2], minor_indices: [0, 1], minor_dim: 5 }, values: [-5, -2] }, Matrix { data: VecStorage { data: [4, -2, -3, -3, -5, 3, 5, 1, -4, -4, 3, 5, 5, 5, -3], nrows: Dynamic { value: 5 }, ncols: Dynamic { value: 3 } } })) +cc dcf67ab7b8febf109cfa58ee0f082b9f7c23d6ad0df2e28dc99984deeb6b113a # shrinks to (beta, alpha, (c, a, b)) = (0, 0, (Matrix { data: VecStorage { data: [0, -1], nrows: Dynamic { value: 1 }, ncols: Dynamic { value: 2 } } }, CsrMatrix { sparsity_pattern: SparsityPattern { major_offsets: [0, 0], minor_indices: [], minor_dim: 4 }, values: [] }, Matrix { data: VecStorage { data: [3, 1, 1, 0, 0, 3, -5, -3], nrows: Dynamic { value: 4 }, ncols: Dynamic { value: 2 } } })) +cc dbaef9886eaad28be7cd48326b857f039d695bc0b19e9ada3304e812e984d2c3 # shrinks to (beta, alpha, (c, a, b)) = (0, -1, (Matrix { data: VecStorage { data: [1], nrows: Dynamic { value: 1 }, ncols: Dynamic { value: 1 } } }, CsrMatrix { sparsity_pattern: SparsityPattern { major_offsets: [0, 0], minor_indices: [], minor_dim: 0 }, values: [] }, Matrix { data: VecStorage { data: [], nrows: Dynamic { value: 0 }, ncols: Dynamic { value: 1 } } })) +cc 99e312beb498ffa79194f41501ea312dce1911878eba131282904ac97205aaa9 # shrinks to SpmmCsrDenseArgs { c, beta, alpha, trans_a, a, trans_b, b } = SpmmCsrDenseArgs { c: Matrix { data: VecStorage { data: [-1, 4, -1, -4, 2, 1, 4, -2, 1, 3, -2, 5], nrows: Dynamic { value: 2 }, ncols: Dynamic { value: 6 } } }, beta: 0, alpha: 0, trans_a: Transpose, a: CsrMatrix { sparsity_pattern: SparsityPattern { major_offsets: [0, 1, 1, 1, 1, 1, 1], minor_indices: [0], minor_dim: 2 }, values: [0] }, trans_b: Transpose, b: Matrix { data: VecStorage { data: [-1, 1, 0, -5, 4, -5, 2, 2, 4, -4, -3, -1, 1, -1, 0, 1, -3, 4, -5, 0, 1, -5, 0, 1, 1, -3, 5, 3, 5, -3, -5, 3, -1, -4, -4, -3], nrows: Dynamic { value: 6 }, ncols: Dynamic { value: 6 } } } } +cc bf74259df2db6eda24eb42098e57ea1c604bb67d6d0023fa308c321027b53a43 # shrinks to (alpha, beta, c, a, b, trans_a, trans_b) = (0, 0, Matrix { data: VecStorage { data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], nrows: Dynamic { value: 4 }, ncols: Dynamic { value: 5 } } }, CsrMatrix { sparsity_pattern: SparsityPattern { major_offsets: [0, 3, 6, 9, 12], minor_indices: [0, 1, 3, 1, 2, 3, 0, 1, 2, 1, 2, 3], minor_dim: 4 }, values: [-3, 3, -3, 1, -3, 0, 2, 1, 3, 0, -4, -1] }, Matrix { data: VecStorage { data: [3, 1, 4, -5, 5, -2, -5, -1, 1, -1, 3, -3, -2, 4, 2, -1, -1, 3, -5, 5], nrows: Dynamic { value: 4 }, ncols: Dynamic { value: 5 } } }, NoTranspose, NoTranspose) +cc cbd6dac45a2f610e10cf4c15d4614cdbf7dfedbfcd733e4cc65c2e79829d14b3 # shrinks to SpmmCsrArgs { c, beta, alpha, trans_a, a, trans_b, b } = SpmmCsrArgs { c: CsrMatrix { sparsity_pattern: SparsityPattern { major_offsets: [0, 0, 1, 1, 1, 1], minor_indices: [0], minor_dim: 1 }, values: [0] }, beta: 0, alpha: 1, trans_a: Transpose(true), a: CsrMatrix { sparsity_pattern: SparsityPattern { major_offsets: [0, 0, 0, 1, 1, 1], minor_indices: [1], minor_dim: 5 }, values: [-1] }, trans_b: Transpose(true), b: CsrMatrix { sparsity_pattern: SparsityPattern { major_offsets: [0, 2], minor_indices: [2, 4], minor_dim: 5 }, values: [-1, 0] } } From c6a8fcdee2b71dce1768728eb7974ff7a6633597 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Wed, 16 Dec 2020 17:30:48 +0100 Subject: [PATCH 044/183] Simplify spadd_pattern API and name --- nalgebra-sparse/src/ops/impl_std_ops.rs | 6 ++---- nalgebra-sparse/src/ops/serial/pattern.rs | 24 ++++++++--------------- nalgebra-sparse/tests/unit_tests/ops.rs | 12 +++++------- 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/nalgebra-sparse/src/ops/impl_std_ops.rs b/nalgebra-sparse/src/ops/impl_std_ops.rs index 17f357a6..34a7bcf5 100644 --- a/nalgebra-sparse/src/ops/impl_std_ops.rs +++ b/nalgebra-sparse/src/ops/impl_std_ops.rs @@ -1,12 +1,11 @@ use crate::csr::CsrMatrix; use std::ops::{Add, Mul}; -use crate::ops::serial::{spadd_csr, spadd_build_pattern, spmm_pattern, spmm_csr}; +use crate::ops::serial::{spadd_csr, spadd_pattern, spmm_pattern, spmm_csr}; use nalgebra::{ClosedAdd, ClosedMul, Scalar}; use num_traits::{Zero, One}; use std::sync::Arc; use crate::ops::Transpose; -use crate::pattern::SparsityPattern; impl<'a, T> Add<&'a CsrMatrix> for &'a CsrMatrix where @@ -17,8 +16,7 @@ where type Output = CsrMatrix; fn add(self, rhs: &'a CsrMatrix) -> Self::Output { - let mut pattern = SparsityPattern::new(self.nrows(), self.ncols()); - spadd_build_pattern(&mut pattern, self.pattern(), rhs.pattern()); + let pattern = spadd_pattern(self.pattern(), rhs.pattern()); let values = vec![T::zero(); pattern.nnz()]; // We are giving data that is valid by definition, so it is safe to unwrap below let mut result = CsrMatrix::try_from_pattern_and_values(Arc::new(pattern), values) diff --git a/nalgebra-sparse/src/ops/serial/pattern.rs b/nalgebra-sparse/src/ops/serial/pattern.rs index 2e442cc0..39b8a1c1 100644 --- a/nalgebra-sparse/src/ops/serial/pattern.rs +++ b/nalgebra-sparse/src/ops/serial/pattern.rs @@ -1,6 +1,5 @@ use crate::pattern::SparsityPattern; -use std::mem::swap; use std::iter; /// Sparse matrix addition pattern construction, `C <- A + B`. @@ -9,21 +8,15 @@ use std::iter; /// The patterns are assumed to have the same major and minor dimensions. In other words, /// both patterns `A` and `B` must both stem from the same kind of compressed matrix: /// CSR or CSC. -/// TODO: Explain that output pattern is only used to avoid allocations -pub fn spadd_build_pattern(pattern: &mut SparsityPattern, - a: &SparsityPattern, - b: &SparsityPattern) +pub fn spadd_pattern(a: &SparsityPattern, + b: &SparsityPattern) -> SparsityPattern { // TODO: Proper error messages - assert_eq!(a.major_dim(), b.major_dim()); - assert_eq!(a.minor_dim(), b.minor_dim()); + assert_eq!(a.major_dim(), b.major_dim(), "Patterns must have identical major dimensions."); + assert_eq!(a.minor_dim(), b.minor_dim(), "Patterns must have identical minor dimensions."); - let input_pattern = pattern; - let mut temp_pattern = SparsityPattern::new(a.major_dim(), b.minor_dim()); - swap(input_pattern, &mut temp_pattern); - let (mut offsets, mut indices) = temp_pattern.disassemble(); - - offsets.clear(); + let mut offsets = Vec::new(); + let mut indices = Vec::new(); offsets.reserve(a.major_dim() + 1); indices.clear(); @@ -37,10 +30,9 @@ pub fn spadd_build_pattern(pattern: &mut SparsityPattern, } // TODO: Consider circumventing format checks? (requires unsafe, should benchmark first) - let mut new_pattern = SparsityPattern::try_from_offsets_and_indices( + SparsityPattern::try_from_offsets_and_indices( a.major_dim(), a.minor_dim(), offsets, indices) - .expect("Pattern must be valid by definition"); - swap(input_pattern, &mut new_pattern); + .expect("Internal error: Pattern must be valid by definition") } /// Sparse matrix multiplication pattern construction, `C <- A * B`. diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index d5f2bba8..16482171 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -1,6 +1,6 @@ use crate::common::{csr_strategy, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ, PROPTEST_I32_VALUE_STRATEGY}; -use nalgebra_sparse::ops::serial::{spmm_csr_dense, spadd_build_pattern, spmm_pattern, spadd_csr, spmm_csr}; +use nalgebra_sparse::ops::serial::{spmm_csr_dense, spadd_pattern, spmm_pattern, spadd_csr, spmm_csr}; use nalgebra_sparse::ops::{Transpose}; use nalgebra_sparse::csr::CsrMatrix; use nalgebra_sparse::proptest::{csr, sparsity_pattern}; @@ -88,8 +88,7 @@ fn spadd_csr_args_strategy() -> impl Strategy> { spadd_build_pattern_strategy() .prop_flat_map(move |(a_pattern, b_pattern)| { - let mut c_pattern = SparsityPattern::new(a_pattern.major_dim(), b_pattern.major_dim()); - spadd_build_pattern(&mut c_pattern, &a_pattern, &b_pattern); + let c_pattern = spadd_pattern(&a_pattern, &b_pattern); let a_values = vec![value_strategy.clone(); a_pattern.nnz()]; let c_values = vec![value_strategy.clone(); c_pattern.nnz()]; @@ -248,11 +247,10 @@ proptest! { } #[test] - fn spadd_build_pattern_test((c, (a, b)) in (pattern_strategy(), spadd_build_pattern_strategy())) + fn spadd_pattern_test((a, b) in spadd_build_pattern_strategy()) { - // (a, b) are dimensionally compatible patterns, whereas c is an *arbitrary* pattern - let mut pattern_result = c.clone(); - spadd_build_pattern(&mut pattern_result, &a, &b); + // (a, b) are dimensionally compatible patterns + let pattern_result = spadd_pattern(&a, &b); // To verify the pattern, we construct CSR matrices with positive integer entries // corresponding to a and b, and convert them to dense matrices. From fe8592fde1752da91cdabb410c85ade51153ab10 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 21 Dec 2020 15:09:29 +0100 Subject: [PATCH 045/183] Refactor ops to use new Op type instead of separate Transpose flag --- nalgebra-sparse/src/ops/impl_std_ops.rs | 14 +- nalgebra-sparse/src/ops/mod.rs | 52 ++++++- nalgebra-sparse/src/ops/serial/csr.rs | 186 ++++++++++++----------- nalgebra-sparse/src/ops/serial/mod.rs | 65 ++++---- nalgebra-sparse/tests/unit_tests/ops.rs | 193 ++++++++++++++---------- 5 files changed, 299 insertions(+), 211 deletions(-) diff --git a/nalgebra-sparse/src/ops/impl_std_ops.rs b/nalgebra-sparse/src/ops/impl_std_ops.rs index 34a7bcf5..c181464d 100644 --- a/nalgebra-sparse/src/ops/impl_std_ops.rs +++ b/nalgebra-sparse/src/ops/impl_std_ops.rs @@ -5,7 +5,7 @@ use crate::ops::serial::{spadd_csr, spadd_pattern, spmm_pattern, spmm_csr}; use nalgebra::{ClosedAdd, ClosedMul, Scalar}; use num_traits::{Zero, One}; use std::sync::Arc; -use crate::ops::Transpose; +use crate::ops::{Op}; impl<'a, T> Add<&'a CsrMatrix> for &'a CsrMatrix where @@ -21,8 +21,8 @@ where // We are giving data that is valid by definition, so it is safe to unwrap below let mut result = CsrMatrix::try_from_pattern_and_values(Arc::new(pattern), values) .unwrap(); - spadd_csr(&mut result, T::zero(), T::one(), Transpose(false), &self).unwrap(); - spadd_csr(&mut result, T::one(), T::one(), Transpose(false), &rhs).unwrap(); + spadd_csr(&mut result, T::zero(), T::one(), Op::NoOp(&self)).unwrap(); + spadd_csr(&mut result, T::one(), T::one(), Op::NoOp(&rhs)).unwrap(); result } } @@ -35,7 +35,7 @@ where fn add(mut self, rhs: &'a CsrMatrix) -> Self::Output { if Arc::ptr_eq(self.pattern(), rhs.pattern()) { - spadd_csr(&mut self, T::one(), T::one(), Transpose(false), &rhs).unwrap(); + spadd_csr(&mut self, T::one(), T::one(), Op::NoOp(rhs)).unwrap(); self } else { &self + rhs @@ -93,10 +93,8 @@ impl_matrix_mul!(<'a>(a: &'a CsrMatrix, b: &'a CsrMatrix) -> CsrMatrix spmm_csr(&mut result, T::zero(), T::one(), - Transpose(false), - a, - Transpose(false), - b) + Op::NoOp(a), + Op::NoOp(b)) .expect("Internal error: spmm failed (please debug)."); result }); diff --git a/nalgebra-sparse/src/ops/mod.rs b/nalgebra-sparse/src/ops/mod.rs index 08939d8a..14a18dc1 100644 --- a/nalgebra-sparse/src/ops/mod.rs +++ b/nalgebra-sparse/src/ops/mod.rs @@ -4,14 +4,54 @@ mod impl_std_ops; pub mod serial; /// TODO -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct Transpose(pub bool); - -impl Transpose { +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Op { /// TODO - pub fn to_bool(&self) -> bool { - self.0 + NoOp(T), + /// TODO + Transpose(T), +} + +impl Op { + /// TODO + pub fn inner_ref(&self) -> &T { + match self { + Op::NoOp(obj) => &obj, + Op::Transpose(obj) => &obj + } + } + + /// TODO + pub fn as_ref(&self) -> Op<&T> { + match self { + Op::NoOp(obj) => Op::NoOp(&obj), + Op::Transpose(obj) => Op::Transpose(&obj) + } + } + + /// TODO + pub fn convert(self) -> Op + where T: Into + { + match self { + Op::NoOp(obj) => Op::NoOp(obj.into()), + Op::Transpose(obj) => Op::Transpose(obj.into()) + } + } + + /// TODO + /// TODO: Rewrite the other functions by leveraging this one + pub fn map_same_op U>(self, f: F) -> Op { + match self { + Op::NoOp(obj) => Op::NoOp(f(obj)), + Op::Transpose(obj) => Op::Transpose(f(obj)) + } } } +impl From for Op { + fn from(obj: T) -> Self { + Self::NoOp(obj) + } +} diff --git a/nalgebra-sparse/src/ops/serial/csr.rs b/nalgebra-sparse/src/ops/serial/csr.rs index 670711cd..e1b5a1c5 100644 --- a/nalgebra-sparse/src/ops/serial/csr.rs +++ b/nalgebra-sparse/src/ops/serial/csr.rs @@ -1,5 +1,5 @@ use crate::csr::CsrMatrix; -use crate::ops::{Transpose}; +use crate::ops::{Op}; use crate::SparseEntryMut; use crate::ops::serial::{OperationError, OperationErrorType}; use nalgebra::{Scalar, DMatrixSlice, ClosedAdd, ClosedMul, DMatrixSliceMut}; @@ -7,65 +7,71 @@ use num_traits::{Zero, One}; use std::sync::Arc; use std::borrow::Cow; -/// Sparse-dense matrix-matrix multiplication `C <- beta * C + alpha * trans(A) * trans(B)`. +/// Sparse-dense matrix-matrix multiplication `C <- beta * C + alpha * op(A) * op(B)`. pub fn spmm_csr_dense<'a, T>(c: impl Into>, beta: T, alpha: T, - trans_a: Transpose, - a: &CsrMatrix, - trans_b: Transpose, - b: impl Into>) + a: Op<&CsrMatrix>, + b: Op>>) where T: Scalar + ClosedAdd + ClosedMul + Zero + One { - spmm_csr_dense_(c.into(), beta, alpha, trans_a, a, trans_b, b.into()) + let b = b.convert(); + spmm_csr_dense_(c.into(), beta, alpha, a, b) } fn spmm_csr_dense_(mut c: DMatrixSliceMut, beta: T, alpha: T, - trans_a: Transpose, - a: &CsrMatrix, - trans_b: Transpose, - b: DMatrixSlice) + a: Op<&CsrMatrix>, + b: Op>) where T: Scalar + ClosedAdd + ClosedMul + Zero + One { - assert_compatible_spmm_dims!(c, a, b, trans_a, trans_b); + assert_compatible_spmm_dims!(c, a, b); - if trans_a.to_bool() { - // In this case, we have to pre-multiply C by beta - c *= beta; + match a { + Op::Transpose(ref a) => { + // In this case, we have to pre-multiply C by beta + c *= beta; - for k in 0..a.nrows() { - let a_row_k = a.row(k); - for (&i, a_ki) in a_row_k.col_indices().iter().zip(a_row_k.values()) { - let gamma_ki = alpha.inlined_clone() * a_ki.inlined_clone(); - let mut c_row_i = c.row_mut(i); - if trans_b.to_bool() { - let b_col_k = b.column(k); - for (c_ij, b_jk) in c_row_i.iter_mut().zip(b_col_k.iter()) { - *c_ij += gamma_ki.inlined_clone() * b_jk.inlined_clone(); - } - } else { - let b_row_k = b.row(k); - for (c_ij, b_kj) in c_row_i.iter_mut().zip(b_row_k.iter()) { - *c_ij += gamma_ki.inlined_clone() * b_kj.inlined_clone(); + for k in 0..a.nrows() { + let a_row_k = a.row(k); + for (&i, a_ki) in a_row_k.col_indices().iter().zip(a_row_k.values()) { + let gamma_ki = alpha.inlined_clone() * a_ki.inlined_clone(); + let mut c_row_i = c.row_mut(i); + match b { + Op::NoOp(ref b) => { + let b_row_k = b.row(k); + for (c_ij, b_kj) in c_row_i.iter_mut().zip(b_row_k.iter()) { + *c_ij += gamma_ki.inlined_clone() * b_kj.inlined_clone(); + } + }, + Op::Transpose(ref b) => { + let b_col_k = b.column(k); + for (c_ij, b_jk) in c_row_i.iter_mut().zip(b_col_k.iter()) { + *c_ij += gamma_ki.inlined_clone() * b_jk.inlined_clone(); + } + }, } } } - } - } else { - for j in 0..c.ncols() { - let mut c_col_j = c.column_mut(j); - for (c_ij, a_row_i) in c_col_j.iter_mut().zip(a.row_iter()) { - let mut dot_ij = T::zero(); - for (&k, a_ik) in a_row_i.col_indices().iter().zip(a_row_i.values()) { - let b_contrib = - if trans_b.to_bool() { b.index((j, k)) } else { b.index((k, j)) }; - dot_ij += a_ik.inlined_clone() * b_contrib.inlined_clone(); + }, + Op::NoOp(ref a) => { + for j in 0..c.ncols() { + let mut c_col_j = c.column_mut(j); + for (c_ij, a_row_i) in c_col_j.iter_mut().zip(a.row_iter()) { + let mut dot_ij = T::zero(); + for (&k, a_ik) in a_row_i.col_indices().iter().zip(a_row_i.values()) { + let b_contrib = + match b { + Op::NoOp(ref b) => b.index((k, j)), + Op::Transpose(ref b) => b.index((j, k)) + }; + dot_ij += a_ik.inlined_clone() * b_contrib.inlined_clone(); + } + *c_ij = beta.inlined_clone() * c_ij.inlined_clone() + alpha.inlined_clone() * dot_ij; } - *c_ij = beta.inlined_clone() * c_ij.inlined_clone() + alpha.inlined_clone() * dot_ij; } } } @@ -77,32 +83,31 @@ fn spadd_csr_unexpected_entry() -> OperationError { String::from("Found entry in `a` that is not present in `c`.")) } -/// Sparse matrix addition `C <- beta * C + alpha * trans(A)`. +/// Sparse matrix addition `C <- beta * C + alpha * op(A)`. /// /// If the pattern of `c` does not accommodate all the non-zero entries in `a`, an error is /// returned. pub fn spadd_csr(c: &mut CsrMatrix, beta: T, alpha: T, - trans_a: Transpose, - a: &CsrMatrix) + a: Op<&CsrMatrix>) -> Result<(), OperationError> where T: Scalar + ClosedAdd + ClosedMul + Zero + One { - assert_compatible_spadd_dims!(c, a, trans_a); + assert_compatible_spadd_dims!(c, a); // TODO: Change CsrMatrix::pattern() to return `&Arc` instead of `Arc` - if Arc::ptr_eq(&c.pattern(), &a.pattern()) { + if Arc::ptr_eq(&c.pattern(), &a.inner_ref().pattern()) { // Special fast path: The two matrices have *exactly* the same sparsity pattern, // so we only need to sum the value arrays - for (c_ij, a_ij) in c.values_mut().iter_mut().zip(a.values()) { + for (c_ij, a_ij) in c.values_mut().iter_mut().zip(a.inner_ref().values()) { let (alpha, beta) = (alpha.inlined_clone(), beta.inlined_clone()); *c_ij = beta * c_ij.inlined_clone() + alpha * a_ij.inlined_clone(); } Ok(()) } else { - if trans_a.to_bool() + if let Op::Transpose(a) = a { if beta != T::one() { for c_ij in c.values_mut() { @@ -120,7 +125,7 @@ where } } } - } else { + } else if let Op::NoOp(a) = a { for (mut c_row_i, a_row_i) in c.row_iter_mut().zip(a.row_iter()) { if beta != T::one() { for c_ij in c_row_i.values_mut() { @@ -160,56 +165,61 @@ pub fn spmm_csr<'a, T>( c: &mut CsrMatrix, beta: T, alpha: T, - trans_a: Transpose, - a: &CsrMatrix, - trans_b: Transpose, - b: &CsrMatrix) + a: Op<&CsrMatrix>, + b: Op<&CsrMatrix>) -> Result<(), OperationError> where T: Scalar + ClosedAdd + ClosedMul + Zero + One { - assert_compatible_spmm_dims!(c, a, b, trans_a, trans_b); + assert_compatible_spmm_dims!(c, a, b); - if !trans_a.to_bool() && !trans_b.to_bool() { - for (mut c_row_i, a_row_i) in c.row_iter_mut().zip(a.row_iter()) { - for c_ij in c_row_i.values_mut() { - *c_ij = beta.inlined_clone() * c_ij.inlined_clone(); - } + use Op::{NoOp, Transpose}; - for (&k, a_ik) in a_row_i.col_indices().iter().zip(a_row_i.values()) { - let b_row_k = b.row(k); - let (mut c_row_i_cols, mut c_row_i_values) = c_row_i.cols_and_values_mut(); - let alpha_aik = alpha.inlined_clone() * a_ik.inlined_clone(); - for (j, b_kj) in b_row_k.col_indices().iter().zip(b_row_k.values()) { - // Determine the location in C to append the value - let (c_local_idx, _) = c_row_i_cols.iter() - .enumerate() - .find(|(_, c_col)| *c_col == j) - .ok_or_else(spmm_csr_unexpected_entry)?; + match (&a, &b) { + (NoOp(ref a), NoOp(ref b)) => { + for (mut c_row_i, a_row_i) in c.row_iter_mut().zip(a.row_iter()) { + for c_ij in c_row_i.values_mut() { + *c_ij = beta.inlined_clone() * c_ij.inlined_clone(); + } - c_row_i_values[c_local_idx] += alpha_aik.inlined_clone() * b_kj.inlined_clone(); - c_row_i_cols = &c_row_i_cols[c_local_idx ..]; - c_row_i_values = &mut c_row_i_values[c_local_idx ..]; + for (&k, a_ik) in a_row_i.col_indices().iter().zip(a_row_i.values()) { + let b_row_k = b.row(k); + let (mut c_row_i_cols, mut c_row_i_values) = c_row_i.cols_and_values_mut(); + let alpha_aik = alpha.inlined_clone() * a_ik.inlined_clone(); + for (j, b_kj) in b_row_k.col_indices().iter().zip(b_row_k.values()) { + // Determine the location in C to append the value + let (c_local_idx, _) = c_row_i_cols.iter() + .enumerate() + .find(|(_, c_col)| *c_col == j) + .ok_or_else(spmm_csr_unexpected_entry)?; + + c_row_i_values[c_local_idx] += alpha_aik.inlined_clone() * b_kj.inlined_clone(); + c_row_i_cols = &c_row_i_cols[c_local_idx ..]; + c_row_i_values = &mut c_row_i_values[c_local_idx ..]; + } } } - } - Ok(()) - } else { - // Currently we handle transposition by explicitly precomputing transposed matrices - // and calling the operation again without transposition - // TODO: At least use workspaces to allow control of allocations. Maybe - // consider implementing certain patterns (like A^T * B) explicitly - let (a, b) = { - use Cow::*; - match (trans_a, trans_b) { - (Transpose(false), Transpose(false)) => unreachable!(), - (Transpose(true), Transpose(false)) => (Owned(a.transpose()), Borrowed(b)), - (Transpose(false), Transpose(true)) => (Borrowed(a), Owned(b.transpose())), - (Transpose(true), Transpose(true)) => (Owned(a.transpose()), Owned(b.transpose())) - } - }; + Ok(()) + }, + _ => { + // Currently we handle transposition by explicitly precomputing transposed matrices + // and calling the operation again without transposition + // TODO: At least use workspaces to allow control of allocations. Maybe + // consider implementing certain patterns (like A^T * B) explicitly + let a_ref: &CsrMatrix = a.inner_ref(); + let b_ref: &CsrMatrix = b.inner_ref(); + let (a, b) = { + use Cow::*; + match (&a, &b) { + (NoOp(_), NoOp(_)) => unreachable!(), + (Transpose(ref a), NoOp(_)) => (Owned(a.transpose()), Borrowed(b_ref)), + (NoOp(_), Transpose(ref b)) => (Borrowed(a_ref), Owned(b.transpose())), + (Transpose(ref a), Transpose(ref b)) => (Owned(a.transpose()), Owned(b.transpose())) + } + }; - spmm_csr(c, beta, alpha, Transpose(false), a.as_ref(), Transpose(false), b.as_ref()) + spmm_csr(c, beta, alpha, NoOp(a.as_ref()), NoOp(b.as_ref())) + } } } diff --git a/nalgebra-sparse/src/ops/serial/mod.rs b/nalgebra-sparse/src/ops/serial/mod.rs index 8ac22ac8..a58ba9a3 100644 --- a/nalgebra-sparse/src/ops/serial/mod.rs +++ b/nalgebra-sparse/src/ops/serial/mod.rs @@ -2,46 +2,47 @@ #[macro_use] macro_rules! assert_compatible_spmm_dims { - ($c:expr, $a:expr, $b:expr, $trans_a:expr, $trans_b:expr) => { - use crate::ops::Transpose; - match ($trans_a, $trans_b) { - (Transpose(false), Transpose(false)) => { - assert_eq!($c.nrows(), $a.nrows(), "C.nrows() != A.nrows()"); - assert_eq!($c.ncols(), $b.ncols(), "C.ncols() != B.ncols()"); - assert_eq!($a.ncols(), $b.nrows(), "A.ncols() != B.nrows()"); - }, - (Transpose(true), Transpose(false)) => { - assert_eq!($c.nrows(), $a.ncols(), "C.nrows() != A.ncols()"); - assert_eq!($c.ncols(), $b.ncols(), "C.ncols() != B.ncols()"); - assert_eq!($a.nrows(), $b.nrows(), "A.nrows() != B.nrows()"); - }, - (Transpose(false), Transpose(true)) => { - assert_eq!($c.nrows(), $a.nrows(), "C.nrows() != A.nrows()"); - assert_eq!($c.ncols(), $b.nrows(), "C.ncols() != B.nrows()"); - assert_eq!($a.ncols(), $b.ncols(), "A.ncols() != B.ncols()"); - }, - (Transpose(true), Transpose(true)) => { - assert_eq!($c.nrows(), $a.ncols(), "C.nrows() != A.ncols()"); - assert_eq!($c.ncols(), $b.nrows(), "C.ncols() != B.nrows()"); - assert_eq!($a.nrows(), $b.ncols(), "A.nrows() != B.ncols()"); + ($c:expr, $a:expr, $b:expr) => { + { + use crate::ops::Op::{NoOp, Transpose}; + match (&$a, &$b) { + (NoOp(ref a), NoOp(ref b)) => { + assert_eq!($c.nrows(), a.nrows(), "C.nrows() != A.nrows()"); + assert_eq!($c.ncols(), b.ncols(), "C.ncols() != B.ncols()"); + assert_eq!(a.ncols(), b.nrows(), "A.ncols() != B.nrows()"); + }, + (Transpose(ref a), NoOp(ref b)) => { + assert_eq!($c.nrows(), a.ncols(), "C.nrows() != A.ncols()"); + assert_eq!($c.ncols(), b.ncols(), "C.ncols() != B.ncols()"); + assert_eq!(a.nrows(), b.nrows(), "A.nrows() != B.nrows()"); + }, + (NoOp(ref a), Transpose(ref b)) => { + assert_eq!($c.nrows(), a.nrows(), "C.nrows() != A.nrows()"); + assert_eq!($c.ncols(), b.nrows(), "C.ncols() != B.nrows()"); + assert_eq!(a.ncols(), b.ncols(), "A.ncols() != B.ncols()"); + }, + (Transpose(ref a), Transpose(ref b)) => { + assert_eq!($c.nrows(), a.ncols(), "C.nrows() != A.ncols()"); + assert_eq!($c.ncols(), b.nrows(), "C.ncols() != B.nrows()"); + assert_eq!(a.nrows(), b.ncols(), "A.nrows() != B.ncols()"); + } } } - } } #[macro_use] macro_rules! assert_compatible_spadd_dims { - ($c:expr, $a:expr, $trans_a:expr) => { - use crate::ops::Transpose; - match $trans_a { - Transpose(false) => { - assert_eq!($c.nrows(), $a.nrows(), "C.nrows() != A.nrows()"); - assert_eq!($c.ncols(), $a.ncols(), "C.ncols() != A.ncols()"); + ($c:expr, $a:expr) => { + use crate::ops::Op; + match $a { + Op::NoOp(a) => { + assert_eq!($c.nrows(), a.nrows(), "C.nrows() != A.nrows()"); + assert_eq!($c.ncols(), a.ncols(), "C.ncols() != A.ncols()"); }, - Transpose(true) => { - assert_eq!($c.nrows(), $a.ncols(), "C.nrows() != A.ncols()"); - assert_eq!($c.ncols(), $a.nrows(), "C.ncols() != A.nrows()"); + Op::Transpose(a) => { + assert_eq!($c.nrows(), a.ncols(), "C.nrows() != A.ncols()"); + assert_eq!($c.ncols(), a.nrows(), "C.ncols() != A.nrows()"); } } diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index 16482171..55953491 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -1,7 +1,7 @@ use crate::common::{csr_strategy, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ, PROPTEST_I32_VALUE_STRATEGY}; use nalgebra_sparse::ops::serial::{spmm_csr_dense, spadd_pattern, spmm_pattern, spadd_csr, spmm_csr}; -use nalgebra_sparse::ops::{Transpose}; +use nalgebra_sparse::ops::{Op}; use nalgebra_sparse::csr::CsrMatrix; use nalgebra_sparse::proptest::{csr, sparsity_pattern}; use nalgebra_sparse::pattern::SparsityPattern; @@ -28,10 +28,8 @@ struct SpmmCsrDenseArgs { c: DMatrix, beta: T, alpha: T, - trans_a: Transpose, - a: CsrMatrix, - trans_b: Transpose, - b: DMatrix, + a: Op>, + b: Op>, } /// Returns matrices C, A and B with compatible dimensions such that it can be used @@ -48,10 +46,10 @@ fn spmm_csr_dense_args_strategy() -> impl Strategy> (c_matrix_strategy, common_dim, trans_strategy.clone(), trans_strategy.clone()) .prop_flat_map(move |(c, common_dim, trans_a, trans_b)| { let a_shape = - if trans_a.to_bool() { (common_dim, c.nrows()) } + if trans_a { (common_dim, c.nrows()) } else { (c.nrows(), common_dim) }; let b_shape = - if trans_b.to_bool() { (c.ncols(), common_dim) } + if trans_b { (c.ncols(), common_dim) } else { (common_dim, c.ncols()) }; let a = csr(value_strategy.clone(), Just(a_shape.0), Just(a_shape.1), max_nnz); let b = matrix(value_strategy.clone(), b_shape.0, b_shape.1); @@ -66,10 +64,8 @@ fn spmm_csr_dense_args_strategy() -> impl Strategy> c, beta, alpha, - trans_a, - a, - trans_b, - b, + a: if trans_a { Op::Transpose(a) } else { Op::NoOp(a) }, + b: if trans_b { Op::Transpose(b) } else { Op::NoOp(b) }, } }) } @@ -79,14 +75,13 @@ struct SpaddCsrArgs { c: CsrMatrix, beta: T, alpha: T, - trans_a: Transpose, - a: CsrMatrix, + a: Op>, } fn spadd_csr_args_strategy() -> impl Strategy> { let value_strategy = PROPTEST_I32_VALUE_STRATEGY; - spadd_build_pattern_strategy() + spadd_pattern_strategy() .prop_flat_map(move |(a_pattern, b_pattern)| { let c_pattern = spadd_pattern(&a_pattern, &b_pattern); @@ -99,8 +94,8 @@ fn spadd_csr_args_strategy() -> impl Strategy> { let c = CsrMatrix::try_from_pattern_and_values(Arc::new(c_pattern), c_values).unwrap(); let a = CsrMatrix::try_from_pattern_and_values(Arc::new(a_pattern), a_values).unwrap(); - let a = if trans_a.to_bool() { a.transpose() } else { a }; - SpaddCsrArgs { c, beta, alpha, trans_a, a } + let a = if trans_a { Op::Transpose(a.transpose()) } else { Op::NoOp(a) }; + SpaddCsrArgs { c, beta, alpha, a } }) } @@ -108,8 +103,20 @@ fn dense_strategy() -> impl Strategy> { matrix(PROPTEST_I32_VALUE_STRATEGY, PROPTEST_MATRIX_DIM, PROPTEST_MATRIX_DIM) } -fn trans_strategy() -> impl Strategy + Clone { - proptest::bool::ANY.prop_map(Transpose) +fn trans_strategy() -> impl Strategy + Clone { + proptest::bool::ANY +} + +/// Wraps the values of the given strategy in `Op`, producing both transposed and non-transposed +/// values. +fn op_strategy(strategy: S) -> impl Strategy> { + let is_transposed = proptest::bool::ANY; + (strategy, is_transposed) + .prop_map(|(obj, is_trans)| if is_trans { + Op::Transpose(obj) + } else { + Op::NoOp(obj) + }) } fn pattern_strategy() -> impl Strategy { @@ -117,7 +124,7 @@ fn pattern_strategy() -> impl Strategy { } /// Constructs pairs (a, b) where a and b have the same dimensions -fn spadd_build_pattern_strategy() -> impl Strategy { +fn spadd_pattern_strategy() -> impl Strategy { pattern_strategy() .prop_flat_map(|a| { let b = sparsity_pattern(Just(a.major_dim()), Just(a.minor_dim()), PROPTEST_MAX_NNZ); @@ -139,10 +146,8 @@ struct SpmmCsrArgs { c: CsrMatrix, beta: T, alpha: T, - trans_a: Transpose, - a: CsrMatrix, - trans_b: Transpose, - b: CsrMatrix + a: Op>, + b: Op>, } fn spmm_csr_args_strategy() -> impl Strategy> { @@ -170,10 +175,8 @@ fn spmm_csr_args_strategy() -> impl Strategy> { c, beta, alpha, - trans_a, - a: if trans_a.to_bool() { a.transpose() } else { a }, - trans_b, - b: if trans_b.to_bool() { b.transpose() } else { b } + a: if trans_a { Op::Transpose(a.transpose()) } else { Op::NoOp(a) }, + b: if trans_b { Op::Transpose(b.transpose()) } else { Op::NoOp(b) } } }) } @@ -182,52 +185,67 @@ fn spmm_csr_args_strategy() -> impl Strategy> { fn dense_gemm<'a>(c: impl Into>, beta: i32, alpha: i32, - trans_a: Transpose, - a: impl Into>, - trans_b: Transpose, - b: impl Into>) + a: Op>>, + b: Op>>) { let mut c = c.into(); - let a = a.into(); - let b = b.into(); + let a = a.convert(); + let b = b.convert(); - match (trans_a, trans_b) { - (Transpose(false), Transpose(false)) => c.gemm(alpha, &a, &b, beta), - (Transpose(true), Transpose(false)) => c.gemm(alpha, &a.transpose(), &b, beta), - (Transpose(false), Transpose(true)) => c.gemm(alpha, &a, &b.transpose(), beta), - (Transpose(true), Transpose(true)) => c.gemm(alpha, &a.transpose(), &b.transpose(), beta) - }; + use Op::{NoOp, Transpose}; + match (a, b) { + (NoOp(a), NoOp(b)) => c.gemm(alpha, &a, &b, beta), + (Transpose(a), NoOp(b)) => c.gemm(alpha, &a.transpose(), &b, beta), + (NoOp(a), Transpose(b)) => c.gemm(alpha, &a, &b.transpose(), beta), + (Transpose(a), Transpose(b)) => c.gemm(alpha, &a.transpose(), &b.transpose(), beta) + } } proptest! { #[test] fn spmm_csr_dense_agrees_with_dense_result( - SpmmCsrDenseArgs { c, beta, alpha, trans_a, a, trans_b, b } + SpmmCsrDenseArgs { c, beta, alpha, a, b } in spmm_csr_dense_args_strategy() ) { let mut spmm_result = c.clone(); - spmm_csr_dense(&mut spmm_result, beta, alpha, trans_a, &a, trans_b, &b); + spmm_csr_dense(&mut spmm_result, beta, alpha, a.as_ref(), b.as_ref()); let mut gemm_result = c.clone(); - dense_gemm(&mut gemm_result, beta, alpha, trans_a, &DMatrix::from(&a), trans_b, &b); + let a_dense = a.map_same_op(|a| DMatrix::from(&a)); + dense_gemm(&mut gemm_result, beta, alpha, a_dense.as_ref(), b.as_ref()); prop_assert_eq!(spmm_result, gemm_result); } #[test] fn spmm_csr_dense_panics_on_dim_mismatch( - (alpha, beta, c, a, b, trans_a, trans_b) - in (-5 ..= 5, -5 ..= 5, dense_strategy(), csr_strategy(), - dense_strategy(), trans_strategy(), trans_strategy()) + (alpha, beta, c, a, b) + in (PROPTEST_I32_VALUE_STRATEGY, + PROPTEST_I32_VALUE_STRATEGY, + dense_strategy(), + op_strategy(csr_strategy()), + op_strategy(dense_strategy())) ) { // We refer to `A * B` as the "product" - let product_rows = if trans_a.to_bool() { a.ncols() } else { a.nrows() }; - let product_cols = if trans_b.to_bool() { b.nrows() } else { b.ncols() }; + let product_rows = match &a { + Op::NoOp(ref a) => a.nrows(), + Op::Transpose(ref a) => a.ncols(), + }; + let product_cols = match &b { + Op::NoOp(ref b) => b.ncols(), + Op::Transpose(ref b) => b.nrows(), + }; // Determine the common dimension in the product // from the perspective of a and b, respectively - let product_a_common = if trans_a.to_bool() { a.nrows() } else { a.ncols() }; - let product_b_common = if trans_b.to_bool() { b.ncols() } else { b.nrows() }; + let product_a_common = match &a { + Op::NoOp(ref a) => a.ncols(), + Op::Transpose(ref a) => a.nrows(), + }; + let product_b_common = match &b { + Op::NoOp(ref b) => b.nrows(), + Op::Transpose(ref b) => b.ncols() + }; let dims_are_compatible = product_rows == c.nrows() && product_cols == c.ncols() @@ -239,7 +257,7 @@ proptest! { let result = catch_unwind(|| { let mut spmm_result = c.clone(); - spmm_csr_dense(&mut spmm_result, beta, alpha, trans_a, &a, trans_b, &b); + spmm_csr_dense(&mut spmm_result, beta, alpha, a.as_ref(), b.as_ref()); }); prop_assert!(result.is_err(), @@ -247,7 +265,7 @@ proptest! { } #[test] - fn spadd_pattern_test((a, b) in spadd_build_pattern_strategy()) + fn spadd_pattern_test((a, b) in spadd_pattern_strategy()) { // (a, b) are dimensionally compatible patterns let pattern_result = spadd_pattern(&a, &b); @@ -269,16 +287,18 @@ proptest! { } #[test] - fn spadd_csr_test(SpaddCsrArgs { c, beta, alpha, trans_a, a } in spadd_csr_args_strategy()) { + fn spadd_csr_test(SpaddCsrArgs { c, beta, alpha, a } in spadd_csr_args_strategy()) { // Test that we get the expected result by comparing to an equivalent dense operation // (here we give in the C matrix, so the sparsity pattern is essentially fixed) let mut c_sparse = c.clone(); - spadd_csr(&mut c_sparse, beta, alpha, trans_a, &a).unwrap(); + spadd_csr(&mut c_sparse, beta, alpha, a.as_ref()).unwrap(); let mut c_dense = DMatrix::from(&c); - let op_a_dense = DMatrix::from(&a); - let op_a_dense = if trans_a.to_bool() { op_a_dense.transpose() } else { op_a_dense }; + let op_a_dense = match a { + Op::NoOp(a) => DMatrix::from(&a), + Op::Transpose(a) => DMatrix::from(&a).transpose(), + }; c_dense = beta * c_dense + alpha * &op_a_dense; prop_assert_eq!(&DMatrix::from(&c_sparse), &c_dense); @@ -343,19 +363,23 @@ proptest! { } #[test] - fn spmm_csr_test(SpmmCsrArgs { c, beta, alpha, trans_a, a, trans_b, b } + fn spmm_csr_test(SpmmCsrArgs { c, beta, alpha, a, b } in spmm_csr_args_strategy() ) { // Test that we get the expected result by comparing to an equivalent dense operation // (here we give in the C matrix, so the sparsity pattern is essentially fixed) let mut c_sparse = c.clone(); - spmm_csr(&mut c_sparse, beta, alpha, trans_a, &a, trans_b, &b).unwrap(); + spmm_csr(&mut c_sparse, beta, alpha, a.as_ref(), b.as_ref()).unwrap(); let mut c_dense = DMatrix::from(&c); - let op_a_dense = DMatrix::from(&a); - let op_a_dense = if trans_a.to_bool() { op_a_dense.transpose() } else { op_a_dense }; - let op_b_dense = DMatrix::from(&b); - let op_b_dense = if trans_b.to_bool() { op_b_dense.transpose() } else { op_b_dense }; + let op_a_dense = match a { + Op::NoOp(ref a) => DMatrix::from(a), + Op::Transpose(ref a) => DMatrix::from(a).transpose(), + }; + let op_b_dense = match b { + Op::NoOp(ref b) => DMatrix::from(b), + Op::Transpose(ref b) => DMatrix::from(b).transpose(), + }; c_dense = beta * c_dense + alpha * &op_a_dense * op_b_dense; prop_assert_eq!(&DMatrix::from(&c_sparse), &c_dense); @@ -363,22 +387,32 @@ proptest! { #[test] fn spmm_csr_panics_on_dim_mismatch( - (alpha, beta, c, a, b, trans_a, trans_b) + (alpha, beta, c, a, b) in (PROPTEST_I32_VALUE_STRATEGY, PROPTEST_I32_VALUE_STRATEGY, csr_strategy(), - csr_strategy(), - csr_strategy(), - trans_strategy(), - trans_strategy()) + op_strategy(csr_strategy()), + op_strategy(csr_strategy())) ) { // We refer to `A * B` as the "product" - let product_rows = if trans_a.to_bool() { a.ncols() } else { a.nrows() }; - let product_cols = if trans_b.to_bool() { b.nrows() } else { b.ncols() }; + let product_rows = match &a { + Op::NoOp(ref a) => a.nrows(), + Op::Transpose(ref a) => a.ncols(), + }; + let product_cols = match &b { + Op::NoOp(ref b) => b.ncols(), + Op::Transpose(ref b) => b.nrows(), + }; // Determine the common dimension in the product // from the perspective of a and b, respectively - let product_a_common = if trans_a.to_bool() { a.nrows() } else { a.ncols() }; - let product_b_common = if trans_b.to_bool() { b.ncols() } else { b.nrows() }; + let product_a_common = match &a { + Op::NoOp(ref a) => a.ncols(), + Op::Transpose(ref a) => a.nrows(), + }; + let product_b_common = match &b { + Op::NoOp(ref b) => b.nrows(), + Op::Transpose(ref b) => b.ncols(), + }; let dims_are_compatible = product_rows == c.nrows() && product_cols == c.ncols() @@ -390,7 +424,7 @@ proptest! { let result = catch_unwind(|| { let mut spmm_result = c.clone(); - spmm_csr(&mut spmm_result, beta, alpha, trans_a, &a, trans_b, &b).unwrap(); + spmm_csr(&mut spmm_result, beta, alpha, a.as_ref(), b.as_ref()).unwrap(); }); prop_assert!(result.is_err(), @@ -399,15 +433,20 @@ proptest! { #[test] fn spadd_csr_panics_on_dim_mismatch( - (alpha, beta, c, a, trans_a) + (alpha, beta, c, op_a) in (PROPTEST_I32_VALUE_STRATEGY, PROPTEST_I32_VALUE_STRATEGY, csr_strategy(), - csr_strategy(), - trans_strategy()) + op_strategy(csr_strategy())) ) { - let op_a_rows = if trans_a.to_bool() { a.ncols() } else { a.nrows() }; - let op_a_cols = if trans_a.to_bool() { a.nrows() } else { a.ncols() }; + let op_a_rows = match &op_a { + &Op::NoOp(ref a) => a.nrows(), + &Op::Transpose(ref a) => a.ncols() + }; + let op_a_cols = match &op_a { + &Op::NoOp(ref a) => a.ncols(), + &Op::Transpose(ref a) => a.nrows() + }; let dims_are_compatible = c.nrows() == op_a_rows && c.ncols() == op_a_cols; @@ -417,7 +456,7 @@ proptest! { let result = catch_unwind(|| { let mut spmm_result = c.clone(); - spadd_csr(&mut spmm_result, beta, alpha, trans_a, &a).unwrap(); + spadd_csr(&mut spmm_result, beta, alpha, op_a.as_ref()).unwrap(); }); prop_assert!(result.is_err(), From 061024ab1fe63807a3c35771a3d1b062b27c40b0 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 21 Dec 2020 15:13:31 +0100 Subject: [PATCH 046/183] Improve Ops API --- nalgebra-sparse/src/ops/mod.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/nalgebra-sparse/src/ops/mod.rs b/nalgebra-sparse/src/ops/mod.rs index 14a18dc1..80fde80d 100644 --- a/nalgebra-sparse/src/ops/mod.rs +++ b/nalgebra-sparse/src/ops/mod.rs @@ -15,10 +15,7 @@ pub enum Op { impl Op { /// TODO pub fn inner_ref(&self) -> &T { - match self { - Op::NoOp(obj) => &obj, - Op::Transpose(obj) => &obj - } + self.as_ref().unwrap() } /// TODO @@ -33,10 +30,7 @@ impl Op { pub fn convert(self) -> Op where T: Into { - match self { - Op::NoOp(obj) => Op::NoOp(obj.into()), - Op::Transpose(obj) => Op::Transpose(obj.into()) - } + self.map_same_op(T::into) } /// TODO @@ -47,6 +41,13 @@ impl Op { Op::Transpose(obj) => Op::Transpose(f(obj)) } } + + /// TODO + pub fn unwrap(self) -> T { + match self { + Op::NoOp(obj) | Op::Transpose(obj) => obj, + } + } } impl From for Op { From 4af3fcbdd3e93cfa93a7d842ad40b8761e830452 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 21 Dec 2020 15:42:32 +0100 Subject: [PATCH 047/183] Reorder parameters in ops to intuitive order --- nalgebra-sparse/src/ops/impl_std_ops.rs | 10 +++++----- nalgebra-sparse/src/ops/serial/csr.rs | 20 ++++++++++---------- nalgebra-sparse/tests/unit_tests/ops.rs | 20 ++++++++++---------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/nalgebra-sparse/src/ops/impl_std_ops.rs b/nalgebra-sparse/src/ops/impl_std_ops.rs index c181464d..92973e34 100644 --- a/nalgebra-sparse/src/ops/impl_std_ops.rs +++ b/nalgebra-sparse/src/ops/impl_std_ops.rs @@ -21,8 +21,8 @@ where // We are giving data that is valid by definition, so it is safe to unwrap below let mut result = CsrMatrix::try_from_pattern_and_values(Arc::new(pattern), values) .unwrap(); - spadd_csr(&mut result, T::zero(), T::one(), Op::NoOp(&self)).unwrap(); - spadd_csr(&mut result, T::one(), T::one(), Op::NoOp(&rhs)).unwrap(); + spadd_csr(T::zero(), &mut result, T::one(), Op::NoOp(&self)).unwrap(); + spadd_csr(T::one(), &mut result, T::one(), Op::NoOp(&rhs)).unwrap(); result } } @@ -35,7 +35,7 @@ where fn add(mut self, rhs: &'a CsrMatrix) -> Self::Output { if Arc::ptr_eq(self.pattern(), rhs.pattern()) { - spadd_csr(&mut self, T::one(), T::one(), Op::NoOp(rhs)).unwrap(); + spadd_csr(T::one(), &mut self, T::one(), Op::NoOp(rhs)).unwrap(); self } else { &self + rhs @@ -90,8 +90,8 @@ impl_matrix_mul!(<'a>(a: &'a CsrMatrix, b: &'a CsrMatrix) -> CsrMatrix let values = vec![T::zero(); pattern.nnz()]; let mut result = CsrMatrix::try_from_pattern_and_values(Arc::new(pattern), values) .unwrap(); - spmm_csr(&mut result, - T::zero(), + spmm_csr(T::zero(), + &mut result, T::one(), Op::NoOp(a), Op::NoOp(b)) diff --git a/nalgebra-sparse/src/ops/serial/csr.rs b/nalgebra-sparse/src/ops/serial/csr.rs index e1b5a1c5..88284114 100644 --- a/nalgebra-sparse/src/ops/serial/csr.rs +++ b/nalgebra-sparse/src/ops/serial/csr.rs @@ -8,8 +8,8 @@ use std::sync::Arc; use std::borrow::Cow; /// Sparse-dense matrix-matrix multiplication `C <- beta * C + alpha * op(A) * op(B)`. -pub fn spmm_csr_dense<'a, T>(c: impl Into>, - beta: T, +pub fn spmm_csr_dense<'a, T>(beta: T, + c: impl Into>, alpha: T, a: Op<&CsrMatrix>, b: Op>>) @@ -17,11 +17,11 @@ pub fn spmm_csr_dense<'a, T>(c: impl Into>, T: Scalar + ClosedAdd + ClosedMul + Zero + One { let b = b.convert(); - spmm_csr_dense_(c.into(), beta, alpha, a, b) + spmm_csr_dense_(beta, c.into(), alpha, a, b) } -fn spmm_csr_dense_(mut c: DMatrixSliceMut, - beta: T, +fn spmm_csr_dense_(beta: T, + mut c: DMatrixSliceMut, alpha: T, a: Op<&CsrMatrix>, b: Op>) @@ -87,8 +87,8 @@ fn spadd_csr_unexpected_entry() -> OperationError { /// /// If the pattern of `c` does not accommodate all the non-zero entries in `a`, an error is /// returned. -pub fn spadd_csr(c: &mut CsrMatrix, - beta: T, +pub fn spadd_csr(beta: T, + c: &mut CsrMatrix, alpha: T, a: Op<&CsrMatrix>) -> Result<(), OperationError> @@ -161,9 +161,9 @@ fn spmm_csr_unexpected_entry() -> OperationError { } /// Sparse-sparse matrix multiplication, `C <- beta * C + alpha * op(A) * op(B)`. -pub fn spmm_csr<'a, T>( - c: &mut CsrMatrix, +pub fn spmm_csr( beta: T, + c: &mut CsrMatrix, alpha: T, a: Op<&CsrMatrix>, b: Op<&CsrMatrix>) @@ -218,7 +218,7 @@ where } }; - spmm_csr(c, beta, alpha, NoOp(a.as_ref()), NoOp(b.as_ref())) + spmm_csr(beta, c, alpha, NoOp(a.as_ref()), NoOp(b.as_ref())) } } } diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index 55953491..c341739d 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -181,9 +181,9 @@ fn spmm_csr_args_strategy() -> impl Strategy> { }) } -/// Helper function to help us call dense GEMM with our transposition parameters -fn dense_gemm<'a>(c: impl Into>, - beta: i32, +/// Helper function to help us call dense GEMM with our `Op` type +fn dense_gemm<'a>(beta: i32, + c: impl Into>, alpha: i32, a: Op>>, b: Op>>) @@ -209,11 +209,11 @@ proptest! { in spmm_csr_dense_args_strategy() ) { let mut spmm_result = c.clone(); - spmm_csr_dense(&mut spmm_result, beta, alpha, a.as_ref(), b.as_ref()); + spmm_csr_dense(beta, &mut spmm_result, alpha, a.as_ref(), b.as_ref()); let mut gemm_result = c.clone(); let a_dense = a.map_same_op(|a| DMatrix::from(&a)); - dense_gemm(&mut gemm_result, beta, alpha, a_dense.as_ref(), b.as_ref()); + dense_gemm(beta, &mut gemm_result, alpha, a_dense.as_ref(), b.as_ref()); prop_assert_eq!(spmm_result, gemm_result); } @@ -257,7 +257,7 @@ proptest! { let result = catch_unwind(|| { let mut spmm_result = c.clone(); - spmm_csr_dense(&mut spmm_result, beta, alpha, a.as_ref(), b.as_ref()); + spmm_csr_dense(beta, &mut spmm_result, alpha, a.as_ref(), b.as_ref()); }); prop_assert!(result.is_err(), @@ -292,7 +292,7 @@ proptest! { // (here we give in the C matrix, so the sparsity pattern is essentially fixed) let mut c_sparse = c.clone(); - spadd_csr(&mut c_sparse, beta, alpha, a.as_ref()).unwrap(); + spadd_csr(beta, &mut c_sparse, alpha, a.as_ref()).unwrap(); let mut c_dense = DMatrix::from(&c); let op_a_dense = match a { @@ -369,7 +369,7 @@ proptest! { // Test that we get the expected result by comparing to an equivalent dense operation // (here we give in the C matrix, so the sparsity pattern is essentially fixed) let mut c_sparse = c.clone(); - spmm_csr(&mut c_sparse, beta, alpha, a.as_ref(), b.as_ref()).unwrap(); + spmm_csr(beta, &mut c_sparse, alpha, a.as_ref(), b.as_ref()).unwrap(); let mut c_dense = DMatrix::from(&c); let op_a_dense = match a { @@ -424,7 +424,7 @@ proptest! { let result = catch_unwind(|| { let mut spmm_result = c.clone(); - spmm_csr(&mut spmm_result, beta, alpha, a.as_ref(), b.as_ref()).unwrap(); + spmm_csr(beta, &mut spmm_result, alpha, a.as_ref(), b.as_ref()).unwrap(); }); prop_assert!(result.is_err(), @@ -456,7 +456,7 @@ proptest! { let result = catch_unwind(|| { let mut spmm_result = c.clone(); - spadd_csr(&mut spmm_result, beta, alpha, op_a.as_ref()).unwrap(); + spadd_csr(beta, &mut spmm_result, alpha, op_a.as_ref()).unwrap(); }); prop_assert!(result.is_err(), From 66cbd26702d6c0ecaa4a4f611ad2818779f7667d Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 21 Dec 2020 16:05:38 +0100 Subject: [PATCH 048/183] Add prealloc suffix to spmm_csr and spadd_csr The suffix is intended to communicate that these methods assume `preallocated` storage, i.e. they try to store the result in a matrix which already has the correct sparsity pattern for the operation. --- nalgebra-sparse/src/ops/impl_std_ops.rs | 10 +++++----- nalgebra-sparse/src/ops/serial/csr.rs | 14 +++++++------- nalgebra-sparse/tests/unit_tests/ops.rs | 24 ++++++++++++------------ 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/nalgebra-sparse/src/ops/impl_std_ops.rs b/nalgebra-sparse/src/ops/impl_std_ops.rs index 92973e34..f558b7c0 100644 --- a/nalgebra-sparse/src/ops/impl_std_ops.rs +++ b/nalgebra-sparse/src/ops/impl_std_ops.rs @@ -1,7 +1,7 @@ use crate::csr::CsrMatrix; use std::ops::{Add, Mul}; -use crate::ops::serial::{spadd_csr, spadd_pattern, spmm_pattern, spmm_csr}; +use crate::ops::serial::{spadd_csr_prealloc, spadd_pattern, spmm_pattern, spmm_csr_prealloc}; use nalgebra::{ClosedAdd, ClosedMul, Scalar}; use num_traits::{Zero, One}; use std::sync::Arc; @@ -21,8 +21,8 @@ where // We are giving data that is valid by definition, so it is safe to unwrap below let mut result = CsrMatrix::try_from_pattern_and_values(Arc::new(pattern), values) .unwrap(); - spadd_csr(T::zero(), &mut result, T::one(), Op::NoOp(&self)).unwrap(); - spadd_csr(T::one(), &mut result, T::one(), Op::NoOp(&rhs)).unwrap(); + spadd_csr_prealloc(T::zero(), &mut result, T::one(), Op::NoOp(&self)).unwrap(); + spadd_csr_prealloc(T::one(), &mut result, T::one(), Op::NoOp(&rhs)).unwrap(); result } } @@ -35,7 +35,7 @@ where fn add(mut self, rhs: &'a CsrMatrix) -> Self::Output { if Arc::ptr_eq(self.pattern(), rhs.pattern()) { - spadd_csr(T::one(), &mut self, T::one(), Op::NoOp(rhs)).unwrap(); + spadd_csr_prealloc(T::one(), &mut self, T::one(), Op::NoOp(rhs)).unwrap(); self } else { &self + rhs @@ -90,7 +90,7 @@ impl_matrix_mul!(<'a>(a: &'a CsrMatrix, b: &'a CsrMatrix) -> CsrMatrix let values = vec![T::zero(); pattern.nnz()]; let mut result = CsrMatrix::try_from_pattern_and_values(Arc::new(pattern), values) .unwrap(); - spmm_csr(T::zero(), + spmm_csr_prealloc(T::zero(), &mut result, T::one(), Op::NoOp(a), diff --git a/nalgebra-sparse/src/ops/serial/csr.rs b/nalgebra-sparse/src/ops/serial/csr.rs index 88284114..4dd3ae61 100644 --- a/nalgebra-sparse/src/ops/serial/csr.rs +++ b/nalgebra-sparse/src/ops/serial/csr.rs @@ -87,11 +87,11 @@ fn spadd_csr_unexpected_entry() -> OperationError { /// /// If the pattern of `c` does not accommodate all the non-zero entries in `a`, an error is /// returned. -pub fn spadd_csr(beta: T, - c: &mut CsrMatrix, - alpha: T, - a: Op<&CsrMatrix>) - -> Result<(), OperationError> +pub fn spadd_csr_prealloc(beta: T, + c: &mut CsrMatrix, + alpha: T, + a: Op<&CsrMatrix>) + -> Result<(), OperationError> where T: Scalar + ClosedAdd + ClosedMul + Zero + One { @@ -161,7 +161,7 @@ fn spmm_csr_unexpected_entry() -> OperationError { } /// Sparse-sparse matrix multiplication, `C <- beta * C + alpha * op(A) * op(B)`. -pub fn spmm_csr( +pub fn spmm_csr_prealloc( beta: T, c: &mut CsrMatrix, alpha: T, @@ -218,7 +218,7 @@ where } }; - spmm_csr(beta, c, alpha, NoOp(a.as_ref()), NoOp(b.as_ref())) + spmm_csr_prealloc(beta, c, alpha, NoOp(a.as_ref()), NoOp(b.as_ref())) } } } diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index c341739d..b56c583a 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -1,6 +1,6 @@ use crate::common::{csr_strategy, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ, PROPTEST_I32_VALUE_STRATEGY}; -use nalgebra_sparse::ops::serial::{spmm_csr_dense, spadd_pattern, spmm_pattern, spadd_csr, spmm_csr}; +use nalgebra_sparse::ops::serial::{spmm_csr_dense, spadd_pattern, spmm_pattern, spadd_csr_prealloc, spmm_csr_prealloc}; use nalgebra_sparse::ops::{Op}; use nalgebra_sparse::csr::CsrMatrix; use nalgebra_sparse::proptest::{csr, sparsity_pattern}; @@ -78,7 +78,7 @@ struct SpaddCsrArgs { a: Op>, } -fn spadd_csr_args_strategy() -> impl Strategy> { +fn spadd_csr_prealloc_args_strategy() -> impl Strategy> { let value_strategy = PROPTEST_I32_VALUE_STRATEGY; spadd_pattern_strategy() @@ -150,7 +150,7 @@ struct SpmmCsrArgs { b: Op>, } -fn spmm_csr_args_strategy() -> impl Strategy> { +fn spmm_csr_prealloc_args_strategy() -> impl Strategy> { spmm_pattern_strategy() .prop_flat_map(|(a_pattern, b_pattern)| { let a_values = vec![PROPTEST_I32_VALUE_STRATEGY; a_pattern.nnz()]; @@ -287,12 +287,12 @@ proptest! { } #[test] - fn spadd_csr_test(SpaddCsrArgs { c, beta, alpha, a } in spadd_csr_args_strategy()) { + fn spadd_csr_prealloc_test(SpaddCsrArgs { c, beta, alpha, a } in spadd_csr_prealloc_args_strategy()) { // Test that we get the expected result by comparing to an equivalent dense operation // (here we give in the C matrix, so the sparsity pattern is essentially fixed) let mut c_sparse = c.clone(); - spadd_csr(beta, &mut c_sparse, alpha, a.as_ref()).unwrap(); + spadd_csr_prealloc(beta, &mut c_sparse, alpha, a.as_ref()).unwrap(); let mut c_dense = DMatrix::from(&c); let op_a_dense = match a { @@ -363,13 +363,13 @@ proptest! { } #[test] - fn spmm_csr_test(SpmmCsrArgs { c, beta, alpha, a, b } - in spmm_csr_args_strategy() + fn spmm_csr_prealloc_test(SpmmCsrArgs { c, beta, alpha, a, b } + in spmm_csr_prealloc_args_strategy() ) { // Test that we get the expected result by comparing to an equivalent dense operation // (here we give in the C matrix, so the sparsity pattern is essentially fixed) let mut c_sparse = c.clone(); - spmm_csr(beta, &mut c_sparse, alpha, a.as_ref(), b.as_ref()).unwrap(); + spmm_csr_prealloc(beta, &mut c_sparse, alpha, a.as_ref(), b.as_ref()).unwrap(); let mut c_dense = DMatrix::from(&c); let op_a_dense = match a { @@ -386,7 +386,7 @@ proptest! { } #[test] - fn spmm_csr_panics_on_dim_mismatch( + fn spmm_csr_prealloc_panics_on_dim_mismatch( (alpha, beta, c, a, b) in (PROPTEST_I32_VALUE_STRATEGY, PROPTEST_I32_VALUE_STRATEGY, @@ -424,7 +424,7 @@ proptest! { let result = catch_unwind(|| { let mut spmm_result = c.clone(); - spmm_csr(beta, &mut spmm_result, alpha, a.as_ref(), b.as_ref()).unwrap(); + spmm_csr_prealloc(beta, &mut spmm_result, alpha, a.as_ref(), b.as_ref()).unwrap(); }); prop_assert!(result.is_err(), @@ -432,7 +432,7 @@ proptest! { } #[test] - fn spadd_csr_panics_on_dim_mismatch( + fn spadd_csr_prealloc_panics_on_dim_mismatch( (alpha, beta, c, op_a) in (PROPTEST_I32_VALUE_STRATEGY, PROPTEST_I32_VALUE_STRATEGY, @@ -456,7 +456,7 @@ proptest! { let result = catch_unwind(|| { let mut spmm_result = c.clone(); - spadd_csr(beta, &mut spmm_result, alpha, op_a.as_ref()).unwrap(); + spadd_csr_prealloc(beta, &mut spmm_result, alpha, op_a.as_ref()).unwrap(); }); prop_assert!(result.is_err(), From 8983027b393a26a721afbee281670f4a93ca9990 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 21 Dec 2020 17:09:26 +0100 Subject: [PATCH 049/183] Minor refactoring for sp* ops --- nalgebra-sparse/src/ops/serial/csr.rs | 98 ++++++++++++++------------- 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/nalgebra-sparse/src/ops/serial/csr.rs b/nalgebra-sparse/src/ops/serial/csr.rs index 4dd3ae61..da975c7a 100644 --- a/nalgebra-sparse/src/ops/serial/csr.rs +++ b/nalgebra-sparse/src/ops/serial/csr.rs @@ -31,6 +31,23 @@ where assert_compatible_spmm_dims!(c, a, b); match a { + Op::NoOp(ref a) => { + for j in 0..c.ncols() { + let mut c_col_j = c.column_mut(j); + for (c_ij, a_row_i) in c_col_j.iter_mut().zip(a.row_iter()) { + let mut dot_ij = T::zero(); + for (&k, a_ik) in a_row_i.col_indices().iter().zip(a_row_i.values()) { + let b_contrib = + match b { + Op::NoOp(ref b) => b.index((k, j)), + Op::Transpose(ref b) => b.index((j, k)) + }; + dot_ij += a_ik.inlined_clone() * b_contrib.inlined_clone(); + } + *c_ij = beta.inlined_clone() * c_ij.inlined_clone() + alpha.inlined_clone() * dot_ij; + } + } + }, Op::Transpose(ref a) => { // In this case, we have to pre-multiply C by beta c *= beta; @@ -57,23 +74,6 @@ where } } }, - Op::NoOp(ref a) => { - for j in 0..c.ncols() { - let mut c_col_j = c.column_mut(j); - for (c_ij, a_row_i) in c_col_j.iter_mut().zip(a.row_iter()) { - let mut dot_ij = T::zero(); - for (&k, a_ik) in a_row_i.col_indices().iter().zip(a_row_i.values()) { - let b_contrib = - match b { - Op::NoOp(ref b) => b.index((k, j)), - Op::Transpose(ref b) => b.index((j, k)) - }; - dot_ij += a_ik.inlined_clone() * b_contrib.inlined_clone(); - } - *c_ij = beta.inlined_clone() * c_ij.inlined_clone() + alpha.inlined_clone() * dot_ij; - } - } - } } } @@ -107,46 +107,48 @@ where } Ok(()) } else { - if let Op::Transpose(a) = a - { - if beta != T::one() { - for c_ij in c.values_mut() { - *c_ij *= beta.inlined_clone(); - } - } + match a { + Op::NoOp(a) => { + for (mut c_row_i, a_row_i) in c.row_iter_mut().zip(a.row_iter()) { + if beta != T::one() { + for c_ij in c_row_i.values_mut() { + *c_ij *= beta.inlined_clone(); + } + } - for (i, a_row_i) in a.row_iter().enumerate() { - for (&j, a_val) in a_row_i.col_indices().iter().zip(a_row_i.values()) { - let a_val = a_val.inlined_clone(); - let alpha = alpha.inlined_clone(); - match c.index_entry_mut(j, i) { - SparseEntryMut::NonZero(c_ji) => { *c_ji += alpha * a_val } - SparseEntryMut::Zero => return Err(spadd_csr_unexpected_entry()), + let (mut c_cols, mut c_vals) = c_row_i.cols_and_values_mut(); + let (a_cols, a_vals) = (a_row_i.col_indices(), a_row_i.values()); + + for (a_col, a_val) in a_cols.iter().zip(a_vals) { + // TODO: Use exponential search instead of linear search. + // If C has substantially more entries in the row than A, then a line search + // will needlessly visit many entries in C. + let (c_idx, _) = c_cols.iter() + .enumerate() + .find(|(_, c_col)| *c_col == a_col) + .ok_or_else(spadd_csr_unexpected_entry)?; + c_vals[c_idx] += alpha.inlined_clone() * a_val.inlined_clone(); + c_cols = &c_cols[c_idx ..]; + c_vals = &mut c_vals[c_idx ..]; } } } - } else if let Op::NoOp(a) = a { - for (mut c_row_i, a_row_i) in c.row_iter_mut().zip(a.row_iter()) { + Op::Transpose(a) => { if beta != T::one() { - for c_ij in c_row_i.values_mut() { + for c_ij in c.values_mut() { *c_ij *= beta.inlined_clone(); } } - let (mut c_cols, mut c_vals) = c_row_i.cols_and_values_mut(); - let (a_cols, a_vals) = (a_row_i.col_indices(), a_row_i.values()); - - for (a_col, a_val) in a_cols.iter().zip(a_vals) { - // TODO: Use exponential search instead of linear search. - // If C has substantially more entries in the row than A, then a line search - // will needlessly visit many entries in C. - let (c_idx, _) = c_cols.iter() - .enumerate() - .find(|(_, c_col)| *c_col == a_col) - .ok_or_else(spadd_csr_unexpected_entry)?; - c_vals[c_idx] += alpha.inlined_clone() * a_val.inlined_clone(); - c_cols = &c_cols[c_idx ..]; - c_vals = &mut c_vals[c_idx ..]; + for (i, a_row_i) in a.row_iter().enumerate() { + for (&j, a_val) in a_row_i.col_indices().iter().zip(a_row_i.values()) { + let a_val = a_val.inlined_clone(); + let alpha = alpha.inlined_clone(); + match c.index_entry_mut(j, i) { + SparseEntryMut::NonZero(c_ji) => { *c_ji += alpha * a_val } + SparseEntryMut::Zero => return Err(spadd_csr_unexpected_entry()), + } + } } } } From b59c4a3216f103c085830307c74ca3ba35a54335 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 22 Dec 2020 10:19:17 +0100 Subject: [PATCH 050/183] Refactor most of Csr/CscMatrix logic into helper type CsMatrix Still need to update CSC API so that it mirrors CsrMatrix in terms of get_entry and so on. --- nalgebra-sparse/src/cs.rs | 286 +++++++++++++++++++++++++++++++++++++ nalgebra-sparse/src/csc.rs | 157 ++++++-------------- nalgebra-sparse/src/csr.rs | 210 ++++++++------------------- nalgebra-sparse/src/lib.rs | 2 + 4 files changed, 394 insertions(+), 261 deletions(-) create mode 100644 nalgebra-sparse/src/cs.rs diff --git a/nalgebra-sparse/src/cs.rs b/nalgebra-sparse/src/cs.rs new file mode 100644 index 00000000..7a38055b --- /dev/null +++ b/nalgebra-sparse/src/cs.rs @@ -0,0 +1,286 @@ +use crate::pattern::SparsityPattern; +use crate::{SparseEntry, SparseEntryMut}; + +use std::sync::Arc; +use std::ops::Range; +use std::ptr::slice_from_raw_parts_mut; + +/// An abstract compressed matrix. +/// +/// For the time being, this is only used internally to share implementation between +/// CSR and CSC matrices. +/// +/// 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)] +pub struct CsMatrix { + sparsity_pattern: Arc, + values: Vec +} + +impl CsMatrix { + /// Create a zero matrix with no explicitly stored entries. + #[inline] + pub fn new(major_dim: usize, minor_dim: usize) -> Self { + Self { + sparsity_pattern: Arc::new(SparsityPattern::new(major_dim, minor_dim)), + values: vec![], + } + } + + #[inline] + pub fn pattern(&self) -> &Arc { + &self.sparsity_pattern + } + + #[inline] + pub fn values(&self) -> &[T] { + &self.values + } + + #[inline] + pub fn values_mut(&mut self) -> &mut [T] { + &mut self.values + } + + /// Returns the raw data represented as a tuple `(major_offsets, minor_indices, values)`. + #[inline] + pub fn cs_data(&self) -> (&[usize], &[usize], &[T]) { + let pattern = self.pattern().as_ref(); + (pattern.major_offsets(), pattern.minor_indices(), &self.values) + } + + /// Returns the raw data represented as a tuple `(major_offsets, minor_indices, values)`. + #[inline] + pub fn cs_data_mut(&mut self) -> (&[usize], &[usize], &mut [T]) { + let pattern = self.sparsity_pattern.as_ref(); + (pattern.major_offsets(), pattern.minor_indices(), &mut self.values) + } + + #[inline] + pub fn pattern_and_values_mut(&mut self) -> (&Arc, &mut [T]) { + (&self.sparsity_pattern, &mut self.values) + } + + #[inline] + pub fn from_pattern_and_values(pattern: Arc, values: Vec) + -> Self { + assert_eq!(pattern.nnz(), values.len(), "Internal error: consumers should verify shape compatibility."); + Self { + sparsity_pattern: pattern, + values, + } + } + + /// Internal method for simplifying access to a lane's data + #[inline] + pub fn get_index_range(&self, row_index: usize) -> Option> { + let row_begin = *self.sparsity_pattern.major_offsets().get(row_index)?; + let row_end = *self.sparsity_pattern.major_offsets().get(row_index + 1)?; + Some(row_begin .. row_end) + } + + pub fn take_pattern_and_values(self) -> (Arc, Vec) { + (self.sparsity_pattern, self.values) + } + + #[inline] + pub fn disassemble(self) -> (Vec, Vec, Vec) { + // Take an Arc to the pattern, which might be the sole reference to the data after + // taking the values. This is important, because it might let us avoid cloning the data + // further below. + let pattern = self.sparsity_pattern; + let values = self.values; + + // Try to take the pattern out of the `Arc` if possible, + // otherwise clone the pattern. + let owned_pattern = Arc::try_unwrap(pattern) + .unwrap_or_else(|arc| SparsityPattern::clone(&*arc)); + let (offsets, indices) = owned_pattern.disassemble(); + + (offsets, indices, values) + } + + /// Returns an entry for the given major/minor indices, or `None` if the indices are out + /// of bounds. + pub fn get_entry(&self, major_index: usize, minor_index: usize) -> Option> { + let row_range = self.get_index_range(major_index)?; + let (_, minor_indices, values) = self.cs_data(); + let minor_indices = &minor_indices[row_range.clone()]; + let values = &values[row_range]; + get_entry_from_slices(self.pattern().minor_dim(), minor_indices, values, minor_index) + } + + /// Returns a mutable entry for the given major/minor indices, or `None` if the indices are out + /// of bounds. + pub fn get_entry_mut(&mut self, major_index: usize, minor_index: usize) + -> Option> { + let row_range = self.get_index_range(major_index)?; + let minor_dim = self.pattern().minor_dim(); + let (_, minor_indices, values) = self.cs_data_mut(); + let minor_indices = &minor_indices[row_range.clone()]; + let values = &mut values[row_range]; + get_mut_entry_from_slices(minor_dim, minor_indices, values, minor_index) + } + + pub fn get_lane(&self, index: usize) -> Option> { + let range = self.get_index_range(index)?; + let (_, minor_indices, values) = self.cs_data(); + Some(CsLane { + minor_indices: &minor_indices[range.clone()], + values: &values[range], + minor_dim: self.pattern().minor_dim() + }) + } + + #[inline] + pub fn get_lane_mut(&mut self, index: usize) -> Option> { + let range = self.get_index_range(index)?; + let minor_dim = self.pattern().minor_dim(); + let (_, minor_indices, values) = self.cs_data_mut(); + Some(CsLaneMut { + minor_dim, + minor_indices: &minor_indices[range.clone()], + values: &mut values[range] + }) + } +} + +pub fn get_entry_from_slices<'a, T>( + minor_dim: usize, + minor_indices: &'a [usize], + values: &'a [T], + global_minor_index: usize) -> Option> { + let local_index = minor_indices.binary_search(&global_minor_index); + if let Ok(local_index) = local_index { + Some(SparseEntry::NonZero(&values[local_index])) + } else if global_minor_index < minor_dim { + Some(SparseEntry::Zero) + } else { + None + } +} + +pub fn get_mut_entry_from_slices<'a, T>( + minor_dim: usize, + minor_indices: &'a [usize], + values: &'a mut [T], + global_minor_indices: usize) -> Option> { + let local_index = minor_indices.binary_search(&global_minor_indices); + if let Ok(local_index) = local_index { + Some(SparseEntryMut::NonZero(&mut values[local_index])) + } else if global_minor_indices < minor_dim { + Some(SparseEntryMut::Zero) + } else { + None + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CsLane<'a, T> { + pub minor_dim: usize, + pub minor_indices: &'a [usize], + pub values: &'a [T] +} + +#[derive(Debug, PartialEq, Eq)] +pub struct CsLaneMut<'a, T> { + pub minor_dim: usize, + pub minor_indices: &'a [usize], + pub values: &'a mut [T] +} + +pub struct CsLaneIter<'a, T> { + // The index of the lane that will be returned on the next iteration + current_lane_idx: usize, + pattern: &'a SparsityPattern, + remaining_values: &'a [T], +} + +impl<'a, T> CsLaneIter<'a, T> { + pub fn new(pattern: &'a SparsityPattern, values: &'a [T]) -> Self { + Self { + current_lane_idx: 0, + pattern, + remaining_values: values + } + } +} + +impl<'a, T> Iterator for CsLaneIter<'a, T> + where + T: 'a +{ + type Item = CsLane<'a, T>; + + fn next(&mut self) -> Option { + let lane = self.pattern.get_lane(self.current_lane_idx); + let minor_dim = self.pattern.minor_dim(); + + if let Some(minor_indices) = lane { + let count = minor_indices.len(); + let values_in_lane = &self.remaining_values[..count]; + self.remaining_values = &self.remaining_values[count ..]; + self.current_lane_idx += 1; + + Some(CsLane { + minor_dim, + minor_indices, + values: values_in_lane + }) + } else { + None + } + } +} + +pub struct CsLaneIterMut<'a, T> { + // The index of the lane that will be returned on the next iteration + current_lane_idx: usize, + pattern: &'a SparsityPattern, + remaining_values: *mut T, +} + +impl<'a, T> CsLaneIterMut<'a, T> { + pub fn new(pattern: &'a SparsityPattern, values: &'a mut [T]) -> Self { + Self { + current_lane_idx: 0, + pattern, + remaining_values: values.as_mut_ptr() + } + } +} + +impl<'a, T> Iterator for CsLaneIterMut<'a, T> + where + T: 'a +{ + type Item = CsLaneMut<'a, T>; + + fn next(&mut self) -> Option { + let lane = self.pattern.get_lane(self.current_lane_idx); + let minor_dim = self.pattern.minor_dim(); + + if let Some(minor_indices) = lane { + let count = minor_indices.len(); + + // Note: I can't think of any way to construct this iterator without unsafe. + let values_in_lane; + unsafe { + values_in_lane = &mut *slice_from_raw_parts_mut(self.remaining_values, count); + self.remaining_values = self.remaining_values.add(count); + } + self.current_lane_idx += 1; + + Some(CsLaneMut { + minor_dim, + minor_indices, + values: values_in_lane + }) + } else { + None + } + } +} + + diff --git a/nalgebra-sparse/src/csc.rs b/nalgebra-sparse/src/csc.rs index a94a5fdc..f39483a2 100644 --- a/nalgebra-sparse/src/csc.rs +++ b/nalgebra-sparse/src/csc.rs @@ -2,13 +2,12 @@ use crate::{SparseFormatError, SparseFormatErrorKind}; use crate::pattern::{SparsityPattern, SparsityPatternFormatError, SparsityPatternIter}; +use crate::csr::CsrMatrix; +use crate::cs::{CsMatrix, CsLane, CsLaneMut, CsLaneIter, CsLaneIterMut}; use std::sync::Arc; use std::slice::{IterMut, Iter}; -use std::ops::Range; use num_traits::Zero; -use std::ptr::slice_from_raw_parts_mut; -use crate::csr::CsrMatrix; use nalgebra::Scalar; /// A CSC representation of a sparse matrix. @@ -21,29 +20,27 @@ use nalgebra::Scalar; #[derive(Debug, Clone, PartialEq, Eq)] pub struct CscMatrix { // Cols are major, rows are minor in the sparsity pattern - sparsity_pattern: Arc, - values: Vec, + cs: CsMatrix, } impl CscMatrix { /// Create a zero CSC matrix with no explicitly stored entries. pub fn new(nrows: usize, ncols: usize) -> Self { Self { - sparsity_pattern: Arc::new(SparsityPattern::new(ncols, nrows)), - values: vec![], + cs: CsMatrix::new(ncols, nrows) } } /// The number of rows in the matrix. #[inline] pub fn nrows(&self) -> usize { - self.sparsity_pattern.minor_dim() + self.cs.pattern().minor_dim() } /// The number of columns in the matrix. #[inline] pub fn ncols(&self) -> usize { - self.sparsity_pattern.major_dim() + self.cs.pattern().major_dim() } /// The number of non-zeros in the matrix. @@ -53,31 +50,31 @@ impl CscMatrix { /// be zero. Corresponds to the number of entries in the sparsity pattern. #[inline] pub fn nnz(&self) -> usize { - self.sparsity_pattern.nnz() + self.pattern().nnz() } /// The column offsets defining part of the CSC format. #[inline] pub fn col_offsets(&self) -> &[usize] { - self.sparsity_pattern.major_offsets() + self.pattern().major_offsets() } /// The row indices defining part of the CSC format. #[inline] pub fn row_indices(&self) -> &[usize] { - self.sparsity_pattern.minor_indices() + self.pattern().minor_indices() } /// The non-zero values defining part of the CSC format. #[inline] pub fn values(&self) -> &[T] { - &self.values + self.cs.values() } /// Mutable access to the non-zero values. #[inline] pub fn values_mut(&mut self) -> &mut [T] { - &mut self.values + self.cs.values_mut() } /// Try to construct a CSC matrix from raw CSC data. @@ -109,8 +106,7 @@ impl CscMatrix { -> Result { if pattern.nnz() == values.len() { Ok(Self { - sparsity_pattern: pattern, - values, + cs: CsMatrix::from_pattern_and_values(pattern, values) }) } else { Err(SparseFormatError::from_kind_and_msg( @@ -140,8 +136,8 @@ impl CscMatrix { /// ``` pub fn triplet_iter(&self) -> CscTripletIter { CscTripletIter { - pattern_iter: self.sparsity_pattern.entries(), - values_iter: self.values.iter() + pattern_iter: self.pattern().entries(), + values_iter: self.values().iter() } } @@ -169,9 +165,10 @@ impl CscMatrix { /// assert_eq!(triplets, vec![(0, 0, 1), (2, 0, 0), (1, 1, 2), (0, 2, 4)]); /// ``` pub fn triplet_iter_mut(&mut self) -> CscTripletIterMut { + let (pattern, values) = self.cs.pattern_and_values_mut(); CscTripletIterMut { - pattern_iter: self.sparsity_pattern.entries(), - values_mut_iter: self.values.iter_mut() + pattern_iter: pattern.entries(), + values_mut_iter: values.iter_mut() } } @@ -200,54 +197,34 @@ impl CscMatrix { /// Return the column at the given column index, or `None` if out of bounds. #[inline] pub fn get_col(&self, index: usize) -> Option> { - let range = self.get_index_range(index)?; - Some(CscCol { - row_indices: &self.sparsity_pattern.minor_indices()[range.clone()], - values: &self.values[range], - nrows: self.nrows() - }) + self.cs + .get_lane(index) + .map(|lane| CscCol { lane }) } /// Mutable column access for the given column index, or `None` if out of bounds. #[inline] pub fn get_col_mut(&mut self, index: usize) -> Option> { - let range = self.get_index_range(index)?; - Some(CscColMut { - nrows: self.nrows(), - row_indices: &self.sparsity_pattern.minor_indices()[range.clone()], - values: &mut self.values[range] - }) - } - - /// Internal method for simplifying access to a column's data. - fn get_index_range(&self, col_index: usize) -> Option> { - let col_begin = *self.sparsity_pattern.major_offsets().get(col_index)?; - let col_end = *self.sparsity_pattern.major_offsets().get(col_index + 1)?; - Some(col_begin .. col_end) + self.cs + .get_lane_mut(index) + .map(|lane| CscColMut { lane }) } /// An iterator over columns in the matrix. pub fn col_iter(&self) -> CscColIter { CscColIter { - current_col_idx: 0, - matrix: self + lane_iter: CsLaneIter::new(self.pattern().as_ref(), self.values()) } } /// A mutable iterator over columns in the matrix. pub fn col_iter_mut(&mut self) -> CscColIterMut { + let (pattern, values) = self.cs.pattern_and_values_mut(); CscColIterMut { - current_col_idx: 0, - pattern: &self.sparsity_pattern, - remaining_values: self.values.as_mut_ptr() + lane_iter: CsLaneIterMut::new(pattern, values) } } - /// Returns the underlying vector containing the values for the explicitly stored entries. - pub fn take_values(self) -> Vec { - self.values - } - /// Disassembles the CSC matrix into its underlying offset, index and value arrays. /// /// If the matrix contains the sole reference to the sparsity pattern, @@ -274,19 +251,7 @@ impl CscMatrix { /// assert_eq!(values2, values); /// ``` pub fn disassemble(self) -> (Vec, Vec, Vec) { - // Take an Arc to the pattern, which might be the sole reference to the data after - // taking the values. This is important, because it might let us avoid cloning the data - // further below. - let pattern = self.sparsity_pattern; - let values = self.values; - - // Try to take the pattern out of the `Arc` if possible, - // otherwise clone the pattern. - let owned_pattern = Arc::try_unwrap(pattern) - .unwrap_or_else(|arc| SparsityPattern::clone(&*arc)); - let (offsets, indices) = owned_pattern.disassemble(); - - (offsets, indices, values) + self.cs.disassemble() } /// Returns the underlying sparsity pattern. @@ -295,15 +260,14 @@ impl CscMatrix { /// the same sparsity pattern for multiple matrices without storing the same pattern multiple /// times in memory. pub fn pattern(&self) -> &Arc { - &self.sparsity_pattern + self.cs.pattern() } /// Reinterprets the CSC matrix as its transpose represented by a CSR matrix. /// /// This operation does not touch the CSC data, and is effectively a no-op. pub fn transpose_as_csr(self) -> CsrMatrix { - let pattern = self.sparsity_pattern; - let values = self.values; + let (pattern, values) = self.cs.take_pattern_and_values(); CsrMatrix::try_from_pattern_and_values(pattern, values).unwrap() } } @@ -422,9 +386,7 @@ impl<'a, T> Iterator for CscTripletIterMut<'a, T> { /// An immutable representation of a column in a CSC matrix. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CscCol<'a, T> { - nrows: usize, - row_indices: &'a [usize], - values: &'a [T], + lane: CsLane<'a, T> } /// A mutable representation of a column in a CSC matrix. @@ -433,9 +395,7 @@ pub struct CscCol<'a, T> { /// to the column cannot be modified. #[derive(Debug, PartialEq, Eq)] pub struct CscColMut<'a, T> { - nrows: usize, - row_indices: &'a [usize], - values: &'a mut [T] + lane: CsLaneMut<'a, T> } /// Implement the methods common to both CscCol and CscColMut @@ -445,25 +405,25 @@ macro_rules! impl_csc_col_common_methods { /// The number of global rows in the column. #[inline] pub fn nrows(&self) -> usize { - self.nrows + self.lane.minor_dim } /// The number of non-zeros in this column. #[inline] pub fn nnz(&self) -> usize { - self.row_indices.len() + self.lane.minor_indices.len() } /// The row indices corresponding to explicitly stored entries in this column. #[inline] pub fn row_indices(&self) -> &[usize] { - self.row_indices + self.lane.minor_indices } /// The values corresponding to explicitly stored entries in this column. #[inline] pub fn values(&self) -> &[T] { - self.values + self.lane.values } } @@ -480,8 +440,8 @@ macro_rules! impl_csc_col_common_methods { pub fn get(&self, global_row_index: usize) -> Option { let local_index = self.row_indices().binary_search(&global_row_index); if let Ok(local_index) = local_index { - Some(self.values[local_index].clone()) - } else if global_row_index < self.nrows { + Some(self.values()[local_index].clone()) + } else if global_row_index < self.lane.minor_dim { Some(T::zero()) } else { None @@ -497,7 +457,7 @@ impl_csc_col_common_methods!(CscColMut<'a, T>); impl<'a, T> CscColMut<'a, T> { /// Mutable access to the values corresponding to explicitly stored entries in this column. pub fn values_mut(&mut self) -> &mut [T] { - self.values + self.lane.values } /// Provides simultaneous access to row indices and mutable values corresponding to the @@ -506,32 +466,28 @@ impl<'a, T> CscColMut<'a, T> { /// This method primarily facilitates low-level access for methods that process data stored /// in CSC format directly. pub fn rows_and_values_mut(&mut self) -> (&[usize], &mut [T]) { - (self.row_indices, self.values) + (self.lane.minor_indices, self.lane.values) } } /// Column iterator for [CscMatrix](struct.CscMatrix.html). pub struct CscColIter<'a, T> { - // The index of the row that will be returned on the next - current_col_idx: usize, - matrix: &'a CscMatrix + lane_iter: CsLaneIter<'a, T> } impl<'a, T> Iterator for CscColIter<'a, T> { type Item = CscCol<'a, T>; fn next(&mut self) -> Option { - let col = self.matrix.get_col(self.current_col_idx); - self.current_col_idx += 1; - col + self.lane_iter + .next() + .map(|lane| CscCol { lane }) } } /// Mutable column iterator for [CscMatrix](struct.CscMatrix.html). pub struct CscColIterMut<'a, T> { - current_col_idx: usize, - pattern: &'a SparsityPattern, - remaining_values: *mut T, + lane_iter: CsLaneIterMut<'a, T> } impl<'a, T> Iterator for CscColIterMut<'a, T> @@ -541,27 +497,8 @@ where type Item = CscColMut<'a, T>; fn next(&mut self) -> Option { - let lane = self.pattern.get_lane(self.current_col_idx); - let nrows = self.pattern.minor_dim(); - - if let Some(row_indices) = lane { - let count = row_indices.len(); - - // Note: I can't think of any way to construct this iterator without unsafe. - let values_in_row; - unsafe { - values_in_row = &mut *slice_from_raw_parts_mut(self.remaining_values, count); - self.remaining_values = self.remaining_values.add(count); - } - self.current_col_idx += 1; - - Some(CscColMut { - nrows, - row_indices, - values: values_in_row - }) - } else { - None - } + self.lane_iter + .next() + .map(|lane| CscColMut { lane }) } } \ No newline at end of file diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index 20db241b..fa10a69d 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -2,14 +2,13 @@ use crate::{SparseFormatError, SparseFormatErrorKind, SparseEntry, SparseEntryMut}; use crate::pattern::{SparsityPattern, SparsityPatternFormatError, SparsityPatternIter}; use crate::csc::CscMatrix; +use crate::cs::{CsMatrix, get_entry_from_slices, get_mut_entry_from_slices, CsLaneIterMut, CsLaneIter, CsLane, CsLaneMut}; use nalgebra::Scalar; use num_traits::Zero; use std::sync::Arc; use std::slice::{IterMut, Iter}; -use std::ops::Range; -use std::ptr::slice_from_raw_parts_mut; /// A CSR representation of a sparse matrix. /// @@ -21,29 +20,27 @@ use std::ptr::slice_from_raw_parts_mut; #[derive(Debug, Clone, PartialEq, Eq)] pub struct CsrMatrix { // Rows are major, cols are minor in the sparsity pattern - sparsity_pattern: Arc, - values: Vec, + cs: CsMatrix, } impl CsrMatrix { /// Create a zero CSR matrix with no explicitly stored entries. pub fn new(nrows: usize, ncols: usize) -> Self { Self { - sparsity_pattern: Arc::new(SparsityPattern::new(nrows, ncols)), - values: vec![], + cs: CsMatrix::new(nrows, ncols) } } /// The number of rows in the matrix. #[inline] pub fn nrows(&self) -> usize { - self.sparsity_pattern.major_dim() + self.cs.pattern().major_dim() } /// The number of columns in the matrix. #[inline] pub fn ncols(&self) -> usize { - self.sparsity_pattern.minor_dim() + self.cs.pattern().minor_dim() } /// The number of non-zeros in the matrix. @@ -53,31 +50,33 @@ impl CsrMatrix { /// be zero. Corresponds to the number of entries in the sparsity pattern. #[inline] pub fn nnz(&self) -> usize { - self.sparsity_pattern.nnz() + self.cs.pattern().nnz() } /// The row offsets defining part of the CSR format. #[inline] pub fn row_offsets(&self) -> &[usize] { - self.sparsity_pattern.major_offsets() + let (offsets, _, _) = self.cs.cs_data(); + offsets } /// The column indices defining part of the CSR format. #[inline] pub fn col_indices(&self) -> &[usize] { - self.sparsity_pattern.minor_indices() + let (_, indices, _) = self.cs.cs_data(); + indices } /// The non-zero values defining part of the CSR format. #[inline] pub fn values(&self) -> &[T] { - &self.values + self.cs.values() } /// Mutable access to the non-zero values. #[inline] pub fn values_mut(&mut self) -> &mut [T] { - &mut self.values + self.cs.values_mut() } /// Try to construct a CSR matrix from raw CSR data. @@ -109,8 +108,7 @@ impl CsrMatrix { -> Result { if pattern.nnz() == values.len() { Ok(Self { - sparsity_pattern: pattern, - values, + cs: CsMatrix::from_pattern_and_values(pattern, values) }) } else { Err(SparseFormatError::from_kind_and_msg( @@ -119,7 +117,6 @@ impl CsrMatrix { } } - /// An iterator over non-zero triplets (i, j, v). /// /// The iteration happens in row-major fashion, meaning that i increases monotonically, @@ -140,8 +137,8 @@ impl CsrMatrix { /// ``` pub fn triplet_iter(&self) -> CsrTripletIter { CsrTripletIter { - pattern_iter: self.sparsity_pattern.entries(), - values_iter: self.values.iter() + pattern_iter: self.pattern().entries(), + values_iter: self.values().iter() } } @@ -169,9 +166,10 @@ impl CsrMatrix { /// assert_eq!(triplets, vec![(0, 0, 1), (0, 2, 2), (1, 1, 3), (2, 0, 0)]); /// ``` pub fn triplet_iter_mut(&mut self) -> CsrTripletIterMut { + let (pattern, values) = self.cs.pattern_and_values_mut(); CsrTripletIterMut { - pattern_iter: self.sparsity_pattern.entries(), - values_mut_iter: self.values.iter_mut() + pattern_iter: pattern.entries(), + values_mut_iter: values.iter_mut() } } @@ -200,54 +198,34 @@ impl CsrMatrix { /// Return the row at the given row index, or `None` if out of bounds. #[inline] pub fn get_row(&self, index: usize) -> Option> { - let range = self.get_index_range(index)?; - Some(CsrRow { - col_indices: &self.sparsity_pattern.minor_indices()[range.clone()], - values: &self.values[range], - ncols: self.ncols() - }) + self.cs + .get_lane(index) + .map(|lane| CsrRow { lane }) } /// Mutable row access for the given row index, or `None` if out of bounds. #[inline] pub fn get_row_mut(&mut self, index: usize) -> Option> { - let range = self.get_index_range(index)?; - Some(CsrRowMut { - ncols: self.ncols(), - col_indices: &self.sparsity_pattern.minor_indices()[range.clone()], - values: &mut self.values[range] - }) - } - - /// Internal method for simplifying access to a row's data. - fn get_index_range(&self, row_index: usize) -> Option> { - let row_begin = *self.sparsity_pattern.major_offsets().get(row_index)?; - let row_end = *self.sparsity_pattern.major_offsets().get(row_index + 1)?; - Some(row_begin .. row_end) + self.cs + .get_lane_mut(index) + .map(|lane| CsrRowMut { lane }) } /// An iterator over rows in the matrix. pub fn row_iter(&self) -> CsrRowIter { CsrRowIter { - current_row_idx: 0, - matrix: self + lane_iter: CsLaneIter::new(self.pattern().as_ref(), self.values()) } } /// A mutable iterator over rows in the matrix. pub fn row_iter_mut(&mut self) -> CsrRowIterMut { + let (pattern, values) = self.cs.pattern_and_values_mut(); CsrRowIterMut { - current_row_idx: 0, - pattern: &self.sparsity_pattern, - remaining_values: self.values.as_mut_ptr() + lane_iter: CsLaneIterMut::new(pattern, values), } } - /// Returns the underlying vector containing the values for the explicitly stored entries. - pub fn take_values(self) -> Vec { - self.values - } - /// Disassembles the CSR matrix into its underlying offset, index and value arrays. /// /// If the matrix contains the sole reference to the sparsity pattern, @@ -274,19 +252,7 @@ impl CsrMatrix { /// assert_eq!(values2, values); /// ``` pub fn disassemble(self) -> (Vec, Vec, Vec) { - // Take an Arc to the pattern, which might be the sole reference to the data after - // taking the values. This is important, because it might let us avoid cloning the data - // further below. - let pattern = self.sparsity_pattern; - let values = self.values; - - // Try to take the pattern out of the `Arc` if possible, - // otherwise clone the pattern. - let owned_pattern = Arc::try_unwrap(pattern) - .unwrap_or_else(|arc| SparsityPattern::clone(&*arc)); - let (offsets, indices) = owned_pattern.disassemble(); - - (offsets, indices, values) + self.cs.disassemble() } /// Returns the underlying sparsity pattern. @@ -295,15 +261,14 @@ impl CsrMatrix { /// the same sparsity pattern for multiple matrices without storing the same pattern multiple /// times in memory. pub fn pattern(&self) -> &Arc { - &self.sparsity_pattern + self.cs.pattern() } /// Reinterprets the CSR matrix as its transpose represented by a CSC matrix. /// /// This operation does not touch the CSR data, and is effectively a no-op. pub fn transpose_as_csc(self) -> CscMatrix { - let pattern = self.sparsity_pattern; - let values = self.values; + let (pattern, values) = self.cs.take_pattern_and_values(); CscMatrix::try_from_pattern_and_values(pattern, values).unwrap() } @@ -312,10 +277,7 @@ impl CsrMatrix { /// Each call to this function incurs the cost of a binary search among the explicitly /// stored column entries for the given row. pub fn get_entry(&self, row_index: usize, col_index: usize) -> Option> { - let row_range = self.get_index_range(row_index)?; - let col_indices = &self.col_indices()[row_range.clone()]; - let values = &self.values()[row_range]; - get_entry_from_slices(self.ncols(), col_indices, values, col_index) + self.cs.get_entry(row_index, col_index) } /// Returns a mutable entry for the given row/col indices, or `None` if the indices are out @@ -325,12 +287,7 @@ impl CsrMatrix { /// stored column entries for the given row. pub fn get_entry_mut(&mut self, row_index: usize, col_index: usize) -> Option> { - let row_range = self.get_index_range(row_index)?; - let ncols = self.ncols(); - let (_, col_indices, values) = self.csr_data_mut(); - let col_indices = &col_indices[row_range.clone()]; - let values = &mut values[row_range]; - get_mut_entry_from_slices(ncols, col_indices, values, col_index) + self.cs.get_entry_mut(row_index, col_index) } /// Returns an entry for the given row/col indices. @@ -361,14 +318,13 @@ impl CsrMatrix { /// Returns a triplet of slices `(row_offsets, col_indices, values)` that make up the CSR data. pub fn csr_data(&self) -> (&[usize], &[usize], &[T]) { - (self.row_offsets(), self.col_indices(), self.values()) + self.cs.cs_data() } /// Returns a triplet of slices `(row_offsets, col_indices, values)` that make up the CSR data, /// where the `values` array is mutable. pub fn csr_data_mut(&mut self) -> (&[usize], &[usize], &mut [T]) { - let pattern = self.sparsity_pattern.as_ref(); - (pattern.major_offsets(), pattern.minor_indices(), &mut self.values) + self.cs.cs_data_mut() } } @@ -460,9 +416,7 @@ impl<'a, T> Iterator for CsrTripletIterMut<'a, T> { /// An immutable representation of a row in a CSR matrix. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CsrRow<'a, T> { - ncols: usize, - col_indices: &'a [usize], - values: &'a [T], + lane: CsLane<'a, T> } /// A mutable representation of a row in a CSR matrix. @@ -471,9 +425,7 @@ pub struct CsrRow<'a, T> { /// to the row cannot be modified. #[derive(Debug, PartialEq, Eq)] pub struct CsrRowMut<'a, T> { - ncols: usize, - col_indices: &'a [usize], - values: &'a mut [T] + lane: CsLaneMut<'a, T> } /// Implement the methods common to both CsrRow and CsrRowMut @@ -483,25 +435,25 @@ macro_rules! impl_csr_row_common_methods { /// The number of global columns in the row. #[inline] pub fn ncols(&self) -> usize { - self.ncols + self.lane.minor_dim } /// The number of non-zeros in this row. #[inline] pub fn nnz(&self) -> usize { - self.col_indices.len() + self.lane.minor_indices.len() } /// The column indices corresponding to explicitly stored entries in this row. #[inline] pub fn col_indices(&self) -> &[usize] { - self.col_indices + self.lane.minor_indices } /// The values corresponding to explicitly stored entries in this row. #[inline] pub fn values(&self) -> &[T] { - self.values + self.lane.values } /// Returns an entry for the given global column index. @@ -509,47 +461,23 @@ macro_rules! impl_csr_row_common_methods { /// Each call to this function incurs the cost of a binary search among the explicitly /// stored column entries. pub fn get_entry(&self, global_col_index: usize) -> Option> { - get_entry_from_slices(self.ncols, self.col_indices, self.values, global_col_index) + get_entry_from_slices( + self.lane.minor_dim, + self.lane.minor_indices, + self.lane.values, + global_col_index) } } } } -fn get_entry_from_slices<'a, T>(ncols: usize, - col_indices: &'a [usize], - values: &'a [T], - global_col_index: usize) -> Option> { - let local_index = col_indices.binary_search(&global_col_index); - if let Ok(local_index) = local_index { - Some(SparseEntry::NonZero(&values[local_index])) - } else if global_col_index < ncols { - Some(SparseEntry::Zero) - } else { - None - } -} - -fn get_mut_entry_from_slices<'a, T>(ncols: usize, - col_indices: &'a [usize], - values: &'a mut [T], - global_col_index: usize) -> Option> { - let local_index = col_indices.binary_search(&global_col_index); - if let Ok(local_index) = local_index { - Some(SparseEntryMut::NonZero(&mut values[local_index])) - } else if global_col_index < ncols { - Some(SparseEntryMut::Zero) - } else { - None - } -} - impl_csr_row_common_methods!(CsrRow<'a, T>); impl_csr_row_common_methods!(CsrRowMut<'a, T>); impl<'a, T> CsrRowMut<'a, T> { /// Mutable access to the values corresponding to explicitly stored entries in this row. pub fn values_mut(&mut self) -> &mut [T] { - self.values + self.lane.values } /// Provides simultaneous access to column indices and mutable values corresponding to the @@ -558,37 +486,36 @@ impl<'a, T> CsrRowMut<'a, T> { /// This method primarily facilitates low-level access for methods that process data stored /// in CSR format directly. pub fn cols_and_values_mut(&mut self) -> (&[usize], &mut [T]) { - (self.col_indices, self.values) + (self.lane.minor_indices, self.lane.values) } /// Returns a mutable entry for the given global column index. pub fn get_entry_mut(&mut self, global_col_index: usize) -> Option> { - get_mut_entry_from_slices(self.ncols, self.col_indices, self.values, global_col_index) + get_mut_entry_from_slices(self.lane.minor_dim, + self.lane.minor_indices, + self.lane.values, + global_col_index) } } /// Row iterator for [CsrMatrix](struct.CsrMatrix.html). pub struct CsrRowIter<'a, T> { - // The index of the row that will be returned on the next - current_row_idx: usize, - matrix: &'a CsrMatrix + lane_iter: CsLaneIter<'a, T> } impl<'a, T> Iterator for CsrRowIter<'a, T> { type Item = CsrRow<'a, T>; fn next(&mut self) -> Option { - let row = self.matrix.get_row(self.current_row_idx); - self.current_row_idx += 1; - row + self.lane_iter + .next() + .map(|lane| CsrRow { lane }) } } /// Mutable row iterator for [CsrMatrix](struct.CsrMatrix.html). pub struct CsrRowIterMut<'a, T> { - current_row_idx: usize, - pattern: &'a SparsityPattern, - remaining_values: *mut T, + lane_iter: CsLaneIterMut<'a, T> } impl<'a, T> Iterator for CsrRowIterMut<'a, T> @@ -598,27 +525,8 @@ where type Item = CsrRowMut<'a, T>; fn next(&mut self) -> Option { - let lane = self.pattern.get_lane(self.current_row_idx); - let ncols = self.pattern.minor_dim(); - - if let Some(col_indices) = lane { - let count = col_indices.len(); - - // Note: I can't think of any way to construct this iterator without unsafe. - let values_in_row; - unsafe { - values_in_row = &mut *slice_from_raw_parts_mut(self.remaining_values, count); - self.remaining_values = self.remaining_values.add(count); - } - self.current_row_idx += 1; - - Some(CsrRowMut { - ncols, - col_indices, - values: values_in_row - }) - } else { - None - } + self.lane_iter + .next() + .map(|lane| CsrRowMut { lane }) } } \ No newline at end of file diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index 536f4b75..1812a60a 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -90,6 +90,8 @@ pub mod pattern; pub mod ops; pub mod convert; +mod cs; + #[cfg(feature = "proptest-support")] pub mod proptest; From e261e7c388bd8744a88f1c37b8900b4998fad851 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 22 Dec 2020 11:01:50 +0100 Subject: [PATCH 051/183] Remove use of unsafe for CsLaneIterMut --- nalgebra-sparse/src/cs.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/nalgebra-sparse/src/cs.rs b/nalgebra-sparse/src/cs.rs index 7a38055b..09320da7 100644 --- a/nalgebra-sparse/src/cs.rs +++ b/nalgebra-sparse/src/cs.rs @@ -3,7 +3,7 @@ use crate::{SparseEntry, SparseEntryMut}; use std::sync::Arc; use std::ops::Range; -use std::ptr::slice_from_raw_parts_mut; +use std::mem::replace; /// An abstract compressed matrix. /// @@ -238,7 +238,7 @@ pub struct CsLaneIterMut<'a, T> { // The index of the lane that will be returned on the next iteration current_lane_idx: usize, pattern: &'a SparsityPattern, - remaining_values: *mut T, + remaining_values: &'a mut [T], } impl<'a, T> CsLaneIterMut<'a, T> { @@ -246,7 +246,7 @@ impl<'a, T> CsLaneIterMut<'a, T> { Self { current_lane_idx: 0, pattern, - remaining_values: values.as_mut_ptr() + remaining_values: values } } } @@ -264,12 +264,9 @@ impl<'a, T> Iterator for CsLaneIterMut<'a, T> if let Some(minor_indices) = lane { let count = minor_indices.len(); - // Note: I can't think of any way to construct this iterator without unsafe. - let values_in_lane; - unsafe { - values_in_lane = &mut *slice_from_raw_parts_mut(self.remaining_values, count); - self.remaining_values = self.remaining_values.add(count); - } + let remaining = replace(&mut self.remaining_values, &mut []); + let (values_in_lane, remaining) = remaining.split_at_mut(count); + self.remaining_values = remaining; self.current_lane_idx += 1; Some(CsLaneMut { From 6a1d12705ff8eee391160f8df879919bea0421d8 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 22 Dec 2020 11:14:53 +0100 Subject: [PATCH 052/183] Remove old .get() API for CscMatrix in favor of entry API This essentially makes the API of CscMatrix analogous to that of CsrMatrix. --- nalgebra-sparse/src/csc.rs | 102 +++++++++++++++++++++++-------------- 1 file changed, 65 insertions(+), 37 deletions(-) diff --git a/nalgebra-sparse/src/csc.rs b/nalgebra-sparse/src/csc.rs index f39483a2..d5d80ee2 100644 --- a/nalgebra-sparse/src/csc.rs +++ b/nalgebra-sparse/src/csc.rs @@ -1,9 +1,10 @@ //! An implementation of the CSC sparse matrix format. -use crate::{SparseFormatError, SparseFormatErrorKind}; +use crate::{SparseFormatError, SparseFormatErrorKind, SparseEntry, SparseEntryMut}; use crate::pattern::{SparsityPattern, SparsityPatternFormatError, SparsityPatternIter}; use crate::csr::CsrMatrix; -use crate::cs::{CsMatrix, CsLane, CsLaneMut, CsLaneIter, CsLaneIterMut}; +use crate::cs::{CsMatrix, CsLane, CsLaneMut, CsLaneIter, CsLaneIterMut, + get_entry_from_slices, get_mut_entry_from_slices}; use std::sync::Arc; use std::slice::{IterMut, Iter}; @@ -270,31 +271,60 @@ impl CscMatrix { let (pattern, values) = self.cs.take_pattern_and_values(); CsrMatrix::try_from_pattern_and_values(pattern, values).unwrap() } -} -impl CscMatrix { - /// Return the value in the matrix at the given global row/col indices, or `None` if out of - /// bounds. - /// - /// If the indices are in bounds, but no explicitly stored entry is associated with it, - /// `T::zero()` is returned. Note that this method offers no way of distinguishing - /// explicitly stored zero entries from zero values that are only implicitly represented. + /// Returns an entry for the given row/col indices, or `None` if the indices are out of bounds. /// /// Each call to this function incurs the cost of a binary search among the explicitly - /// stored column entries for the given row. - #[inline] - pub fn get(&self, row_index: usize, col_index: usize) -> Option { - self.get_col(row_index)?.get(col_index) + /// stored row entries for the given column. + pub fn get_entry(&self, row_index: usize, col_index: usize) -> Option> { + self.cs.get_entry(col_index, row_index) } - /// Same as `get`, but panics if indices are out of bounds. + /// Returns a mutable entry for the given row/col indices, or `None` if the indices are out + /// of bounds. + /// + /// Each call to this function incurs the cost of a binary search among the explicitly + /// stored row entries for the given column. + pub fn get_entry_mut(&mut self, row_index: usize, col_index: usize) + -> Option> { + self.cs.get_entry_mut(col_index, row_index) + } + + /// Returns an entry for the given row/col indices. + /// + /// Same as `get_entry`, except that it directly panics upon encountering row/col indices + /// out of bounds. /// /// Panics /// ------ - /// Panics if either index is out of bounds. - #[inline] - pub fn index(&self, row_index: usize, col_index: usize) -> T { - self.get(row_index, col_index).unwrap() + /// Panics if `row_index` or `col_index` is out of bounds. + pub fn index_entry(&self, row_index: usize, col_index: usize) -> SparseEntry { + self.get_entry(row_index, col_index) + .expect("Out of bounds matrix indices encountered") + } + + /// Returns a mutable entry for the given row/col indices. + /// + /// Same as `get_entry_mut`, except that it directly panics upon encountering row/col indices + /// out of bounds. + /// + /// Panics + /// ------ + /// Panics if `row_index` or `col_index` is out of bounds. + pub fn index_entry_mut(&mut self, row_index: usize, col_index: usize) -> SparseEntryMut { + self.get_entry_mut(row_index, col_index) + .expect("Out of bounds matrix indices encountered") + } + + /// Returns a triplet of slices `(row_offsets, col_indices, values)` that make up the CSC data. + pub fn csc_data(&self) -> (&[usize], &[usize], &[T]) { + self.cs.cs_data() + } + + /// Returns a triplet of slices `(row_offsets, col_indices, values)` that make up the CSC data, + /// where the `values` array is mutable. + pub fn csc_data_mut(&mut self) -> (&[usize], &[usize], &mut [T]) { + self.cs.cs_data_mut() } } @@ -425,27 +455,17 @@ macro_rules! impl_csc_col_common_methods { pub fn values(&self) -> &[T] { self.lane.values } - } - impl<'a, T: Clone + Zero> $name { - /// Return the value in the matrix at the given global row index, or `None` if out of - /// bounds. - /// - /// If the index is in bounds, but no explicitly stored entry is associated with it, - /// `T::zero()` is returned. Note that this method offers no way of distinguishing - /// explicitly stored zero entries from zero values that are only implicitly represented. + /// Returns an entry for the given global row index. /// /// Each call to this function incurs the cost of a binary search among the explicitly - /// stored row entries for the current column. - pub fn get(&self, global_row_index: usize) -> Option { - let local_index = self.row_indices().binary_search(&global_row_index); - if let Ok(local_index) = local_index { - Some(self.values()[local_index].clone()) - } else if global_row_index < self.lane.minor_dim { - Some(T::zero()) - } else { - None - } + /// stored row entries. + pub fn get_entry(&self, global_row_index: usize) -> Option> { + get_entry_from_slices( + self.lane.minor_dim, + self.lane.minor_indices, + self.lane.values, + global_row_index) } } } @@ -468,6 +488,14 @@ impl<'a, T> CscColMut<'a, T> { pub fn rows_and_values_mut(&mut self) -> (&[usize], &mut [T]) { (self.lane.minor_indices, self.lane.values) } + + /// Returns a mutable entry for the given global row index. + pub fn get_entry_mut(&mut self, global_row_index: usize) -> Option> { + get_mut_entry_from_slices(self.lane.minor_dim, + self.lane.minor_indices, + self.lane.values, + global_row_index) + } } /// Column iterator for [CscMatrix](struct.CscMatrix.html). From dbdf5567fccf3dc84eb514cbf3536120a3826b70 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Wed, 30 Dec 2020 16:09:46 +0100 Subject: [PATCH 053/183] Implement arithmetic operations for CSC matrices --- nalgebra-sparse/src/cs.rs | 81 +++++- nalgebra-sparse/src/csc.rs | 28 +- nalgebra-sparse/src/csr.rs | 31 +-- nalgebra-sparse/src/lib.rs | 2 +- nalgebra-sparse/src/ops/impl_std_ops.rs | 173 ++++++------ nalgebra-sparse/src/ops/mod.rs | 11 + nalgebra-sparse/src/ops/serial/cs.rs | 194 +++++++++++++ nalgebra-sparse/src/ops/serial/csc.rs | 92 +++++++ nalgebra-sparse/src/ops/serial/csr.rs | 149 +--------- nalgebra-sparse/src/ops/serial/mod.rs | 3 + nalgebra-sparse/tests/common/mod.rs | 4 +- nalgebra-sparse/tests/unit_tests/ops.rs | 345 +++++++++++++++++++++++- 12 files changed, 838 insertions(+), 275 deletions(-) create mode 100644 nalgebra-sparse/src/ops/serial/cs.rs create mode 100644 nalgebra-sparse/src/ops/serial/csc.rs diff --git a/nalgebra-sparse/src/cs.rs b/nalgebra-sparse/src/cs.rs index 09320da7..edcb7fa3 100644 --- a/nalgebra-sparse/src/cs.rs +++ b/nalgebra-sparse/src/cs.rs @@ -144,9 +144,19 @@ impl CsMatrix { values: &mut values[range] }) } + + #[inline] + pub fn lane_iter(&self) -> CsLaneIter { + CsLaneIter::new(self.pattern().as_ref(), self.values()) + } + + #[inline] + pub fn lane_iter_mut(&mut self) -> CsLaneIterMut { + CsLaneIterMut::new(self.sparsity_pattern.as_ref(), &mut self.values) + } } -pub fn get_entry_from_slices<'a, T>( +fn get_entry_from_slices<'a, T>( minor_dim: usize, minor_indices: &'a [usize], values: &'a [T], @@ -161,7 +171,7 @@ pub fn get_entry_from_slices<'a, T>( } } -pub fn get_mut_entry_from_slices<'a, T>( +fn get_mut_entry_from_slices<'a, T>( minor_dim: usize, minor_indices: &'a [usize], values: &'a mut [T], @@ -178,16 +188,16 @@ pub fn get_mut_entry_from_slices<'a, T>( #[derive(Debug, Clone, PartialEq, Eq)] pub struct CsLane<'a, T> { - pub minor_dim: usize, - pub minor_indices: &'a [usize], - pub values: &'a [T] + minor_dim: usize, + minor_indices: &'a [usize], + values: &'a [T] } #[derive(Debug, PartialEq, Eq)] pub struct CsLaneMut<'a, T> { - pub minor_dim: usize, - pub minor_indices: &'a [usize], - pub values: &'a mut [T] + minor_dim: usize, + minor_indices: &'a [usize], + values: &'a mut [T] } pub struct CsLaneIter<'a, T> { @@ -280,4 +290,59 @@ impl<'a, T> Iterator for CsLaneIterMut<'a, T> } } +/// Implement the methods common to both CsLane and CsLaneMut. See the documentation for the +/// methods delegated here by CsrMatrix and CscMatrix members for more information. +macro_rules! impl_cs_lane_common_methods { + ($name:ty) => { + impl<'a, T> $name { + #[inline] + pub fn minor_dim(&self) -> usize { + self.minor_dim + } + #[inline] + pub fn nnz(&self) -> usize { + self.minor_indices.len() + } + + #[inline] + pub fn minor_indices(&self) -> &[usize] { + self.minor_indices + } + + #[inline] + pub fn values(&self) -> &[T] { + self.values + } + + #[inline] + pub fn get_entry(&self, global_col_index: usize) -> Option> { + get_entry_from_slices( + self.minor_dim, + self.minor_indices, + self.values, + global_col_index) + } + } + } +} + +impl_cs_lane_common_methods!(CsLane<'a, T>); +impl_cs_lane_common_methods!(CsLaneMut<'a, T>); + +impl<'a, T> CsLaneMut<'a, T> { + pub fn values_mut(&mut self) -> &mut [T] { + self.values + } + + pub fn indices_and_values_mut(&mut self) -> (&[usize], &mut [T]) { + (self.minor_indices, self.values) + } + + pub fn get_entry_mut(&mut self, global_minor_index: usize) -> Option> { + get_mut_entry_from_slices(self.minor_dim, + self.minor_indices, + self.values, + global_minor_index) + } +} \ No newline at end of file diff --git a/nalgebra-sparse/src/csc.rs b/nalgebra-sparse/src/csc.rs index d5d80ee2..7b3b8c10 100644 --- a/nalgebra-sparse/src/csc.rs +++ b/nalgebra-sparse/src/csc.rs @@ -3,8 +3,7 @@ use crate::{SparseFormatError, SparseFormatErrorKind, SparseEntry, SparseEntryMut}; use crate::pattern::{SparsityPattern, SparsityPatternFormatError, SparsityPatternIter}; use crate::csr::CsrMatrix; -use crate::cs::{CsMatrix, CsLane, CsLaneMut, CsLaneIter, CsLaneIterMut, - get_entry_from_slices, get_mut_entry_from_slices}; +use crate::cs::{CsMatrix, CsLane, CsLaneMut, CsLaneIter, CsLaneIterMut}; use std::sync::Arc; use std::slice::{IterMut, Iter}; @@ -21,7 +20,7 @@ use nalgebra::Scalar; #[derive(Debug, Clone, PartialEq, Eq)] pub struct CscMatrix { // Cols are major, rows are minor in the sparsity pattern - cs: CsMatrix, + pub(crate) cs: CsMatrix, } impl CscMatrix { @@ -435,25 +434,25 @@ macro_rules! impl_csc_col_common_methods { /// The number of global rows in the column. #[inline] pub fn nrows(&self) -> usize { - self.lane.minor_dim + self.lane.minor_dim() } /// The number of non-zeros in this column. #[inline] pub fn nnz(&self) -> usize { - self.lane.minor_indices.len() + self.lane.nnz() } /// The row indices corresponding to explicitly stored entries in this column. #[inline] pub fn row_indices(&self) -> &[usize] { - self.lane.minor_indices + self.lane.minor_indices() } /// The values corresponding to explicitly stored entries in this column. #[inline] pub fn values(&self) -> &[T] { - self.lane.values + self.lane.values() } /// Returns an entry for the given global row index. @@ -461,11 +460,7 @@ macro_rules! impl_csc_col_common_methods { /// Each call to this function incurs the cost of a binary search among the explicitly /// stored row entries. pub fn get_entry(&self, global_row_index: usize) -> Option> { - get_entry_from_slices( - self.lane.minor_dim, - self.lane.minor_indices, - self.lane.values, - global_row_index) + self.lane.get_entry(global_row_index) } } } @@ -477,7 +472,7 @@ impl_csc_col_common_methods!(CscColMut<'a, T>); impl<'a, T> CscColMut<'a, T> { /// Mutable access to the values corresponding to explicitly stored entries in this column. pub fn values_mut(&mut self) -> &mut [T] { - self.lane.values + self.lane.values_mut() } /// Provides simultaneous access to row indices and mutable values corresponding to the @@ -486,15 +481,12 @@ impl<'a, T> CscColMut<'a, T> { /// This method primarily facilitates low-level access for methods that process data stored /// in CSC format directly. pub fn rows_and_values_mut(&mut self) -> (&[usize], &mut [T]) { - (self.lane.minor_indices, self.lane.values) + self.lane.indices_and_values_mut() } /// Returns a mutable entry for the given global row index. pub fn get_entry_mut(&mut self, global_row_index: usize) -> Option> { - get_mut_entry_from_slices(self.lane.minor_dim, - self.lane.minor_indices, - self.lane.values, - global_row_index) + self.lane.get_entry_mut(global_row_index) } } diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index fa10a69d..d5b8e92e 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -2,7 +2,7 @@ use crate::{SparseFormatError, SparseFormatErrorKind, SparseEntry, SparseEntryMut}; use crate::pattern::{SparsityPattern, SparsityPatternFormatError, SparsityPatternIter}; use crate::csc::CscMatrix; -use crate::cs::{CsMatrix, get_entry_from_slices, get_mut_entry_from_slices, CsLaneIterMut, CsLaneIter, CsLane, CsLaneMut}; +use crate::cs::{CsMatrix, CsLaneIterMut, CsLaneIter, CsLane, CsLaneMut}; use nalgebra::Scalar; use num_traits::Zero; @@ -20,7 +20,7 @@ use std::slice::{IterMut, Iter}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct CsrMatrix { // Rows are major, cols are minor in the sparsity pattern - cs: CsMatrix, + pub(crate) cs: CsMatrix, } impl CsrMatrix { @@ -435,37 +435,34 @@ macro_rules! impl_csr_row_common_methods { /// The number of global columns in the row. #[inline] pub fn ncols(&self) -> usize { - self.lane.minor_dim + self.lane.minor_dim() } /// The number of non-zeros in this row. #[inline] pub fn nnz(&self) -> usize { - self.lane.minor_indices.len() + self.lane.nnz() } /// The column indices corresponding to explicitly stored entries in this row. #[inline] pub fn col_indices(&self) -> &[usize] { - self.lane.minor_indices + self.lane.minor_indices() } /// The values corresponding to explicitly stored entries in this row. #[inline] pub fn values(&self) -> &[T] { - self.lane.values + self.lane.values() } /// Returns an entry for the given global column index. /// /// Each call to this function incurs the cost of a binary search among the explicitly /// stored column entries. + #[inline] pub fn get_entry(&self, global_col_index: usize) -> Option> { - get_entry_from_slices( - self.lane.minor_dim, - self.lane.minor_indices, - self.lane.values, - global_col_index) + self.lane.get_entry(global_col_index) } } } @@ -476,8 +473,9 @@ impl_csr_row_common_methods!(CsrRowMut<'a, T>); impl<'a, T> CsrRowMut<'a, T> { /// Mutable access to the values corresponding to explicitly stored entries in this row. + #[inline] pub fn values_mut(&mut self) -> &mut [T] { - self.lane.values + self.lane.values_mut() } /// Provides simultaneous access to column indices and mutable values corresponding to the @@ -485,16 +483,15 @@ impl<'a, T> CsrRowMut<'a, T> { /// /// This method primarily facilitates low-level access for methods that process data stored /// in CSR format directly. + #[inline] pub fn cols_and_values_mut(&mut self) -> (&[usize], &mut [T]) { - (self.lane.minor_indices, self.lane.values) + self.lane.indices_and_values_mut() } /// Returns a mutable entry for the given global column index. + #[inline] pub fn get_entry_mut(&mut self, global_col_index: usize) -> Option> { - get_mut_entry_from_slices(self.lane.minor_dim, - self.lane.minor_indices, - self.lane.values, - global_col_index) + self.lane.get_entry_mut(global_col_index) } } diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index 1812a60a..c81b429b 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -90,7 +90,7 @@ pub mod pattern; pub mod ops; pub mod convert; -mod cs; +pub(crate) mod cs; #[cfg(feature = "proptest-support")] pub mod proptest; diff --git a/nalgebra-sparse/src/ops/impl_std_ops.rs b/nalgebra-sparse/src/ops/impl_std_ops.rs index f558b7c0..51d4f3bb 100644 --- a/nalgebra-sparse/src/ops/impl_std_ops.rs +++ b/nalgebra-sparse/src/ops/impl_std_ops.rs @@ -1,103 +1,112 @@ use crate::csr::CsrMatrix; +use crate::csc::CscMatrix; use std::ops::{Add, Mul}; -use crate::ops::serial::{spadd_csr_prealloc, spadd_pattern, spmm_pattern, spmm_csr_prealloc}; +use crate::ops::serial::{spadd_csr_prealloc, spadd_csc_prealloc, spadd_pattern, + spmm_pattern, spmm_csr_prealloc, spmm_csc_prealloc}; use nalgebra::{ClosedAdd, ClosedMul, Scalar}; use num_traits::{Zero, One}; use std::sync::Arc; use crate::ops::{Op}; -impl<'a, T> Add<&'a CsrMatrix> for &'a CsrMatrix -where - // TODO: Consider introducing wrapper trait for these things? It's technically a "Ring", - // I guess... - T: Scalar + ClosedAdd + ClosedMul + Zero + One -{ - type Output = CsrMatrix; - - fn add(self, rhs: &'a CsrMatrix) -> Self::Output { - let pattern = spadd_pattern(self.pattern(), rhs.pattern()); - let values = vec![T::zero(); pattern.nnz()]; - // We are giving data that is valid by definition, so it is safe to unwrap below - let mut result = CsrMatrix::try_from_pattern_and_values(Arc::new(pattern), values) - .unwrap(); - spadd_csr_prealloc(T::zero(), &mut result, T::one(), Op::NoOp(&self)).unwrap(); - spadd_csr_prealloc(T::one(), &mut result, T::one(), Op::NoOp(&rhs)).unwrap(); - result - } -} - -impl<'a, T> Add<&'a CsrMatrix> for CsrMatrix -where - T: Scalar + ClosedAdd + ClosedMul + Zero + One -{ - type Output = CsrMatrix; - - fn add(mut self, rhs: &'a CsrMatrix) -> Self::Output { - if Arc::ptr_eq(self.pattern(), rhs.pattern()) { - spadd_csr_prealloc(T::one(), &mut self, T::one(), Op::NoOp(rhs)).unwrap(); - self - } else { - &self + rhs - } - } -} - -impl<'a, T> Add> for &'a CsrMatrix - where - T: Scalar + ClosedAdd + ClosedMul + Zero + One -{ - type Output = CsrMatrix; - - fn add(self, rhs: CsrMatrix) -> Self::Output { - rhs + self - } -} - -impl Add> for CsrMatrix -where - T: Scalar + ClosedAdd + ClosedMul + Zero + One -{ - type Output = Self; - - fn add(self, rhs: CsrMatrix) -> Self::Output { - self + &rhs - } -} - -/// Helper macro for implementing matrix multiplication for different matrix types +/// Helper macro for implementing binary operators for different matrix types /// See below for usage. -macro_rules! impl_matrix_mul { - (<$($life:lifetime),*>($a_name:ident : $a:ty, $b_name:ident : $b:ty) -> $ret:ty $body:block) +macro_rules! impl_bin_op { + ($trait:ident, $method:ident, + <$($life:lifetime),*>($a:ident : $a_type:ty, $b:ident : $b_type:ty) -> $ret:ty $body:block) => { - impl<$($life,)* T> Mul<$b> for $a + impl<$($life,)* T> $trait<$b_type> for $a_type where T: Scalar + ClosedAdd + ClosedMul + Zero + One { type Output = $ret; - fn mul(self, rhs: $b) -> Self::Output { - let $a_name = self; - let $b_name = rhs; + fn $method(self, rhs: $b_type) -> Self::Output { + let $a = self; + let $b = rhs; $body } } } } -impl_matrix_mul!(<'a>(a: &'a CsrMatrix, b: &'a CsrMatrix) -> CsrMatrix { - let pattern = spmm_pattern(a.pattern(), b.pattern()); - let values = vec![T::zero(); pattern.nnz()]; - let mut result = CsrMatrix::try_from_pattern_and_values(Arc::new(pattern), values) - .unwrap(); - spmm_csr_prealloc(T::zero(), - &mut result, - T::one(), - Op::NoOp(a), - Op::NoOp(b)) - .expect("Internal error: spmm failed (please debug)."); - result -}); -impl_matrix_mul!(<'a>(a: &'a CsrMatrix, b: CsrMatrix) -> CsrMatrix { a * &b}); -impl_matrix_mul!(<'a>(a: CsrMatrix, b: &'a CsrMatrix) -> CsrMatrix { &a * b}); -impl_matrix_mul!(<>(a: CsrMatrix, b: CsrMatrix) -> CsrMatrix { &a * &b}); \ No newline at end of file +macro_rules! impl_add { + ($($args:tt)*) => { + impl_bin_op!(Add, add, $($args)*); + } +} + +/// Implements a + b for all combinations of reference and owned matrices, for +/// CsrMatrix or CscMatrix. +macro_rules! impl_spadd { + ($matrix_type:ident, $spadd_fn:ident) => { + impl_add!(<'a>(a: &'a $matrix_type, b: &'a $matrix_type) -> $matrix_type { + // If both matrices have the same pattern, then we can immediately re-use it + let pattern = if Arc::ptr_eq(a.pattern(), b.pattern()) { + Arc::clone(a.pattern()) + } else { + Arc::new(spadd_pattern(a.pattern(), b.pattern())) + }; + let values = vec![T::zero(); pattern.nnz()]; + // We are giving data that is valid by definition, so it is safe to unwrap below + let mut result = $matrix_type::try_from_pattern_and_values(pattern, values) + .unwrap(); + $spadd_fn(T::zero(), &mut result, T::one(), Op::NoOp(&a)).unwrap(); + $spadd_fn(T::one(), &mut result, T::one(), Op::NoOp(&b)).unwrap(); + result + }); + + impl_add!(<'a>(a: $matrix_type, b: &'a $matrix_type) -> $matrix_type { + let mut a = a; + if Arc::ptr_eq(a.pattern(), b.pattern()) { + $spadd_fn(T::one(), &mut a, T::one(), Op::NoOp(b)).unwrap(); + a + } else { + &a + b + } + }); + + impl_add!(<'a>(a: &'a $matrix_type, b: $matrix_type) -> $matrix_type { + b + a + }); + impl_add!(<>(a: $matrix_type, b: $matrix_type) -> $matrix_type { + a + &b + }); + } +} + +impl_spadd!(CsrMatrix, spadd_csr_prealloc); +impl_spadd!(CscMatrix, spadd_csc_prealloc); + +macro_rules! impl_mul { + ($($args:tt)*) => { + impl_bin_op!(Mul, mul, $($args)*); + } +} + +/// Implements a + b for all combinations of reference and owned matrices, for +/// CsrMatrix or CscMatrix. +macro_rules! impl_spmm { + ($matrix_type:ident, $pattern_fn:expr, $spmm_fn:expr) => { + impl_mul!(<'a>(a: &'a $matrix_type, b: &'a $matrix_type) -> $matrix_type { + let pattern = $pattern_fn(a.pattern(), b.pattern()); + let values = vec![T::zero(); pattern.nnz()]; + let mut result = $matrix_type::try_from_pattern_and_values(Arc::new(pattern), values) + .unwrap(); + $spmm_fn(T::zero(), + &mut result, + T::one(), + Op::NoOp(a), + Op::NoOp(b)) + .expect("Internal error: spmm failed (please debug)."); + result + }); + impl_mul!(<'a>(a: &'a $matrix_type, b: $matrix_type) -> $matrix_type { a * &b}); + impl_mul!(<'a>(a: $matrix_type, b: &'a $matrix_type) -> $matrix_type { &a * b}); + impl_mul!(<>(a: $matrix_type, b: $matrix_type) -> $matrix_type { &a * &b}); + } +} + +impl_spmm!(CsrMatrix, spmm_pattern, spmm_csr_prealloc); +// Need to switch order of operations for CSC pattern +impl_spmm!(CscMatrix, |a, b| spmm_pattern(b, a), spmm_csc_prealloc); \ No newline at end of file diff --git a/nalgebra-sparse/src/ops/mod.rs b/nalgebra-sparse/src/ops/mod.rs index 80fde80d..ef6a2497 100644 --- a/nalgebra-sparse/src/ops/mod.rs +++ b/nalgebra-sparse/src/ops/mod.rs @@ -48,6 +48,17 @@ impl Op { Op::NoOp(obj) | Op::Transpose(obj) => obj, } } + + /// Applies the transpose operation. + /// + /// This operation follows the usual semantics of transposition. In particular, double + /// transposition is equivalent to no transposition. + pub fn transposed(self) -> Self { + match self { + Op::NoOp(obj) => Op::Transpose(obj), + Op::Transpose(obj) => Op::NoOp(obj) + } + } } impl From for Op { diff --git a/nalgebra-sparse/src/ops/serial/cs.rs b/nalgebra-sparse/src/ops/serial/cs.rs new file mode 100644 index 00000000..22f3b524 --- /dev/null +++ b/nalgebra-sparse/src/ops/serial/cs.rs @@ -0,0 +1,194 @@ +use crate::cs::CsMatrix; +use crate::ops::Op; +use crate::ops::serial::{OperationErrorType, OperationError}; +use nalgebra::{Scalar, ClosedAdd, ClosedMul, DMatrixSliceMut, DMatrixSlice}; +use num_traits::{Zero, One}; +use crate::SparseEntryMut; +use std::sync::Arc; + +fn spmm_cs_unexpected_entry() -> OperationError { + OperationError::from_type_and_message( + OperationErrorType::InvalidPattern, + String::from("Found unexpected entry that is not present in `c`.")) +} + +/// Helper functionality for implementing CSR/CSC SPMM. +/// +/// Since CSR/CSC matrices are basically transpositions of each other, which lets us use the same +/// algorithm for the SPMM implementation. The implementation here is written in a CSR-centric +/// manner. This means that when using it for CSC, the order of the matrices needs to be +/// reversed (since transpose(AB) = transpose(B) * transpose(A) and CSC(A) = transpose(CSR(A)). +/// +/// We assume here that the matrices have already been verified to be dimensionally compatible. +pub fn spmm_cs_prealloc( + beta: T, + c: &mut CsMatrix, + alpha: T, + a: &CsMatrix, + b: &CsMatrix) + -> Result<(), OperationError> + where + T: Scalar + ClosedAdd + ClosedMul + Zero + One +{ + for i in 0 .. c.pattern().major_dim() { + let a_lane_i = a.get_lane(i).unwrap(); + let mut c_lane_i = c.get_lane_mut(i).unwrap(); + for c_ij in c_lane_i.values_mut() { + *c_ij = beta.inlined_clone() * c_ij.inlined_clone(); + } + + for (&k, a_ik) in a_lane_i.minor_indices().iter().zip(a_lane_i.values()) { + let b_lane_k = b.get_lane(k).unwrap(); + let (mut c_lane_i_cols, mut c_lane_i_values) = c_lane_i.indices_and_values_mut(); + let alpha_aik = alpha.inlined_clone() * a_ik.inlined_clone(); + for (j, b_kj) in b_lane_k.minor_indices().iter().zip(b_lane_k.values()) { + // Determine the location in C to append the value + let (c_local_idx, _) = c_lane_i_cols.iter() + .enumerate() + .find(|(_, c_col)| *c_col == j) + .ok_or_else(spmm_cs_unexpected_entry)?; + + c_lane_i_values[c_local_idx] += alpha_aik.inlined_clone() * b_kj.inlined_clone(); + c_lane_i_cols = &c_lane_i_cols[c_local_idx ..]; + c_lane_i_values = &mut c_lane_i_values[c_local_idx ..]; + } + } + } + + Ok(()) +} + +fn spadd_cs_unexpected_entry() -> OperationError { + OperationError::from_type_and_message( + OperationErrorType::InvalidPattern, + String::from("Found entry in `op(a)` that is not present in `c`.")) +} + +/// Helper functionality for implementing CSR/CSC SPADD. +pub fn spadd_cs_prealloc(beta: T, + c: &mut CsMatrix, + alpha: T, + a: Op<&CsMatrix>) + -> Result<(), OperationError> + where + T: Scalar + ClosedAdd + ClosedMul + Zero + One +{ + + if Arc::ptr_eq(&c.pattern(), &a.inner_ref().pattern()) { + // Special fast path: The two matrices have *exactly* the same sparsity pattern, + // so we only need to sum the value arrays + // TODO: Test this fast path + for (c_ij, a_ij) in c.values_mut().iter_mut().zip(a.inner_ref().values()) { + let (alpha, beta) = (alpha.inlined_clone(), beta.inlined_clone()); + *c_ij = beta * c_ij.inlined_clone() + alpha * a_ij.inlined_clone(); + } + Ok(()) + } else { + match a { + Op::NoOp(a) => { + for (mut c_lane_i, a_lane_i) in c.lane_iter_mut().zip(a.lane_iter()) { + if beta != T::one() { + for c_ij in c_lane_i.values_mut() { + *c_ij *= beta.inlined_clone(); + } + } + + let (mut c_minors, mut c_vals) = c_lane_i.indices_and_values_mut(); + let (a_minors, a_vals) = (a_lane_i.minor_indices(), a_lane_i.values()); + + for (a_col, a_val) in a_minors.iter().zip(a_vals) { + // TODO: Use exponential search instead of linear search. + // If C has substantially more entries in the row than A, then a line search + // will needlessly visit many entries in C. + let (c_idx, _) = c_minors.iter() + .enumerate() + .find(|(_, c_col)| *c_col == a_col) + .ok_or_else(spadd_cs_unexpected_entry)?; + c_vals[c_idx] += alpha.inlined_clone() * a_val.inlined_clone(); + c_minors = &c_minors[c_idx ..]; + c_vals = &mut c_vals[c_idx ..]; + } + } + } + Op::Transpose(a) => { + if beta != T::one() { + for c_ij in c.values_mut() { + *c_ij *= beta.inlined_clone(); + } + } + + for (i, a_lane_i) in a.lane_iter().enumerate() { + for (&j, a_val) in a_lane_i.minor_indices().iter().zip(a_lane_i.values()) { + let a_val = a_val.inlined_clone(); + let alpha = alpha.inlined_clone(); + match c.get_entry_mut(j, i).unwrap() { + SparseEntryMut::NonZero(c_ji) => { *c_ji += alpha * a_val } + SparseEntryMut::Zero => return Err(spadd_cs_unexpected_entry()), + } + } + } + } + } + Ok(()) + } +} + +/// Helper functionality for implementing CSR/CSC SPMM. +/// +/// The implementation essentially assumes that `a` is a CSR matrix. To use it with CSC matrices, +/// the transposed operation must be specified for the CSC matrix. +pub fn spmm_cs_dense(beta: T, + mut c: DMatrixSliceMut, + alpha: T, + a: Op<&CsMatrix>, + b: Op>) + where + T: Scalar + ClosedAdd + ClosedMul + Zero + One +{ + match a { + Op::NoOp(a) => { + for j in 0..c.ncols() { + let mut c_col_j = c.column_mut(j); + for (c_ij, a_row_i) in c_col_j.iter_mut().zip(a.lane_iter()) { + let mut dot_ij = T::zero(); + for (&k, a_ik) in a_row_i.minor_indices().iter().zip(a_row_i.values()) { + let b_contrib = + match b { + Op::NoOp(ref b) => b.index((k, j)), + Op::Transpose(ref b) => b.index((j, k)) + }; + dot_ij += a_ik.inlined_clone() * b_contrib.inlined_clone(); + } + *c_ij = beta.inlined_clone() * c_ij.inlined_clone() + alpha.inlined_clone() * dot_ij; + } + } + }, + Op::Transpose(a) => { + // In this case, we have to pre-multiply C by beta + c *= beta; + + for k in 0..a.pattern().major_dim() { + let a_row_k = a.get_lane(k).unwrap(); + for (&i, a_ki) in a_row_k.minor_indices().iter().zip(a_row_k.values()) { + let gamma_ki = alpha.inlined_clone() * a_ki.inlined_clone(); + let mut c_row_i = c.row_mut(i); + match b { + Op::NoOp(ref b) => { + let b_row_k = b.row(k); + for (c_ij, b_kj) in c_row_i.iter_mut().zip(b_row_k.iter()) { + *c_ij += gamma_ki.inlined_clone() * b_kj.inlined_clone(); + } + }, + Op::Transpose(ref b) => { + let b_col_k = b.column(k); + for (c_ij, b_jk) in c_row_i.iter_mut().zip(b_col_k.iter()) { + *c_ij += gamma_ki.inlined_clone() * b_jk.inlined_clone(); + } + }, + } + } + } + }, + } +} + diff --git a/nalgebra-sparse/src/ops/serial/csc.rs b/nalgebra-sparse/src/ops/serial/csc.rs new file mode 100644 index 00000000..584041db --- /dev/null +++ b/nalgebra-sparse/src/ops/serial/csc.rs @@ -0,0 +1,92 @@ +use crate::csc::CscMatrix; +use crate::ops::Op; +use crate::ops::serial::cs::{spmm_cs_prealloc, spmm_cs_dense, spadd_cs_prealloc}; +use crate::ops::serial::OperationError; +use nalgebra::{Scalar, ClosedAdd, ClosedMul, DMatrixSliceMut, DMatrixSlice}; +use num_traits::{Zero, One}; + +use std::borrow::Cow; + +/// Sparse-dense matrix-matrix multiplication `C <- beta * C + alpha * op(A) * op(B)`. +pub fn spmm_csc_dense<'a, T>(beta: T, + c: impl Into>, + alpha: T, + a: Op<&CscMatrix>, + b: Op>>) + where + T: Scalar + ClosedAdd + ClosedMul + Zero + One +{ + let b = b.convert(); + spmm_csc_dense_(beta, c.into(), alpha, a, b) +} + +fn spmm_csc_dense_(beta: T, + c: DMatrixSliceMut, + alpha: T, + a: Op<&CscMatrix>, + b: Op>) + where + T: Scalar + ClosedAdd + ClosedMul + Zero + One +{ + assert_compatible_spmm_dims!(c, a, b); + // Need to interpret matrix as transposed since the spmm_cs_dense function assumes CSR layout + let a = a.transposed().map_same_op(|a| &a.cs); + spmm_cs_dense(beta, c, alpha, a, b) +} + +/// Sparse matrix addition `C <- beta * C + alpha * op(A)`. +/// +/// If the pattern of `c` does not accommodate all the non-zero entries in `a`, an error is +/// returned. +pub fn spadd_csc_prealloc(beta: T, + c: &mut CscMatrix, + alpha: T, + a: Op<&CscMatrix>) + -> Result<(), OperationError> + where + T: Scalar + ClosedAdd + ClosedMul + Zero + One +{ + assert_compatible_spadd_dims!(c, a); + spadd_cs_prealloc(beta, &mut c.cs, alpha, a.map_same_op(|a| &a.cs)) +} + + +/// Sparse-sparse matrix multiplication, `C <- beta * C + alpha * op(A) * op(B)`. +pub fn spmm_csc_prealloc( + beta: T, + c: &mut CscMatrix, + alpha: T, + a: Op<&CscMatrix>, + b: Op<&CscMatrix>) + -> Result<(), OperationError> + where + T: Scalar + ClosedAdd + ClosedMul + Zero + One +{ + assert_compatible_spmm_dims!(c, a, b); + + use Op::{NoOp, Transpose}; + + match (&a, &b) { + (NoOp(ref a), NoOp(ref b)) => { + // Note: We have to reverse the order for CSC matrices + spmm_cs_prealloc(beta, &mut c.cs, alpha, &b.cs, &a.cs) + }, + _ => { + // Currently we handle transposition by explicitly precomputing transposed matrices + // and calling the operation again without transposition + let a_ref: &CscMatrix = a.inner_ref(); + let b_ref: &CscMatrix = b.inner_ref(); + let (a, b) = { + use Cow::*; + match (&a, &b) { + (NoOp(_), NoOp(_)) => unreachable!(), + (Transpose(ref a), NoOp(_)) => (Owned(a.transpose()), Borrowed(b_ref)), + (NoOp(_), Transpose(ref b)) => (Borrowed(a_ref), Owned(b.transpose())), + (Transpose(ref a), Transpose(ref b)) => (Owned(a.transpose()), Owned(b.transpose())) + } + }; + + spmm_csc_prealloc(beta, c, alpha, NoOp(a.as_ref()), NoOp(b.as_ref())) + } + } +} \ No newline at end of file diff --git a/nalgebra-sparse/src/ops/serial/csr.rs b/nalgebra-sparse/src/ops/serial/csr.rs index da975c7a..cec051d5 100644 --- a/nalgebra-sparse/src/ops/serial/csr.rs +++ b/nalgebra-sparse/src/ops/serial/csr.rs @@ -1,11 +1,10 @@ use crate::csr::CsrMatrix; use crate::ops::{Op}; -use crate::SparseEntryMut; -use crate::ops::serial::{OperationError, OperationErrorType}; +use crate::ops::serial::{OperationError}; use nalgebra::{Scalar, DMatrixSlice, ClosedAdd, ClosedMul, DMatrixSliceMut}; use num_traits::{Zero, One}; -use std::sync::Arc; use std::borrow::Cow; +use crate::ops::serial::cs::{spmm_cs_prealloc, spmm_cs_dense, spadd_cs_prealloc}; /// Sparse-dense matrix-matrix multiplication `C <- beta * C + alpha * op(A) * op(B)`. pub fn spmm_csr_dense<'a, T>(beta: T, @@ -21,7 +20,7 @@ pub fn spmm_csr_dense<'a, T>(beta: T, } fn spmm_csr_dense_(beta: T, - mut c: DMatrixSliceMut, + c: DMatrixSliceMut, alpha: T, a: Op<&CsrMatrix>, b: Op>) @@ -29,58 +28,7 @@ where T: Scalar + ClosedAdd + ClosedMul + Zero + One { assert_compatible_spmm_dims!(c, a, b); - - match a { - Op::NoOp(ref a) => { - for j in 0..c.ncols() { - let mut c_col_j = c.column_mut(j); - for (c_ij, a_row_i) in c_col_j.iter_mut().zip(a.row_iter()) { - let mut dot_ij = T::zero(); - for (&k, a_ik) in a_row_i.col_indices().iter().zip(a_row_i.values()) { - let b_contrib = - match b { - Op::NoOp(ref b) => b.index((k, j)), - Op::Transpose(ref b) => b.index((j, k)) - }; - dot_ij += a_ik.inlined_clone() * b_contrib.inlined_clone(); - } - *c_ij = beta.inlined_clone() * c_ij.inlined_clone() + alpha.inlined_clone() * dot_ij; - } - } - }, - Op::Transpose(ref a) => { - // In this case, we have to pre-multiply C by beta - c *= beta; - - for k in 0..a.nrows() { - let a_row_k = a.row(k); - for (&i, a_ki) in a_row_k.col_indices().iter().zip(a_row_k.values()) { - let gamma_ki = alpha.inlined_clone() * a_ki.inlined_clone(); - let mut c_row_i = c.row_mut(i); - match b { - Op::NoOp(ref b) => { - let b_row_k = b.row(k); - for (c_ij, b_kj) in c_row_i.iter_mut().zip(b_row_k.iter()) { - *c_ij += gamma_ki.inlined_clone() * b_kj.inlined_clone(); - } - }, - Op::Transpose(ref b) => { - let b_col_k = b.column(k); - for (c_ij, b_jk) in c_row_i.iter_mut().zip(b_col_k.iter()) { - *c_ij += gamma_ki.inlined_clone() * b_jk.inlined_clone(); - } - }, - } - } - } - }, - } -} - -fn spadd_csr_unexpected_entry() -> OperationError { - OperationError::from_type_and_message( - OperationErrorType::InvalidPattern, - String::from("Found entry in `a` that is not present in `c`.")) + spmm_cs_dense(beta, c, alpha, a.map_same_op(|a| &a.cs), b) } /// Sparse matrix addition `C <- beta * C + alpha * op(A)`. @@ -96,70 +44,7 @@ where T: Scalar + ClosedAdd + ClosedMul + Zero + One { assert_compatible_spadd_dims!(c, a); - - // TODO: Change CsrMatrix::pattern() to return `&Arc` instead of `Arc` - if Arc::ptr_eq(&c.pattern(), &a.inner_ref().pattern()) { - // Special fast path: The two matrices have *exactly* the same sparsity pattern, - // so we only need to sum the value arrays - for (c_ij, a_ij) in c.values_mut().iter_mut().zip(a.inner_ref().values()) { - let (alpha, beta) = (alpha.inlined_clone(), beta.inlined_clone()); - *c_ij = beta * c_ij.inlined_clone() + alpha * a_ij.inlined_clone(); - } - Ok(()) - } else { - match a { - Op::NoOp(a) => { - for (mut c_row_i, a_row_i) in c.row_iter_mut().zip(a.row_iter()) { - if beta != T::one() { - for c_ij in c_row_i.values_mut() { - *c_ij *= beta.inlined_clone(); - } - } - - let (mut c_cols, mut c_vals) = c_row_i.cols_and_values_mut(); - let (a_cols, a_vals) = (a_row_i.col_indices(), a_row_i.values()); - - for (a_col, a_val) in a_cols.iter().zip(a_vals) { - // TODO: Use exponential search instead of linear search. - // If C has substantially more entries in the row than A, then a line search - // will needlessly visit many entries in C. - let (c_idx, _) = c_cols.iter() - .enumerate() - .find(|(_, c_col)| *c_col == a_col) - .ok_or_else(spadd_csr_unexpected_entry)?; - c_vals[c_idx] += alpha.inlined_clone() * a_val.inlined_clone(); - c_cols = &c_cols[c_idx ..]; - c_vals = &mut c_vals[c_idx ..]; - } - } - } - Op::Transpose(a) => { - if beta != T::one() { - for c_ij in c.values_mut() { - *c_ij *= beta.inlined_clone(); - } - } - - for (i, a_row_i) in a.row_iter().enumerate() { - for (&j, a_val) in a_row_i.col_indices().iter().zip(a_row_i.values()) { - let a_val = a_val.inlined_clone(); - let alpha = alpha.inlined_clone(); - match c.index_entry_mut(j, i) { - SparseEntryMut::NonZero(c_ji) => { *c_ji += alpha * a_val } - SparseEntryMut::Zero => return Err(spadd_csr_unexpected_entry()), - } - } - } - } - } - Ok(()) - } -} - -fn spmm_csr_unexpected_entry() -> OperationError { - OperationError::from_type_and_message( - OperationErrorType::InvalidPattern, - String::from("Found unexpected entry that is not present in `c`.")) + spadd_cs_prealloc(beta, &mut c.cs, alpha, a.map_same_op(|a| &a.cs)) } /// Sparse-sparse matrix multiplication, `C <- beta * C + alpha * op(A) * op(B)`. @@ -179,29 +64,7 @@ where match (&a, &b) { (NoOp(ref a), NoOp(ref b)) => { - for (mut c_row_i, a_row_i) in c.row_iter_mut().zip(a.row_iter()) { - for c_ij in c_row_i.values_mut() { - *c_ij = beta.inlined_clone() * c_ij.inlined_clone(); - } - - for (&k, a_ik) in a_row_i.col_indices().iter().zip(a_row_i.values()) { - let b_row_k = b.row(k); - let (mut c_row_i_cols, mut c_row_i_values) = c_row_i.cols_and_values_mut(); - let alpha_aik = alpha.inlined_clone() * a_ik.inlined_clone(); - for (j, b_kj) in b_row_k.col_indices().iter().zip(b_row_k.values()) { - // Determine the location in C to append the value - let (c_local_idx, _) = c_row_i_cols.iter() - .enumerate() - .find(|(_, c_col)| *c_col == j) - .ok_or_else(spmm_csr_unexpected_entry)?; - - c_row_i_values[c_local_idx] += alpha_aik.inlined_clone() * b_kj.inlined_clone(); - c_row_i_cols = &c_row_i_cols[c_local_idx ..]; - c_row_i_values = &mut c_row_i_values[c_local_idx ..]; - } - } - } - Ok(()) + spmm_cs_prealloc(beta, &mut c.cs, alpha, &a.cs, &b.cs) }, _ => { // Currently we handle transposition by explicitly precomputing transposed matrices diff --git a/nalgebra-sparse/src/ops/serial/mod.rs b/nalgebra-sparse/src/ops/serial/mod.rs index a58ba9a3..ea6e6d2b 100644 --- a/nalgebra-sparse/src/ops/serial/mod.rs +++ b/nalgebra-sparse/src/ops/serial/mod.rs @@ -49,9 +49,12 @@ macro_rules! assert_compatible_spadd_dims { } } +mod csc; mod csr; mod pattern; +mod cs; +pub use csc::*; pub use csr::*; pub use pattern::*; diff --git a/nalgebra-sparse/tests/common/mod.rs b/nalgebra-sparse/tests/common/mod.rs index 0a6e31a1..7a559b2f 100644 --- a/nalgebra-sparse/tests/common/mod.rs +++ b/nalgebra-sparse/tests/common/mod.rs @@ -30,9 +30,9 @@ pub const PROPTEST_MAX_NNZ: usize = 40; pub const PROPTEST_I32_VALUE_STRATEGY: RangeInclusive = -5 ..= 5; pub fn csr_strategy() -> impl Strategy> { - csr(-5 ..= 5, PROPTEST_MATRIX_DIM, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ) + csr(PROPTEST_I32_VALUE_STRATEGY, PROPTEST_MATRIX_DIM, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ) } pub fn csc_strategy() -> impl Strategy> { - csc(-5 ..= 5, PROPTEST_MATRIX_DIM, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ) + csc(PROPTEST_I32_VALUE_STRATEGY, PROPTEST_MATRIX_DIM, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ) } diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index b56c583a..fddc5045 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -1,9 +1,12 @@ -use crate::common::{csr_strategy, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ, +use crate::common::{csc_strategy, csr_strategy, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ, PROPTEST_I32_VALUE_STRATEGY}; -use nalgebra_sparse::ops::serial::{spmm_csr_dense, spadd_pattern, spmm_pattern, spadd_csr_prealloc, spmm_csr_prealloc}; +use nalgebra_sparse::ops::serial::{spmm_csr_dense, spmm_csc_dense, spadd_pattern, spmm_pattern, + spadd_csr_prealloc, spadd_csc_prealloc, + spmm_csr_prealloc, spmm_csc_prealloc}; use nalgebra_sparse::ops::{Op}; use nalgebra_sparse::csr::CsrMatrix; -use nalgebra_sparse::proptest::{csr, sparsity_pattern}; +use nalgebra_sparse::csc::CscMatrix; +use nalgebra_sparse::proptest::{csc, csr, sparsity_pattern}; use nalgebra_sparse::pattern::SparsityPattern; use nalgebra::{DMatrix, Scalar, DMatrixSliceMut, DMatrixSlice}; @@ -23,6 +26,15 @@ fn dense_csr_pattern(pattern: &SparsityPattern) -> DMatrix { DMatrix::from(&boolean_csr) } +/// Represents the sparsity pattern of a CSC matrix as a dense matrix with 0/1 +fn dense_csc_pattern(pattern: &SparsityPattern) -> DMatrix { + let boolean_csc = CscMatrix::try_from_pattern_and_values( + Arc::new(pattern.clone()), + vec![1; pattern.nnz()]) + .unwrap(); + DMatrix::from(&boolean_csc) +} + #[derive(Debug)] struct SpmmCsrDenseArgs { c: DMatrix, @@ -32,6 +44,15 @@ struct SpmmCsrDenseArgs { b: Op>, } +#[derive(Debug)] +struct SpmmCscDenseArgs { + c: DMatrix, + beta: T, + alpha: T, + a: Op>, + b: Op>, +} + /// Returns matrices C, A and B with compatible dimensions such that it can be used /// in an `spmm` operation `C = beta * C + alpha * trans(A) * trans(B)`. fn spmm_csr_dense_args_strategy() -> impl Strategy> { @@ -70,6 +91,21 @@ fn spmm_csr_dense_args_strategy() -> impl Strategy> }) } +/// Returns matrices C, A and B with compatible dimensions such that it can be used +/// in an `spmm` operation `C = beta * C + alpha * trans(A) * trans(B)`. +fn spmm_csc_dense_args_strategy() -> impl Strategy> { + spmm_csr_dense_args_strategy() + .prop_map(|args| { + SpmmCscDenseArgs { + c: args.c, + beta: args.beta, + alpha: args.alpha, + a: args.a.map_same_op(|a| CscMatrix::from(&a)), + b: args.b + } + }) +} + #[derive(Debug)] struct SpaddCsrArgs { c: CsrMatrix, @@ -78,6 +114,14 @@ struct SpaddCsrArgs { a: Op>, } +#[derive(Debug)] +struct SpaddCscArgs { + c: CscMatrix, + beta: T, + alpha: T, + a: Op>, +} + fn spadd_csr_prealloc_args_strategy() -> impl Strategy> { let value_strategy = PROPTEST_I32_VALUE_STRATEGY; @@ -99,6 +143,16 @@ fn spadd_csr_prealloc_args_strategy() -> impl Strategy> }) } +fn spadd_csc_prealloc_args_strategy() -> impl Strategy> { + spadd_csr_prealloc_args_strategy() + .prop_map(|args| SpaddCscArgs { + c: CscMatrix::from(&args.c), + beta: args.beta, + alpha: args.alpha, + a: args.a.map_same_op(|a| CscMatrix::from(&a)) + }) +} + fn dense_strategy() -> impl Strategy> { matrix(PROPTEST_I32_VALUE_STRATEGY, PROPTEST_MATRIX_DIM, PROPTEST_MATRIX_DIM) } @@ -150,6 +204,15 @@ struct SpmmCsrArgs { b: Op>, } +#[derive(Debug)] +struct SpmmCscArgs { + c: CscMatrix, + beta: T, + alpha: T, + a: Op>, + b: Op>, +} + fn spmm_csr_prealloc_args_strategy() -> impl Strategy> { spmm_pattern_strategy() .prop_flat_map(|(a_pattern, b_pattern)| { @@ -181,6 +244,21 @@ fn spmm_csr_prealloc_args_strategy() -> impl Strategy> { }) } +fn spmm_csc_prealloc_args_strategy() -> impl Strategy> { + // Note: Converting from CSR is simple, but might be significantly slower than + // writing a common implementation that can be shared between CSR and CSC args + spmm_csr_prealloc_args_strategy() + .prop_map(|args| { + SpmmCscArgs { + c: CscMatrix::from(&args.c), + beta: args.beta, + alpha: args.alpha, + a: args.a.map_same_op(|a| CscMatrix::from(&a)), + b: args.b.map_same_op(|b| CscMatrix::from(&b)) + } + }) +} + /// Helper function to help us call dense GEMM with our `Op` type fn dense_gemm<'a>(beta: i32, c: impl Into>, @@ -310,7 +388,7 @@ proptest! { (a, b) in csr_strategy() .prop_flat_map(|a| { - let b = csr(PROPTEST_I32_VALUE_STRATEGY, Just(a.nrows()), Just(a.ncols()), 40); + let b = csr(PROPTEST_I32_VALUE_STRATEGY, Just(a.nrows()), Just(a.ncols()), PROPTEST_MAX_NNZ); (Just(a), b) })) { @@ -500,4 +578,263 @@ proptest! { prop_assert_eq!(&DMatrix::from(&c_ref_ref), &c_dense); prop_assert_eq!(c_ref_ref.pattern(), &c_pattern); } + + #[test] + fn spmm_csc_prealloc_test(SpmmCscArgs { c, beta, alpha, a, b } + in spmm_csc_prealloc_args_strategy() + ) { + // Test that we get the expected result by comparing to an equivalent dense operation + // (here we give in the C matrix, so the sparsity pattern is essentially fixed) + let mut c_sparse = c.clone(); + spmm_csc_prealloc(beta, &mut c_sparse, alpha, a.as_ref(), b.as_ref()).unwrap(); + + let mut c_dense = DMatrix::from(&c); + let op_a_dense = match a { + Op::NoOp(ref a) => DMatrix::from(a), + Op::Transpose(ref a) => DMatrix::from(a).transpose(), + }; + let op_b_dense = match b { + Op::NoOp(ref b) => DMatrix::from(b), + Op::Transpose(ref b) => DMatrix::from(b).transpose(), + }; + c_dense = beta * c_dense + alpha * &op_a_dense * op_b_dense; + + prop_assert_eq!(&DMatrix::from(&c_sparse), &c_dense); + } + + #[test] + fn spmm_csc_prealloc_panics_on_dim_mismatch( + (alpha, beta, c, a, b) + in (PROPTEST_I32_VALUE_STRATEGY, + PROPTEST_I32_VALUE_STRATEGY, + csc_strategy(), + op_strategy(csc_strategy()), + op_strategy(csc_strategy())) + ) { + // We refer to `A * B` as the "product" + let product_rows = match &a { + Op::NoOp(ref a) => a.nrows(), + Op::Transpose(ref a) => a.ncols(), + }; + let product_cols = match &b { + Op::NoOp(ref b) => b.ncols(), + Op::Transpose(ref b) => b.nrows(), + }; + // Determine the common dimension in the product + // from the perspective of a and b, respectively + let product_a_common = match &a { + Op::NoOp(ref a) => a.ncols(), + Op::Transpose(ref a) => a.nrows(), + }; + let product_b_common = match &b { + Op::NoOp(ref b) => b.nrows(), + Op::Transpose(ref b) => b.ncols(), + }; + + let dims_are_compatible = product_rows == c.nrows() + && product_cols == c.ncols() + && product_a_common == product_b_common; + + // If the dimensions randomly happen to be compatible, then of course we need to + // skip the test, so we assume that they are not. + prop_assume!(!dims_are_compatible); + + let result = catch_unwind(|| { + let mut spmm_result = c.clone(); + spmm_csc_prealloc(beta, &mut spmm_result, alpha, a.as_ref(), b.as_ref()).unwrap(); + }); + + prop_assert!(result.is_err(), + "The SPMM kernel executed successfully despite mismatch dimensions"); + } + + #[test] + fn csc_mul_csc( + // a and b have dimensions compatible for multiplication + (a, b) + in csc_strategy() + .prop_flat_map(|a| { + let max_nnz = PROPTEST_MAX_NNZ; + let cols = PROPTEST_MATRIX_DIM; + let b = csc(PROPTEST_I32_VALUE_STRATEGY, Just(a.ncols()), cols, max_nnz); + (Just(a), b) + }) + .prop_map(|(a, b)| { + println!("a: {} x {}, b: {} x {}", a.nrows(), a.ncols(), b.nrows(), b.ncols()); + (a, b) + })) + { + assert_eq!(a.ncols(), b.nrows()); + // We use the dense result as the ground truth for the arithmetic result + let c_dense = DMatrix::from(&a) * DMatrix::from(&b); + // However, it's not enough only to cover the dense result, we also need to verify the + // sparsity pattern. We can determine the exact sparsity pattern by using + // dense arithmetic with positive integer values and extracting positive entries. + let c_dense_pattern = dense_csc_pattern(a.pattern()) * dense_csc_pattern(b.pattern()); + let c_pattern = CscMatrix::from(&c_dense_pattern).pattern().clone(); + + // Check each combination of owned matrices and references + let c_owned_owned = a.clone() * b.clone(); + prop_assert_eq!(&DMatrix::from(&c_owned_owned), &c_dense); + prop_assert_eq!(c_owned_owned.pattern(), &c_pattern); + + let c_owned_ref = a.clone() * &b; + prop_assert_eq!(&DMatrix::from(&c_owned_ref), &c_dense); + prop_assert_eq!(c_owned_ref.pattern(), &c_pattern); + + let c_ref_owned = &a * b.clone(); + prop_assert_eq!(&DMatrix::from(&c_ref_owned), &c_dense); + prop_assert_eq!(c_ref_owned.pattern(), &c_pattern); + + let c_ref_ref = &a * &b; + prop_assert_eq!(&DMatrix::from(&c_ref_ref), &c_dense); + prop_assert_eq!(c_ref_ref.pattern(), &c_pattern); + } + + #[test] + fn spmm_csc_dense_agrees_with_dense_result( + SpmmCscDenseArgs { c, beta, alpha, a, b } + in spmm_csc_dense_args_strategy() + ) { + let mut spmm_result = c.clone(); + spmm_csc_dense(beta, &mut spmm_result, alpha, a.as_ref(), b.as_ref()); + + let mut gemm_result = c.clone(); + let a_dense = a.map_same_op(|a| DMatrix::from(&a)); + dense_gemm(beta, &mut gemm_result, alpha, a_dense.as_ref(), b.as_ref()); + + prop_assert_eq!(spmm_result, gemm_result); + } + + #[test] + fn spmm_csc_dense_panics_on_dim_mismatch( + (alpha, beta, c, a, b) + in (PROPTEST_I32_VALUE_STRATEGY, + PROPTEST_I32_VALUE_STRATEGY, + dense_strategy(), + op_strategy(csc_strategy()), + op_strategy(dense_strategy())) + ) { + // We refer to `A * B` as the "product" + let product_rows = match &a { + Op::NoOp(ref a) => a.nrows(), + Op::Transpose(ref a) => a.ncols(), + }; + let product_cols = match &b { + Op::NoOp(ref b) => b.ncols(), + Op::Transpose(ref b) => b.nrows(), + }; + // Determine the common dimension in the product + // from the perspective of a and b, respectively + let product_a_common = match &a { + Op::NoOp(ref a) => a.ncols(), + Op::Transpose(ref a) => a.nrows(), + }; + let product_b_common = match &b { + Op::NoOp(ref b) => b.nrows(), + Op::Transpose(ref b) => b.ncols() + }; + + let dims_are_compatible = product_rows == c.nrows() + && product_cols == c.ncols() + && product_a_common == product_b_common; + + // If the dimensions randomly happen to be compatible, then of course we need to + // skip the test, so we assume that they are not. + prop_assume!(!dims_are_compatible); + + let result = catch_unwind(|| { + let mut spmm_result = c.clone(); + spmm_csc_dense(beta, &mut spmm_result, alpha, a.as_ref(), b.as_ref()); + }); + + prop_assert!(result.is_err(), + "The SPMM kernel executed successfully despite mismatch dimensions"); + } + + #[test] + fn spadd_csc_prealloc_test(SpaddCscArgs { c, beta, alpha, a } in spadd_csc_prealloc_args_strategy()) { + // Test that we get the expected result by comparing to an equivalent dense operation + // (here we give in the C matrix, so the sparsity pattern is essentially fixed) + + let mut c_sparse = c.clone(); + spadd_csc_prealloc(beta, &mut c_sparse, alpha, a.as_ref()).unwrap(); + + let mut c_dense = DMatrix::from(&c); + let op_a_dense = match a { + Op::NoOp(a) => DMatrix::from(&a), + Op::Transpose(a) => DMatrix::from(&a).transpose(), + }; + c_dense = beta * c_dense + alpha * &op_a_dense; + + prop_assert_eq!(&DMatrix::from(&c_sparse), &c_dense); + } + + #[test] + fn spadd_csc_prealloc_panics_on_dim_mismatch( + (alpha, beta, c, op_a) + in (PROPTEST_I32_VALUE_STRATEGY, + PROPTEST_I32_VALUE_STRATEGY, + csc_strategy(), + op_strategy(csc_strategy())) + ) { + let op_a_rows = match &op_a { + &Op::NoOp(ref a) => a.nrows(), + &Op::Transpose(ref a) => a.ncols() + }; + let op_a_cols = match &op_a { + &Op::NoOp(ref a) => a.ncols(), + &Op::Transpose(ref a) => a.nrows() + }; + + let dims_are_compatible = c.nrows() == op_a_rows && c.ncols() == op_a_cols; + + // If the dimensions randomly happen to be compatible, then of course we need to + // skip the test, so we assume that they are not. + prop_assume!(!dims_are_compatible); + + let result = catch_unwind(|| { + let mut spmm_result = c.clone(); + spadd_csc_prealloc(beta, &mut spmm_result, alpha, op_a.as_ref()).unwrap(); + }); + + prop_assert!(result.is_err(), + "The SPMM kernel executed successfully despite mismatch dimensions"); + } + + #[test] + fn csc_add_csc( + // a and b have the same dimensions + (a, b) + in csc_strategy() + .prop_flat_map(|a| { + let b = csc(PROPTEST_I32_VALUE_STRATEGY, Just(a.nrows()), Just(a.ncols()), PROPTEST_MAX_NNZ); + (Just(a), b) + })) + { + // We use the dense result as the ground truth for the arithmetic result + let c_dense = DMatrix::from(&a) + DMatrix::from(&b); + // However, it's not enough only to cover the dense result, we also need to verify the + // sparsity pattern. We can determine the exact sparsity pattern by using + // dense arithmetic with positive integer values and extracting positive entries. + let c_dense_pattern = dense_csc_pattern(a.pattern()) + dense_csc_pattern(b.pattern()); + let c_pattern = CscMatrix::from(&c_dense_pattern).pattern().clone(); + + // Check each combination of owned matrices and references + let c_owned_owned = a.clone() + b.clone(); + prop_assert_eq!(&DMatrix::from(&c_owned_owned), &c_dense); + prop_assert_eq!(c_owned_owned.pattern(), &c_pattern); + + let c_owned_ref = a.clone() + &b; + prop_assert_eq!(&DMatrix::from(&c_owned_ref), &c_dense); + prop_assert_eq!(c_owned_ref.pattern(), &c_pattern); + + let c_ref_owned = &a + b.clone(); + prop_assert_eq!(&DMatrix::from(&c_ref_owned), &c_dense); + prop_assert_eq!(c_ref_owned.pattern(), &c_pattern); + + let c_ref_ref = &a + &b; + prop_assert_eq!(&DMatrix::from(&c_ref_ref), &c_dense); + prop_assert_eq!(c_ref_ref.pattern(), &c_pattern); + } } \ No newline at end of file From 7aeb663165766d0f0bc36e0c2f2767216e546d20 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 4 Jan 2021 13:39:41 +0100 Subject: [PATCH 054/183] Implement matrix-scalar multiplication --- nalgebra-sparse/src/ops/impl_std_ops.rs | 102 ++++++++++++++++++++---- nalgebra-sparse/tests/common/mod.rs | 11 +++ nalgebra-sparse/tests/unit_tests/ops.rs | 91 ++++++++++++++++++++- 3 files changed, 188 insertions(+), 16 deletions(-) diff --git a/nalgebra-sparse/src/ops/impl_std_ops.rs b/nalgebra-sparse/src/ops/impl_std_ops.rs index 51d4f3bb..f20dbd52 100644 --- a/nalgebra-sparse/src/ops/impl_std_ops.rs +++ b/nalgebra-sparse/src/ops/impl_std_ops.rs @@ -1,7 +1,7 @@ use crate::csr::CsrMatrix; use crate::csc::CscMatrix; -use std::ops::{Add, Mul}; +use std::ops::{Add, Mul, MulAssign}; use crate::ops::serial::{spadd_csr_prealloc, spadd_csc_prealloc, spadd_pattern, spmm_pattern, spmm_csr_prealloc, spmm_csc_prealloc}; use nalgebra::{ClosedAdd, ClosedMul, Scalar}; @@ -13,17 +13,16 @@ use crate::ops::{Op}; /// See below for usage. macro_rules! impl_bin_op { ($trait:ident, $method:ident, - <$($life:lifetime),*>($a:ident : $a_type:ty, $b:ident : $b_type:ty) -> $ret:ty $body:block) + <$($life:lifetime),* $(,)? $($scalar_type:ident)?>($a:ident : $a_type:ty, $b:ident : $b_type:ty) -> $ret:ty $body:block) => { - impl<$($life,)* T> $trait<$b_type> for $a_type + impl<$($life,)* $($scalar_type)?> $trait<$b_type> for $a_type where - T: Scalar + ClosedAdd + ClosedMul + Zero + One + $($scalar_type: Scalar + ClosedAdd + ClosedMul + Zero + One)? { type Output = $ret; - fn $method(self, rhs: $b_type) -> Self::Output { + fn $method(self, $b: $b_type) -> Self::Output { let $a = self; - let $b = rhs; $body } } @@ -40,7 +39,7 @@ macro_rules! impl_add { /// CsrMatrix or CscMatrix. macro_rules! impl_spadd { ($matrix_type:ident, $spadd_fn:ident) => { - impl_add!(<'a>(a: &'a $matrix_type, b: &'a $matrix_type) -> $matrix_type { + impl_add!(<'a, T>(a: &'a $matrix_type, b: &'a $matrix_type) -> $matrix_type { // If both matrices have the same pattern, then we can immediately re-use it let pattern = if Arc::ptr_eq(a.pattern(), b.pattern()) { Arc::clone(a.pattern()) @@ -56,7 +55,7 @@ macro_rules! impl_spadd { result }); - impl_add!(<'a>(a: $matrix_type, b: &'a $matrix_type) -> $matrix_type { + impl_add!(<'a, T>(a: $matrix_type, b: &'a $matrix_type) -> $matrix_type { let mut a = a; if Arc::ptr_eq(a.pattern(), b.pattern()) { $spadd_fn(T::one(), &mut a, T::one(), Op::NoOp(b)).unwrap(); @@ -66,10 +65,10 @@ macro_rules! impl_spadd { } }); - impl_add!(<'a>(a: &'a $matrix_type, b: $matrix_type) -> $matrix_type { + impl_add!(<'a, T>(a: &'a $matrix_type, b: $matrix_type) -> $matrix_type { b + a }); - impl_add!(<>(a: $matrix_type, b: $matrix_type) -> $matrix_type { + impl_add!((a: $matrix_type, b: $matrix_type) -> $matrix_type { a + &b }); } @@ -88,7 +87,7 @@ macro_rules! impl_mul { /// CsrMatrix or CscMatrix. macro_rules! impl_spmm { ($matrix_type:ident, $pattern_fn:expr, $spmm_fn:expr) => { - impl_mul!(<'a>(a: &'a $matrix_type, b: &'a $matrix_type) -> $matrix_type { + impl_mul!(<'a, T>(a: &'a $matrix_type, b: &'a $matrix_type) -> $matrix_type { let pattern = $pattern_fn(a.pattern(), b.pattern()); let values = vec![T::zero(); pattern.nnz()]; let mut result = $matrix_type::try_from_pattern_and_values(Arc::new(pattern), values) @@ -101,12 +100,85 @@ macro_rules! impl_spmm { .expect("Internal error: spmm failed (please debug)."); result }); - impl_mul!(<'a>(a: &'a $matrix_type, b: $matrix_type) -> $matrix_type { a * &b}); - impl_mul!(<'a>(a: $matrix_type, b: &'a $matrix_type) -> $matrix_type { &a * b}); - impl_mul!(<>(a: $matrix_type, b: $matrix_type) -> $matrix_type { &a * &b}); + impl_mul!(<'a, T>(a: &'a $matrix_type, b: $matrix_type) -> $matrix_type { a * &b}); + impl_mul!(<'a, T>(a: $matrix_type, b: &'a $matrix_type) -> $matrix_type { &a * b}); + impl_mul!((a: $matrix_type, b: $matrix_type) -> $matrix_type { &a * &b}); } } impl_spmm!(CsrMatrix, spmm_pattern, spmm_csr_prealloc); // Need to switch order of operations for CSC pattern -impl_spmm!(CscMatrix, |a, b| spmm_pattern(b, a), spmm_csc_prealloc); \ No newline at end of file +impl_spmm!(CscMatrix, |a, b| spmm_pattern(b, a), spmm_csc_prealloc); + +/// Implements Scalar * Matrix operations for *concrete* scalar types. The reason this is necessary +/// is that we are not able to implement Mul> for all T generically due to orphan rules. +macro_rules! impl_concrete_scalar_matrix_mul { + ($matrix_type:ident, $($scalar_type:ty),*) => { + // For each concrete scalar type, forward the implementation of scalar * matrix + // to matrix * scalar, which we have already implemented through generics + $( + impl_mul!(<>(a: $scalar_type, b: $matrix_type<$scalar_type>) + -> $matrix_type<$scalar_type> { b * a }); + impl_mul!(<'a>(a: $scalar_type, b: &'a $matrix_type<$scalar_type>) + -> $matrix_type<$scalar_type> { b * a }); + impl_mul!(<'a>(a: &'a $scalar_type, b: $matrix_type<$scalar_type>) + -> $matrix_type<$scalar_type> { b * (*a) }); + impl_mul!(<'a>(a: &'a $scalar_type, b: &'a $matrix_type<$scalar_type>) + -> $matrix_type<$scalar_type> { b * *a }); + )* + } +} + +/// Implements multiplication between matrix and scalar for various matrix types +macro_rules! impl_scalar_mul { + ($matrix_type: ident) => { + impl_mul!(<'a, T>(a: &'a $matrix_type, b: &'a T) -> $matrix_type { + let values: Vec<_> = a.values() + .iter() + .map(|v_i| v_i.inlined_clone() * b.inlined_clone()) + .collect(); + $matrix_type::try_from_pattern_and_values(Arc::clone(a.pattern()), values).unwrap() + }); + impl_mul!(<'a, T>(a: &'a $matrix_type, b: T) -> $matrix_type { + a * &b + }); + impl_mul!(<'a, T>(a: $matrix_type, b: &'a T) -> $matrix_type { + let mut a = a; + for value in a.values_mut() { + *value = b.inlined_clone() * value.inlined_clone(); + } + a + }); + impl_mul!((a: $matrix_type, b: T) -> $matrix_type { + a * &b + }); + impl_concrete_scalar_matrix_mul!( + $matrix_type, + i8, i16, i32, i64, u8, u16, u32, u64, isize, usize, f32, f64); + + impl MulAssign for $matrix_type + where + T: Scalar + ClosedAdd + ClosedMul + Zero + One + { + fn mul_assign(&mut self, scalar: T) { + for val in self.values_mut() { + *val *= scalar.inlined_clone(); + } + } + } + + impl<'a, T> MulAssign<&'a T> for $matrix_type + where + T: Scalar + ClosedAdd + ClosedMul + Zero + One + { + fn mul_assign(&mut self, scalar: &'a T) { + for val in self.values_mut() { + *val *= scalar.inlined_clone(); + } + } + } + } +} + +impl_scalar_mul!(CsrMatrix); +impl_scalar_mul!(CscMatrix); \ No newline at end of file diff --git a/nalgebra-sparse/tests/common/mod.rs b/nalgebra-sparse/tests/common/mod.rs index 7a559b2f..2b39ab93 100644 --- a/nalgebra-sparse/tests/common/mod.rs +++ b/nalgebra-sparse/tests/common/mod.rs @@ -3,6 +3,8 @@ use nalgebra_sparse::csr::CsrMatrix; use nalgebra_sparse::proptest::{csr, csc}; use nalgebra_sparse::csc::CscMatrix; use std::ops::RangeInclusive; +use std::convert::{TryFrom}; +use std::fmt::Debug; #[macro_export] macro_rules! assert_panics { @@ -29,6 +31,15 @@ pub const PROPTEST_MATRIX_DIM: RangeInclusive = 0..=6; pub const PROPTEST_MAX_NNZ: usize = 40; pub const PROPTEST_I32_VALUE_STRATEGY: RangeInclusive = -5 ..= 5; +pub fn value_strategy() -> RangeInclusive +where + T: TryFrom, + T::Error: Debug +{ + let (start, end) = (PROPTEST_I32_VALUE_STRATEGY.start(), PROPTEST_I32_VALUE_STRATEGY.end()); + T::try_from(*start).unwrap() ..= T::try_from(*end).unwrap() +} + pub fn csr_strategy() -> impl Strategy> { csr(PROPTEST_I32_VALUE_STRATEGY, PROPTEST_MATRIX_DIM, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ) } diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index fddc5045..3cbee92f 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -280,7 +280,6 @@ fn dense_gemm<'a>(beta: i32, } proptest! { - #[test] fn spmm_csr_dense_agrees_with_dense_result( SpmmCsrDenseArgs { c, beta, alpha, a, b } @@ -837,4 +836,94 @@ proptest! { prop_assert_eq!(&DMatrix::from(&c_ref_ref), &c_dense); prop_assert_eq!(c_ref_ref.pattern(), &c_pattern); } + + #[test] + fn csr_mul_scalar((scalar, matrix) in (PROPTEST_I32_VALUE_STRATEGY, csr_strategy())) { + let dense = DMatrix::from(&matrix); + let dense_result = dense * scalar; + + let result_owned_owned = matrix.clone() * scalar; + let result_owned_ref = matrix.clone() * &scalar; + let result_ref_owned = &matrix * scalar; + let result_ref_ref = &matrix * &scalar; + + // Check that all the combinations of reference and owned variables return the same + // result + prop_assert_eq!(&result_owned_ref, &result_owned_owned); + prop_assert_eq!(&result_ref_owned, &result_owned_owned); + prop_assert_eq!(&result_ref_ref, &result_owned_owned); + + // Check that this result is consistent with the dense result, and that the + // NNZ is the same as before + prop_assert_eq!(result_owned_owned.nnz(), matrix.nnz()); + prop_assert_eq!(DMatrix::from(&result_owned_owned), dense_result); + + // Finally, check mul-assign + let mut result_assign_owned = matrix.clone(); + result_assign_owned *= scalar; + let mut result_assign_ref = matrix.clone(); + result_assign_ref *= &scalar; + + prop_assert_eq!(&result_assign_owned, &result_owned_owned); + prop_assert_eq!(&result_assign_ref, &result_owned_owned); + } + + #[test] + fn csc_mul_scalar((scalar, matrix) in (PROPTEST_I32_VALUE_STRATEGY, csc_strategy())) { + let dense = DMatrix::from(&matrix); + let dense_result = dense * scalar; + + let result_owned_owned = matrix.clone() * scalar; + let result_owned_ref = matrix.clone() * &scalar; + let result_ref_owned = &matrix * scalar; + let result_ref_ref = &matrix * &scalar; + + // Check that all the combinations of reference and owned variables return the same + // result + prop_assert_eq!(&result_owned_ref, &result_owned_owned); + prop_assert_eq!(&result_ref_owned, &result_owned_owned); + prop_assert_eq!(&result_ref_ref, &result_owned_owned); + + // Check that this result is consistent with the dense result, and that the + // NNZ is the same as before + prop_assert_eq!(result_owned_owned.nnz(), matrix.nnz()); + prop_assert_eq!(DMatrix::from(&result_owned_owned), dense_result); + + // Finally, check mul-assign + let mut result_assign_owned = matrix.clone(); + result_assign_owned *= scalar; + let mut result_assign_ref = matrix.clone(); + result_assign_ref *= &scalar; + + prop_assert_eq!(&result_assign_owned, &result_owned_owned); + prop_assert_eq!(&result_assign_ref, &result_owned_owned); + } + + #[test] + fn scalar_mul_csr((scalar, matrix) in (PROPTEST_I32_VALUE_STRATEGY, csr_strategy())) { + // For scalar * matrix, we cannot generally implement this for any type T, + // so we have implemented this for the built in types separately. This requires + // us to also test these types separately. For validation, we check that + // scalar * matrix == matrix * scalar, + // which is sufficient for correctness if matrix * scalar is correctly implemented + // (which is tested separately). + // We only test for i32 here, because with our current implementation, the implementations + // for different types are completely identical and only rely on basic arithmetic + // operations + let result = &matrix * scalar; + prop_assert_eq!(&(scalar * matrix.clone()), &result); + prop_assert_eq!(&(scalar * &matrix), &result); + prop_assert_eq!(&(&scalar * matrix.clone()), &result); + prop_assert_eq!(&(&scalar * &matrix), &result); + } + + #[test] + fn scalar_mul_csc((scalar, matrix) in (PROPTEST_I32_VALUE_STRATEGY, csc_strategy())) { + // See comments for scalar_mul_csr + let result = &matrix * scalar; + prop_assert_eq!(&(scalar * matrix.clone()), &result); + prop_assert_eq!(&(scalar * &matrix), &result); + prop_assert_eq!(&(&scalar * matrix.clone()), &result); + prop_assert_eq!(&(&scalar * &matrix), &result); + } } \ No newline at end of file From 0b4356eb0e8d08b74270c16c4d6475ede62ba53d Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 5 Jan 2021 14:59:54 +0100 Subject: [PATCH 055/183] Implement Sub for Csr/CscMatrix --- nalgebra-sparse/src/ops/impl_std_ops.rs | 67 ++++++++++++++++--------- nalgebra-sparse/tests/unit_tests/ops.rs | 66 ++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 25 deletions(-) diff --git a/nalgebra-sparse/src/ops/impl_std_ops.rs b/nalgebra-sparse/src/ops/impl_std_ops.rs index f20dbd52..51b2dd8b 100644 --- a/nalgebra-sparse/src/ops/impl_std_ops.rs +++ b/nalgebra-sparse/src/ops/impl_std_ops.rs @@ -1,10 +1,10 @@ use crate::csr::CsrMatrix; use crate::csc::CscMatrix; -use std::ops::{Add, Mul, MulAssign}; +use std::ops::{Add, Mul, MulAssign, Sub, Neg}; use crate::ops::serial::{spadd_csr_prealloc, spadd_csc_prealloc, spadd_pattern, spmm_pattern, spmm_csr_prealloc, spmm_csc_prealloc}; -use nalgebra::{ClosedAdd, ClosedMul, Scalar}; +use nalgebra::{ClosedAdd, ClosedMul, ClosedSub, Scalar}; use num_traits::{Zero, One}; use std::sync::Arc; use crate::ops::{Op}; @@ -18,7 +18,10 @@ macro_rules! impl_bin_op { { impl<$($life,)* $($scalar_type)?> $trait<$b_type> for $a_type where - $($scalar_type: Scalar + ClosedAdd + ClosedMul + Zero + One)? + // Note: The Signed bound is currently required because we delegate e.g. + // Sub to SpAdd with negative coefficients. This is not well-defined for + // unsigned data types. + $($scalar_type: Scalar + ClosedAdd + ClosedSub + ClosedMul + Zero + One + Neg)? { type Output = $ret; fn $method(self, $b: $b_type) -> Self::Output { @@ -29,17 +32,19 @@ macro_rules! impl_bin_op { } } -macro_rules! impl_add { - ($($args:tt)*) => { - impl_bin_op!(Add, add, $($args)*); - } -} - -/// Implements a + b for all combinations of reference and owned matrices, for +/// Implements a +/- b for all combinations of reference and owned matrices, for /// CsrMatrix or CscMatrix. -macro_rules! impl_spadd { - ($matrix_type:ident, $spadd_fn:ident) => { - impl_add!(<'a, T>(a: &'a $matrix_type, b: &'a $matrix_type) -> $matrix_type { +macro_rules! impl_sp_plus_minus { + // We first match on some special-case syntax, and forward to the actual implementation + ($matrix_type:ident, $spadd_fn:ident, +) => { + impl_sp_plus_minus!(Add, add, $matrix_type, $spadd_fn, +, T::one()); + }; + ($matrix_type:ident, $spadd_fn:ident, -) => { + impl_sp_plus_minus!(Sub, sub, $matrix_type, $spadd_fn, -, -T::one()); + }; + ($trait:ident, $method:ident, $matrix_type:ident, $spadd_fn:ident, $sign:tt, $factor:expr) => { + impl_bin_op!($trait, $method, + <'a, T>(a: &'a $matrix_type, b: &'a $matrix_type) -> $matrix_type { // If both matrices have the same pattern, then we can immediately re-use it let pattern = if Arc::ptr_eq(a.pattern(), b.pattern()) { Arc::clone(a.pattern()) @@ -51,31 +56,41 @@ macro_rules! impl_spadd { let mut result = $matrix_type::try_from_pattern_and_values(pattern, values) .unwrap(); $spadd_fn(T::zero(), &mut result, T::one(), Op::NoOp(&a)).unwrap(); - $spadd_fn(T::one(), &mut result, T::one(), Op::NoOp(&b)).unwrap(); + $spadd_fn(T::one(), &mut result, $factor * T::one(), Op::NoOp(&b)).unwrap(); result }); - impl_add!(<'a, T>(a: $matrix_type, b: &'a $matrix_type) -> $matrix_type { + impl_bin_op!($trait, $method, + <'a, T>(a: $matrix_type, b: &'a $matrix_type) -> $matrix_type { let mut a = a; if Arc::ptr_eq(a.pattern(), b.pattern()) { - $spadd_fn(T::one(), &mut a, T::one(), Op::NoOp(b)).unwrap(); + $spadd_fn(T::one(), &mut a, $factor * T::one(), Op::NoOp(b)).unwrap(); a } else { - &a + b + &a $sign b } }); - impl_add!(<'a, T>(a: &'a $matrix_type, b: $matrix_type) -> $matrix_type { - b + a + impl_bin_op!($trait, $method, + <'a, T>(a: &'a $matrix_type, b: $matrix_type) -> $matrix_type { + let mut b = b; + if Arc::ptr_eq(a.pattern(), b.pattern()) { + $spadd_fn($factor * T::one(), &mut b, T::one(), Op::NoOp(a)).unwrap(); + b + } else { + a $sign &b + } }); - impl_add!((a: $matrix_type, b: $matrix_type) -> $matrix_type { - a + &b + impl_bin_op!($trait, $method, (a: $matrix_type, b: $matrix_type) -> $matrix_type { + a $sign &b }); } } -impl_spadd!(CsrMatrix, spadd_csr_prealloc); -impl_spadd!(CscMatrix, spadd_csc_prealloc); +impl_sp_plus_minus!(CsrMatrix, spadd_csr_prealloc, +); +impl_sp_plus_minus!(CsrMatrix, spadd_csr_prealloc, -); +impl_sp_plus_minus!(CscMatrix, spadd_csc_prealloc, +); +impl_sp_plus_minus!(CscMatrix, spadd_csc_prealloc, -); macro_rules! impl_mul { ($($args:tt)*) => { @@ -154,7 +169,7 @@ macro_rules! impl_scalar_mul { }); impl_concrete_scalar_matrix_mul!( $matrix_type, - i8, i16, i32, i64, u8, u16, u32, u64, isize, usize, f32, f64); + i8, i16, i32, i64, isize, f32, f64); impl MulAssign for $matrix_type where @@ -181,4 +196,6 @@ macro_rules! impl_scalar_mul { } impl_scalar_mul!(CsrMatrix); -impl_scalar_mul!(CscMatrix); \ No newline at end of file +impl_scalar_mul!(CscMatrix); + +// TODO: Neg, Div \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index 3cbee92f..76817f61 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -417,6 +417,39 @@ proptest! { prop_assert_eq!(c_ref_ref.pattern(), &c_pattern); } + #[test] + fn csr_sub_csr( + // a and b have the same dimensions + (a, b) + in csr_strategy() + .prop_flat_map(|a| { + let b = csr(PROPTEST_I32_VALUE_STRATEGY, Just(a.nrows()), Just(a.ncols()), PROPTEST_MAX_NNZ); + (Just(a), b) + })) + { + // See comments in csr_add_csr for rationale for checking the pattern this way + let c_dense = DMatrix::from(&a) - DMatrix::from(&b); + let c_dense_pattern = dense_csr_pattern(a.pattern()) + dense_csr_pattern(b.pattern()); + let c_pattern = CsrMatrix::from(&c_dense_pattern).pattern().clone(); + + // Check each combination of owned matrices and references + let c_owned_owned = a.clone() - b.clone(); + prop_assert_eq!(&DMatrix::from(&c_owned_owned), &c_dense); + prop_assert_eq!(c_owned_owned.pattern(), &c_pattern); + + let c_owned_ref = a.clone() - &b; + prop_assert_eq!(&DMatrix::from(&c_owned_ref), &c_dense); + prop_assert_eq!(c_owned_ref.pattern(), &c_pattern); + + let c_ref_owned = &a - b.clone(); + prop_assert_eq!(&DMatrix::from(&c_ref_owned), &c_dense); + prop_assert_eq!(c_ref_owned.pattern(), &c_pattern); + + let c_ref_ref = &a - &b; + prop_assert_eq!(&DMatrix::from(&c_ref_ref), &c_dense); + prop_assert_eq!(c_ref_ref.pattern(), &c_pattern); + } + #[test] fn spmm_pattern_test((a, b) in spmm_pattern_strategy()) { @@ -837,6 +870,39 @@ proptest! { prop_assert_eq!(c_ref_ref.pattern(), &c_pattern); } + #[test] + fn csc_sub_csc( + // a and b have the same dimensions + (a, b) + in csc_strategy() + .prop_flat_map(|a| { + let b = csc(PROPTEST_I32_VALUE_STRATEGY, Just(a.nrows()), Just(a.ncols()), PROPTEST_MAX_NNZ); + (Just(a), b) + })) + { + // See comments in csc_add_csc for rationale for checking the pattern this way + let c_dense = DMatrix::from(&a) - DMatrix::from(&b); + let c_dense_pattern = dense_csc_pattern(a.pattern()) + dense_csc_pattern(b.pattern()); + let c_pattern = CscMatrix::from(&c_dense_pattern).pattern().clone(); + + // Check each combination of owned matrices and references + let c_owned_owned = a.clone() - b.clone(); + prop_assert_eq!(&DMatrix::from(&c_owned_owned), &c_dense); + prop_assert_eq!(c_owned_owned.pattern(), &c_pattern); + + let c_owned_ref = a.clone() - &b; + prop_assert_eq!(&DMatrix::from(&c_owned_ref), &c_dense); + prop_assert_eq!(c_owned_ref.pattern(), &c_pattern); + + let c_ref_owned = &a - b.clone(); + prop_assert_eq!(&DMatrix::from(&c_ref_owned), &c_dense); + prop_assert_eq!(c_ref_owned.pattern(), &c_pattern); + + let c_ref_ref = &a - &b; + prop_assert_eq!(&DMatrix::from(&c_ref_ref), &c_dense); + prop_assert_eq!(c_ref_ref.pattern(), &c_pattern); + } + #[test] fn csr_mul_scalar((scalar, matrix) in (PROPTEST_I32_VALUE_STRATEGY, csr_strategy())) { let dense = DMatrix::from(&matrix); From b7a7f967b859474e98e3111f0bef159d8802ab9a Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Wed, 6 Jan 2021 11:04:49 +0100 Subject: [PATCH 056/183] Implement Neg, Div, DivAssign for Csr/CscMatrix --- nalgebra-sparse/src/ops/impl_std_ops.rs | 91 +++++++++++++++++++++-- nalgebra-sparse/tests/common/mod.rs | 9 +++ nalgebra-sparse/tests/unit_tests/ops.rs | 97 ++++++++++++++++++++++++- 3 files changed, 189 insertions(+), 8 deletions(-) diff --git a/nalgebra-sparse/src/ops/impl_std_ops.rs b/nalgebra-sparse/src/ops/impl_std_ops.rs index 51b2dd8b..b9ee23a4 100644 --- a/nalgebra-sparse/src/ops/impl_std_ops.rs +++ b/nalgebra-sparse/src/ops/impl_std_ops.rs @@ -1,10 +1,10 @@ use crate::csr::CsrMatrix; use crate::csc::CscMatrix; -use std::ops::{Add, Mul, MulAssign, Sub, Neg}; +use std::ops::{Add, Div, DivAssign, Mul, MulAssign, Sub, Neg}; use crate::ops::serial::{spadd_csr_prealloc, spadd_csc_prealloc, spadd_pattern, spmm_pattern, spmm_csr_prealloc, spmm_csc_prealloc}; -use nalgebra::{ClosedAdd, ClosedMul, ClosedSub, Scalar}; +use nalgebra::{ClosedAdd, ClosedMul, ClosedSub, ClosedDiv, Scalar}; use num_traits::{Zero, One}; use std::sync::Arc; use crate::ops::{Op}; @@ -13,15 +13,15 @@ use crate::ops::{Op}; /// See below for usage. macro_rules! impl_bin_op { ($trait:ident, $method:ident, - <$($life:lifetime),* $(,)? $($scalar_type:ident)?>($a:ident : $a_type:ty, $b:ident : $b_type:ty) -> $ret:ty $body:block) + <$($life:lifetime),* $(,)? $($scalar_type:ident $(: $bounds:path)?)?>($a:ident : $a_type:ty, $b:ident : $b_type:ty) -> $ret:ty $body:block) => { impl<$($life,)* $($scalar_type)?> $trait<$b_type> for $a_type where - // Note: The Signed bound is currently required because we delegate e.g. + // Note: The Neg bound is currently required because we delegate e.g. // Sub to SpAdd with negative coefficients. This is not well-defined for // unsigned data types. - $($scalar_type: Scalar + ClosedAdd + ClosedSub + ClosedMul + Zero + One + Neg)? + $($scalar_type: $($bounds + )? Scalar + ClosedAdd + ClosedSub + ClosedMul + Zero + One + Neg)? { type Output = $ret; fn $method(self, $b: $b_type) -> Self::Output { @@ -29,7 +29,7 @@ macro_rules! impl_bin_op { $body } } - } + }; } /// Implements a +/- b for all combinations of reference and owned matrices, for @@ -198,4 +198,81 @@ macro_rules! impl_scalar_mul { impl_scalar_mul!(CsrMatrix); impl_scalar_mul!(CscMatrix); -// TODO: Neg, Div \ No newline at end of file +macro_rules! impl_neg { + ($matrix_type:ident) => { + impl Neg for $matrix_type + where + T: Scalar + Neg + { + type Output = $matrix_type; + + fn neg(mut self) -> Self::Output { + for v_i in self.values_mut() { + *v_i = -v_i.inlined_clone(); + } + self + } + } + + impl<'a, T> Neg for &'a $matrix_type + where + T: Scalar + Neg + { + type Output = $matrix_type; + + fn neg(self) -> Self::Output { + // TODO: This is inefficient. Ideally we'd have a method that would let us + // obtain both the sparsity pattern and values from the matrix, + // and then modify the values before creating a new matrix from the pattern + // and negated values. + - self.clone() + } + } + } +} + +impl_neg!(CsrMatrix); +impl_neg!(CscMatrix); + +macro_rules! impl_div { + ($matrix_type:ident) => { + impl_bin_op!(Div, div, (matrix: $matrix_type, scalar: T) -> $matrix_type { + let mut matrix = matrix; + matrix /= scalar; + matrix + }); + impl_bin_op!(Div, div, <'a, T: ClosedDiv>(matrix: $matrix_type, scalar: &T) -> $matrix_type { + matrix / scalar.inlined_clone() + }); + impl_bin_op!(Div, div, <'a, T: ClosedDiv>(matrix: &'a $matrix_type, scalar: T) -> $matrix_type { + let new_values = matrix.values() + .iter() + .map(|v_i| v_i.inlined_clone() / scalar.inlined_clone()) + .collect(); + $matrix_type::try_from_pattern_and_values(Arc::clone(matrix.pattern()), new_values) + .unwrap() + }); + impl_bin_op!(Div, div, <'a, T: ClosedDiv>(matrix: &'a $matrix_type, scalar: &'a T) -> $matrix_type { + matrix / scalar.inlined_clone() + }); + + impl DivAssign for $matrix_type + where T : Scalar + ClosedAdd + ClosedMul + ClosedDiv + Zero + One + { + fn div_assign(&mut self, scalar: T) { + self.values_mut().iter_mut().for_each(|v_i| *v_i /= scalar.inlined_clone()); + } + } + + impl<'a, T> DivAssign<&'a T> for $matrix_type + where T : Scalar + ClosedAdd + ClosedMul + ClosedDiv + Zero + One + { + fn div_assign(&mut self, scalar: &'a T) { + *self /= scalar.inlined_clone(); + } + } + } +} + +impl_div!(CsrMatrix); +impl_div!(CscMatrix); \ No newline at end of file diff --git a/nalgebra-sparse/tests/common/mod.rs b/nalgebra-sparse/tests/common/mod.rs index 2b39ab93..2ff441fe 100644 --- a/nalgebra-sparse/tests/common/mod.rs +++ b/nalgebra-sparse/tests/common/mod.rs @@ -40,6 +40,15 @@ where T::try_from(*start).unwrap() ..= T::try_from(*end).unwrap() } +pub fn non_zero_i32_value_strategy() -> impl Strategy { + let (start, end) = (PROPTEST_I32_VALUE_STRATEGY.start(), PROPTEST_I32_VALUE_STRATEGY.end()); + assert!(start < &0); + assert!(end > &0); + // Note: we don't use RangeInclusive for the second range, because then we'd have different + // types, which would require boxing + (*start .. 0).prop_union(1 .. *end + 1) +} + pub fn csr_strategy() -> impl Strategy> { csr(PROPTEST_I32_VALUE_STRATEGY, PROPTEST_MATRIX_DIM, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ) } diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index 76817f61..001945a9 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -1,5 +1,5 @@ use crate::common::{csc_strategy, csr_strategy, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ, - PROPTEST_I32_VALUE_STRATEGY}; + PROPTEST_I32_VALUE_STRATEGY, non_zero_i32_value_strategy}; use nalgebra_sparse::ops::serial::{spmm_csr_dense, spmm_csc_dense, spadd_pattern, spmm_pattern, spadd_csr_prealloc, spadd_csc_prealloc, spmm_csr_prealloc, spmm_csc_prealloc}; @@ -992,4 +992,99 @@ proptest! { prop_assert_eq!(&(&scalar * matrix.clone()), &result); prop_assert_eq!(&(&scalar * &matrix), &result); } + + #[test] + fn csr_neg(csr in csr_strategy()) { + let result = &csr - 2 * &csr; + prop_assert_eq!(-&csr, result.clone()); + prop_assert_eq!(-csr, result); + } + + #[test] + fn csc_neg(csc in csc_strategy()) { + let result = &csc - 2 * &csc; + prop_assert_eq!(-&csc, result.clone()); + prop_assert_eq!(-csc, result); + } + + #[test] + fn csr_div((csr, divisor) in (csr_strategy(), non_zero_i32_value_strategy())) { + let result_owned_owned = csr.clone() / divisor; + let result_owned_ref = csr.clone() / &divisor; + let result_ref_owned = &csr / divisor; + let result_ref_ref = &csr / &divisor; + + // Verify that all results are the same + prop_assert_eq!(&result_owned_ref, &result_owned_owned); + prop_assert_eq!(&result_ref_owned, &result_owned_owned); + prop_assert_eq!(&result_ref_ref, &result_owned_owned); + + // Check that NNZ was left unchanged + prop_assert_eq!(result_owned_owned.nnz(), csr.nnz()); + + // Then compare against the equivalent dense result + let dense_result = DMatrix::from(&csr) / divisor; + prop_assert_eq!(DMatrix::from(&result_owned_owned), dense_result); + } + + #[test] + fn csc_div((csc, divisor) in (csc_strategy(), non_zero_i32_value_strategy())) { + let result_owned_owned = csc.clone() / divisor; + let result_owned_ref = csc.clone() / &divisor; + let result_ref_owned = &csc / divisor; + let result_ref_ref = &csc / &divisor; + + // Verify that all results are the same + prop_assert_eq!(&result_owned_ref, &result_owned_owned); + prop_assert_eq!(&result_ref_owned, &result_owned_owned); + prop_assert_eq!(&result_ref_ref, &result_owned_owned); + + // Check that NNZ was left unchanged + prop_assert_eq!(result_owned_owned.nnz(), csc.nnz()); + + // Then compare against the equivalent dense result + let dense_result = DMatrix::from(&csc) / divisor; + prop_assert_eq!(DMatrix::from(&result_owned_owned), dense_result); + } + + #[test] + fn csr_div_assign((csr, divisor) in (csr_strategy(), non_zero_i32_value_strategy())) { + let result_owned = { + let mut csr = csr.clone(); + csr /= divisor; + csr + }; + + let result_ref = { + let mut csr = csr.clone(); + csr /= &divisor; + csr + }; + + let expected_result = csr / divisor; + + prop_assert_eq!(&result_owned, &expected_result); + prop_assert_eq!(&result_ref, &expected_result); + } + + #[test] + fn csc_div_assign((csc, divisor) in (csc_strategy(), non_zero_i32_value_strategy())) { + let result_owned = { + let mut csc = csc.clone(); + csc /= divisor; + csc + }; + + let result_ref = { + let mut csc = csc.clone(); + csc /= &divisor; + csc + }; + + let expected_result = csc / divisor; + + prop_assert_eq!(&result_owned, &expected_result); + prop_assert_eq!(&result_ref, &expected_result); + } + } \ No newline at end of file From 885480a6344010499f6552d58fe6fd0dd77e42d7 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Wed, 6 Jan 2021 13:07:55 +0100 Subject: [PATCH 057/183] Implement CSR/CSC * Dense std operations --- nalgebra-sparse/src/ops/impl_std_ops.rs | 37 ++++++++++++++++++++++--- nalgebra-sparse/tests/unit_tests/ops.rs | 28 +++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/nalgebra-sparse/src/ops/impl_std_ops.rs b/nalgebra-sparse/src/ops/impl_std_ops.rs index b9ee23a4..12db5264 100644 --- a/nalgebra-sparse/src/ops/impl_std_ops.rs +++ b/nalgebra-sparse/src/ops/impl_std_ops.rs @@ -2,12 +2,14 @@ use crate::csr::CsrMatrix; use crate::csc::CscMatrix; use std::ops::{Add, Div, DivAssign, Mul, MulAssign, Sub, Neg}; -use crate::ops::serial::{spadd_csr_prealloc, spadd_csc_prealloc, spadd_pattern, - spmm_pattern, spmm_csr_prealloc, spmm_csc_prealloc}; -use nalgebra::{ClosedAdd, ClosedMul, ClosedSub, ClosedDiv, Scalar}; +use crate::ops::serial::{spadd_csr_prealloc, spadd_csc_prealloc, spadd_pattern, spmm_pattern, + spmm_csr_prealloc, spmm_csc_prealloc, spmm_csc_dense, spmm_csr_dense}; +use nalgebra::{ClosedAdd, ClosedMul, ClosedSub, ClosedDiv, Scalar, Matrix, Dim, + DMatrixSlice, DMatrix, Dynamic}; use num_traits::{Zero, One}; use std::sync::Arc; use crate::ops::{Op}; +use nalgebra::base::storage::Storage; /// Helper macro for implementing binary operators for different matrix types /// See below for usage. @@ -275,4 +277,31 @@ macro_rules! impl_div { } impl_div!(CsrMatrix); -impl_div!(CscMatrix); \ No newline at end of file +impl_div!(CscMatrix); + +macro_rules! impl_spmm_cs_dense { + ($matrix_type:ident, $spmm_fn:ident) => { + impl<'a, T, R, C, S> Mul<&'a Matrix> for &'a $matrix_type + where + &'a Matrix: Into>, + T: Scalar + ClosedMul + ClosedAdd + ClosedSub + ClosedDiv + Neg + Zero + One, + R: Dim, + C: Dim, + S: Storage, + { + type Output = DMatrix; + + fn mul(self, rhs: &'a Matrix) -> Self::Output { + let rhs = rhs.into(); + let (_, ncols) = rhs.data.shape(); + let nrows = Dynamic::new(self.nrows()); + let mut result = Matrix::zeros_generic(nrows, ncols); + $spmm_fn(T::zero(), &mut result, T::one(), Op::NoOp(self), Op::NoOp(rhs)); + result + } + } + } +} + +impl_spmm_cs_dense!(CsrMatrix, spmm_csr_dense); +impl_spmm_cs_dense!(CscMatrix, spmm_csc_dense); \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index 001945a9..a8dc248b 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -1087,4 +1087,32 @@ proptest! { prop_assert_eq!(&result_ref, &expected_result); } + #[test] + fn csr_mul_dense( + // a and b have dimensions compatible for multiplication + (a, b) + in csr_strategy() + .prop_flat_map(|a| { + let cols = PROPTEST_MATRIX_DIM; + let b = matrix(PROPTEST_I32_VALUE_STRATEGY, a.ncols(), cols); + (Just(a), b) + })) + { + prop_assert_eq!(&a * &b, &DMatrix::from(&a) * &b); + } + + #[test] + fn csc_mul_dense( + // a and b have dimensions compatible for multiplication + (a, b) + in csc_strategy() + .prop_flat_map(|a| { + let cols = PROPTEST_MATRIX_DIM; + let b = matrix(PROPTEST_I32_VALUE_STRATEGY, a.ncols(), cols); + (Just(a), b) + })) + { + prop_assert_eq!(&a * &b, &DMatrix::from(&a) * &b); + } + } \ No newline at end of file From ea6c1451b42bccfdad1f056ab1f51e250c3105c2 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Wed, 6 Jan 2021 13:10:43 +0100 Subject: [PATCH 058/183] Rename Op::unwrap to Op::into_inner --- nalgebra-sparse/src/ops/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nalgebra-sparse/src/ops/mod.rs b/nalgebra-sparse/src/ops/mod.rs index ef6a2497..abf31069 100644 --- a/nalgebra-sparse/src/ops/mod.rs +++ b/nalgebra-sparse/src/ops/mod.rs @@ -15,7 +15,7 @@ pub enum Op { impl Op { /// TODO pub fn inner_ref(&self) -> &T { - self.as_ref().unwrap() + self.as_ref().into_inner() } /// TODO @@ -43,7 +43,7 @@ impl Op { } /// TODO - pub fn unwrap(self) -> T { + pub fn into_inner(self) -> T { match self { Op::NoOp(obj) | Op::Transpose(obj) => obj, } From 6e34c23d054809001958166af3467b5c7d620643 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 11 Jan 2021 15:03:58 +0100 Subject: [PATCH 059/183] Implement Csr/CscMatrix::identity --- nalgebra-sparse/src/cs.rs | 17 +++++++++++++++++ nalgebra-sparse/src/csc.rs | 12 +++++++++++- nalgebra-sparse/src/csr.rs | 12 +++++++++++- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/nalgebra-sparse/src/cs.rs b/nalgebra-sparse/src/cs.rs index edcb7fa3..c1b4b449 100644 --- a/nalgebra-sparse/src/cs.rs +++ b/nalgebra-sparse/src/cs.rs @@ -4,6 +4,8 @@ use crate::{SparseEntry, SparseEntryMut}; use std::sync::Arc; use std::ops::Range; use std::mem::replace; +use num_traits::One; +use nalgebra::Scalar; /// An abstract compressed matrix. /// @@ -156,6 +158,21 @@ impl CsMatrix { } } +impl CsMatrix { + /// TODO + #[inline] + pub fn identity(n: usize) -> Self { + let offsets: Vec<_> = (0 ..= n).collect(); + let indices: Vec<_> = (0 .. n).collect(); + let values = vec![T::one(); n]; + + // TODO: We should skip checks here + let pattern = SparsityPattern::try_from_offsets_and_indices(n, n, offsets, indices) + .unwrap(); + Self::from_pattern_and_values(Arc::new(pattern), values) + } +} + fn get_entry_from_slices<'a, T>( minor_dim: usize, minor_indices: &'a [usize], diff --git a/nalgebra-sparse/src/csc.rs b/nalgebra-sparse/src/csc.rs index 7b3b8c10..f77bfaef 100644 --- a/nalgebra-sparse/src/csc.rs +++ b/nalgebra-sparse/src/csc.rs @@ -7,7 +7,7 @@ use crate::cs::{CsMatrix, CsLane, CsLaneMut, CsLaneIter, CsLaneIterMut}; use std::sync::Arc; use std::slice::{IterMut, Iter}; -use num_traits::Zero; +use num_traits::{Zero, One}; use nalgebra::Scalar; /// A CSC representation of a sparse matrix. @@ -337,6 +337,16 @@ impl CscMatrix } } +impl CscMatrix { + /// TODO + #[inline] + pub fn identity(n: usize) -> Self { + Self { + cs: CsMatrix::identity(n) + } + } +} + /// Convert pattern format errors into more meaningful CSC-specific errors. /// /// This ensures that the terminology is consistent: we are talking about rows and columns, diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index d5b8e92e..ca42492c 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -5,7 +5,7 @@ use crate::csc::CscMatrix; use crate::cs::{CsMatrix, CsLaneIterMut, CsLaneIter, CsLane, CsLaneMut}; use nalgebra::Scalar; -use num_traits::Zero; +use num_traits::{Zero, One}; use std::sync::Arc; use std::slice::{IterMut, Iter}; @@ -338,6 +338,16 @@ where } } +impl CsrMatrix { + /// TODO + #[inline] + pub fn identity(n: usize) -> Self { + Self { + cs: CsMatrix::identity(n) + } + } +} + /// Convert pattern format errors into more meaningful CSR-specific errors. /// /// This ensures that the terminology is consistent: we are talking about rows and columns, From aad2216c56f96229a09a73c6d6a050838d6ae2f6 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 11 Jan 2021 15:14:54 +0100 Subject: [PATCH 060/183] Initial port from nalgebra::CsCholesky factorization to CscCholesky --- nalgebra-sparse/Cargo.toml | 2 + nalgebra-sparse/src/csc.rs | 1 - nalgebra-sparse/src/factorization/cholesky.rs | 294 ++++++++++++++++++ nalgebra-sparse/src/factorization/mod.rs | 4 + nalgebra-sparse/src/lib.rs | 1 + nalgebra-sparse/tests/unit_tests/cholesky.rs | 93 ++++++ nalgebra-sparse/tests/unit_tests/mod.rs | 1 + 7 files changed, 395 insertions(+), 1 deletion(-) create mode 100644 nalgebra-sparse/src/factorization/cholesky.rs create mode 100644 nalgebra-sparse/src/factorization/mod.rs create mode 100644 nalgebra-sparse/tests/unit_tests/cholesky.rs diff --git a/nalgebra-sparse/Cargo.toml b/nalgebra-sparse/Cargo.toml index 3a4f08c1..988850d2 100644 --- a/nalgebra-sparse/Cargo.toml +++ b/nalgebra-sparse/Cargo.toml @@ -17,3 +17,5 @@ proptest = { version = "0.10", optional = true } [dev-dependencies] itertools = "0.9" +matrixcompare = "0.1.3" +nalgebra = { version="0.23", path = "../", features = ["compare"] } diff --git a/nalgebra-sparse/src/csc.rs b/nalgebra-sparse/src/csc.rs index f77bfaef..11b96a6f 100644 --- a/nalgebra-sparse/src/csc.rs +++ b/nalgebra-sparse/src/csc.rs @@ -115,7 +115,6 @@ impl CscMatrix { } } - /// An iterator over non-zero triplets (i, j, v). /// /// The iteration happens in column-major fashion, meaning that j increases monotonically, diff --git a/nalgebra-sparse/src/factorization/cholesky.rs b/nalgebra-sparse/src/factorization/cholesky.rs new file mode 100644 index 00000000..3e9c032e --- /dev/null +++ b/nalgebra-sparse/src/factorization/cholesky.rs @@ -0,0 +1,294 @@ +// TODO: Remove this allowance +#![allow(missing_docs)] + +use crate::pattern::SparsityPattern; +use crate::csc::CscMatrix; +use core::{mem, iter}; +use nalgebra::{U1, VectorN, Dynamic, Scalar, RealField}; +use num_traits::Zero; +use std::sync::Arc; +use std::ops::Add; + +pub struct CscSymbolicCholesky { + // Pattern of the original matrix that was decomposed + m_pattern: Arc, + l_pattern: SparsityPattern, + // u in this context is L^T, so that M = L L^T + u_pattern: SparsityPattern +} + +impl CscSymbolicCholesky { + pub fn factor(pattern: &Arc) -> Self { + assert_eq!(pattern.major_dim(), pattern.minor_dim(), + "Major and minor dimensions must be the same (square matrix)."); + + // TODO: Temporary stopgap solution to make things work until we can refactor + #[derive(Copy, Clone, PartialEq, Eq, Debug)] + struct DummyVal; + impl Zero for DummyVal { + fn zero() -> Self { + DummyVal + } + + fn is_zero(&self) -> bool { + true + } + } + + impl Add for DummyVal { + type Output = Self; + + fn add(self, rhs: DummyVal) -> Self::Output { + rhs + } + } + + let dummy_vals = vec![DummyVal; pattern.nnz()]; + let dummy_csc = CscMatrix::try_from_pattern_and_values(Arc::clone(pattern), dummy_vals) + .unwrap(); + let (l, u) = nonzero_pattern(&dummy_csc); + // TODO: Don't clone unnecessarily + Self { + m_pattern: Arc::clone(pattern), + l_pattern: l.pattern().as_ref().clone(), + u_pattern: u.pattern().as_ref().clone() + } + } + + pub fn l_pattern(&self) -> &SparsityPattern { + &self.l_pattern + } +} + +pub struct CscCholesky { + // Pattern of the original matrix + m_pattern: Arc, + l_factor: CscMatrix, + u_pattern: SparsityPattern, + work_x: Vec, + work_c: Vec +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum CholeskyError { + +} + +impl CscCholesky { + + pub fn factor(matrix: &CscMatrix) -> Result { + let symbolic = CscSymbolicCholesky::factor(&*matrix.pattern()); + assert_eq!(symbolic.l_pattern.nnz(), symbolic.u_pattern.nnz(), + "u is just the transpose of l, so should have the same nnz"); + + let l_nnz = symbolic.l_pattern.nnz(); + let l_values = vec![T::zero(); l_nnz]; + let l_factor = CscMatrix::try_from_pattern_and_values(Arc::new(symbolic.l_pattern), l_values) + .unwrap(); + + let mut factorization = CscCholesky { + m_pattern: symbolic.m_pattern, + l_factor, + u_pattern: symbolic.u_pattern, + work_x: vec![T::zero(); matrix.nrows()], + // Fill with MAX so that things hopefully totally fail if values are not + // overwritten. Might be easier to debug this way + work_c: vec![usize::MAX, matrix.ncols()], + }; + + factorization.refactor(matrix.values())?; + Ok(factorization) + } + + pub fn refactor(&mut self, values: &[T]) -> Result<(), CholeskyError> { + self.decompose_left_looking(values) + } + + pub fn l(&self) -> &CscMatrix { + &self.l_factor + } + + pub fn take_l(self) -> CscMatrix { + self.l_factor + } + + /// Perform a numerical left-looking cholesky decomposition of a matrix with the same structure as the + /// one used to initialize `self`, but with different non-zero values provided by `values`. + fn decompose_left_looking(&mut self, values: &[T]) -> Result<(), CholeskyError> { + assert!( + values.len() >= self.m_pattern.nnz(), + // TODO: Improve error message + "The set of values is too small." + ); + + let n = self.l_factor.nrows(); + + // Reset `work_c` to the column pointers of `l`. + self.work_c.clear(); + self.work_c.extend_from_slice(self.l_factor.col_offsets()); + + unsafe { + for k in 0..n { + // Scatter the k-th column of the original matrix with the values provided. + let range_begin = *self.m_pattern.major_offsets().get_unchecked(k); + let range_end = *self.m_pattern.major_offsets().get_unchecked(k + 1); + let range_k = range_begin..range_end; + + *self.work_x.get_unchecked_mut(k) = T::zero(); + for p in range_k.clone() { + let irow = *self.m_pattern.minor_indices().get_unchecked(p); + + if irow >= k { + *self.work_x.get_unchecked_mut(irow) = *values.get_unchecked(p); + } + } + + for &j in self.u_pattern.lane(k) { + let factor = -*self + .l_factor + .values() + .get_unchecked(*self.work_c.get_unchecked(j)); + *self.work_c.get_unchecked_mut(j) += 1; + + if j < k { + let col_j = self.l_factor.col(j); + let col_j_entries = col_j.row_indices().iter().zip(col_j.values()); + for (&z, val) in col_j_entries { + if z >= k { + *self.work_x.get_unchecked_mut(z) += val.inlined_clone() * factor; + } + } + } + } + + let diag = *self.work_x.get_unchecked(k); + + if diag > T::zero() { + let denom = diag.sqrt(); + + { + let (offsets, _, values) = self.l_factor.csc_data_mut(); + *values + .get_unchecked_mut(*offsets.get_unchecked(k)) = denom; + } + + + let mut col_k = self.l_factor.col_mut(k); + let (col_k_rows, col_k_values) = col_k.rows_and_values_mut(); + let col_k_entries = col_k_rows.iter().zip(col_k_values); + for (&p, val) in col_k_entries { + *val = *self.work_x.get_unchecked(p) / denom; + *self.work_x.get_unchecked_mut(p) = T::zero(); + } + } else { + // self.ok = false; + // TODO: Return indefinite error (i.e. encountered non-positive diagonal + unimplemented!() + } + } + } + + Ok(()) + } + +} + + + + +fn reach( + m: &CscMatrix, + j: usize, + max_j: usize, + tree: &[usize], + marks: &mut Vec, + out: &mut Vec, +) { + marks.clear(); + marks.resize(tree.len(), false); + + // TODO: avoid all those allocations. + let mut tmp = Vec::new(); + let mut res = Vec::new(); + + for &irow in m.col(j).row_indices() { + let mut curr = irow; + while curr != usize::max_value() && curr <= max_j && !marks[curr] { + marks[curr] = true; + tmp.push(curr); + curr = tree[curr]; + } + + tmp.append(&mut res); + mem::swap(&mut tmp, &mut res); + } + + // TODO: Is this right? + res.sort_unstable(); + + out.append(&mut res); +} + +fn nonzero_pattern( + m: &CscMatrix +) -> (CscMatrix, CscMatrix) { + // TODO: In order to stay as faithful as possible to the original implementation, + // we here return full matrices, whereas we actually only need to construct sparsity patterns + + let etree = elimination_tree(m); + let (nrows, ncols) = (m.nrows(), m.ncols()); + let mut rows = Vec::with_capacity(m.nnz()); + // TODO: Use a Vec here instead + let mut cols = unsafe { VectorN::new_uninitialized_generic(Dynamic::new(nrows), U1) }; + let mut marks = Vec::new(); + + // NOTE: the following will actually compute the non-zero pattern of + // the transpose of l. + for i in 0..nrows { + cols[i] = rows.len(); + reach(m, i, i, &etree, &mut marks, &mut rows); + } + + // TODO: Get rid of this in particular + let mut vals = Vec::with_capacity(rows.len()); + unsafe { + vals.set_len(rows.len()); + } + vals.shrink_to_fit(); + + // TODO: Remove this unnecessary conversion by using Vec throughout + let mut cols: Vec<_> = cols.iter().cloned().collect(); + cols.push(rows.len()); + + let u = CscMatrix::try_from_csc_data(nrows, ncols, cols, rows, vals).unwrap(); + // TODO: Avoid this transpose + let l = u.transpose(); + + (l, u) +} + +fn elimination_tree(m: &CscMatrix) -> Vec { + let nrows = m.nrows(); + let mut forest: Vec<_> = iter::repeat(usize::max_value()).take(nrows).collect(); + let mut ancestor: Vec<_> = iter::repeat(usize::max_value()).take(nrows).collect(); + + for k in 0..nrows { + for &irow in m.col(k).row_indices() { + let mut i = irow; + + while i < k { + let i_ancestor = ancestor[i]; + ancestor[i] = k; + + if i_ancestor == usize::max_value() { + forest[i] = k; + break; + } + + i = i_ancestor; + } + } + } + + forest +} \ No newline at end of file diff --git a/nalgebra-sparse/src/factorization/mod.rs b/nalgebra-sparse/src/factorization/mod.rs new file mode 100644 index 00000000..6596f530 --- /dev/null +++ b/nalgebra-sparse/src/factorization/mod.rs @@ -0,0 +1,4 @@ +//! Matrix factorization for sparse matrices. +mod cholesky; + +pub use cholesky::*; \ No newline at end of file diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index c81b429b..9441fc96 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -89,6 +89,7 @@ pub mod csr; pub mod pattern; pub mod ops; pub mod convert; +pub mod factorization; pub(crate) mod cs; diff --git a/nalgebra-sparse/tests/unit_tests/cholesky.rs b/nalgebra-sparse/tests/unit_tests/cholesky.rs new file mode 100644 index 00000000..95550278 --- /dev/null +++ b/nalgebra-sparse/tests/unit_tests/cholesky.rs @@ -0,0 +1,93 @@ +#![cfg_attr(rustfmt, rustfmt_skip)] +use crate::common::{value_strategy, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ}; +use nalgebra_sparse::csc::CscMatrix; +use nalgebra_sparse::factorization::{CscCholesky}; +use nalgebra_sparse::proptest::csc; +use nalgebra::{Matrix5, Vector5, Cholesky, DMatrix}; + +use proptest::prelude::*; +use matrixcompare::assert_matrix_eq; + +fn positive_definite() -> impl Strategy> { + let csc_f64 = csc(value_strategy::(), + PROPTEST_MATRIX_DIM, + PROPTEST_MATRIX_DIM, + PROPTEST_MAX_NNZ); + csc_f64 + .prop_map(|x| { + // Add a small multiple of the identity to ensure positive definiteness + x.transpose() * &x + CscMatrix::identity(x.ncols()) + }) +} + +proptest! { + #[test] + fn cholesky_correct_for_positive_definite_matrices( + matrix in positive_definite() + ) { + let cholesky = CscCholesky::factor(&matrix).unwrap(); + let l = cholesky.take_l(); + let matrix_reconstructed = &l * l.transpose(); + + // TODO: Use matrixcompare instead + let diff = DMatrix::from(&(matrix_reconstructed - matrix)); + prop_assert!(diff.abs().max() < 1e-8); + + // TODO: Check that L is in fact lower triangular + } + +} + +// This is a test ported from nalgebra's "sparse" module, for the original CsCholesky impl +#[test] +fn cs_cholesky() { + let mut a = Matrix5::new( + 40.0, 0.0, 0.0, 0.0, 0.0, + 2.0, 60.0, 0.0, 0.0, 0.0, + 1.0, 0.0, 11.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 50.0, 0.0, + 1.0, 0.0, 0.0, 4.0, 10.0 + ); + a.fill_upper_triangle_with_lower_triangle(); + test_cholesky(a); + + let a = Matrix5::from_diagonal(&Vector5::new(40.0, 60.0, 11.0, 50.0, 10.0)); + test_cholesky(a); + + let mut a = Matrix5::new( + 40.0, 0.0, 0.0, 0.0, 0.0, + 2.0, 60.0, 0.0, 0.0, 0.0, + 1.0, 0.0, 11.0, 0.0, 0.0, + 1.0, 0.0, 0.0, 50.0, 0.0, + 0.0, 0.0, 0.0, 4.0, 10.0 + ); + a.fill_upper_triangle_with_lower_triangle(); + test_cholesky(a); + + let mut a = Matrix5::new( + 2.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 2.0, 0.0, 0.0, 0.0, + 1.0, 1.0, 2.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 2.0, 0.0, + 1.0, 1.0, 0.0, 0.0, 2.0 + ); + a.fill_upper_triangle_with_lower_triangle(); + // Test crate::new, left_looking, and up_looking implementations. + test_cholesky(a); +} + +fn test_cholesky(a: Matrix5) { + // TODO: Test "refactor" + + let cs_a = CscMatrix::from(&a); + + let chol_a = Cholesky::new(a).unwrap(); + let chol_cs_a = CscCholesky::factor(&cs_a).unwrap(); + + let l = chol_a.l(); + let cs_l = chol_cs_a.take_l(); + + let l = DMatrix::from_iterator(l.nrows(), l.ncols(), l.iter().cloned()); + let cs_l_mat = DMatrix::from(&cs_l); + assert_matrix_eq!(l, cs_l_mat, comp = abs, tol = 1e-12); +} \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit_tests/mod.rs b/nalgebra-sparse/tests/unit_tests/mod.rs index cb32cd71..a1f25715 100644 --- a/nalgebra-sparse/tests/unit_tests/mod.rs +++ b/nalgebra-sparse/tests/unit_tests/mod.rs @@ -1,4 +1,5 @@ mod coo; +mod cholesky; mod ops; mod pattern; mod csr; From 84557d8046b20f9b9b215e77161ed00c7f220735 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Wed, 13 Jan 2021 13:10:21 +0100 Subject: [PATCH 061/183] Implement matrixcompare traits for sparse matrices --- nalgebra-sparse/Cargo.toml | 2 + nalgebra-sparse/src/lib.rs | 4 ++ nalgebra-sparse/src/matrixcompare.rs | 61 ++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 nalgebra-sparse/src/matrixcompare.rs diff --git a/nalgebra-sparse/Cargo.toml b/nalgebra-sparse/Cargo.toml index 988850d2..8284bc7a 100644 --- a/nalgebra-sparse/Cargo.toml +++ b/nalgebra-sparse/Cargo.toml @@ -6,6 +6,7 @@ edition = "2018" [features] proptest-support = ["proptest", "nalgebra/proptest"] +compare = [ "matrixcompare-core" ] # Enable to enable running some tests that take a lot of time to run slow-tests = [] @@ -14,6 +15,7 @@ slow-tests = [] nalgebra = { version="0.23", path = "../" } num-traits = { version = "0.2", default-features = false } proptest = { version = "0.10", optional = true } +matrixcompare-core = { version = "0.1.0", optional = true } [dev-dependencies] itertools = "0.9" diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index 9441fc96..fe4255a8 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -76,6 +76,7 @@ //! - Overall design ("easy API" vs. "expert" API etc.) //! - Conversions (From, explicit "expert" API etc.) //! - Matrix ops design +//! - Proptest and matrixcompare integrations #![deny(non_camel_case_types)] #![deny(unused_parens)] #![deny(non_upper_case_globals)] @@ -96,6 +97,9 @@ pub(crate) mod cs; #[cfg(feature = "proptest-support")] pub mod proptest; +#[cfg(feature = "compare")] +mod matrixcompare; + use std::error::Error; use std::fmt; use num_traits::Zero; diff --git a/nalgebra-sparse/src/matrixcompare.rs b/nalgebra-sparse/src/matrixcompare.rs new file mode 100644 index 00000000..a4aafe3c --- /dev/null +++ b/nalgebra-sparse/src/matrixcompare.rs @@ -0,0 +1,61 @@ +//! Implements core traits for use with `matrixcompare`. +use crate::csr::CsrMatrix; +use crate::csc::CscMatrix; +use matrixcompare_core; +use matrixcompare_core::{Access, SparseAccess}; +use crate::coo::CooMatrix; + +macro_rules! impl_matrix_for_csr_csc { + ($MatrixType:ident) => { + impl SparseAccess for $MatrixType { + fn nnz(&self) -> usize { + $MatrixType::nnz(self) + } + + fn fetch_triplets(&self) -> Vec<(usize, usize, T)> { + self.triplet_iter().map(|(i, j, v)| (i, j, v.clone())).collect() + } + } + + impl matrixcompare_core::Matrix for $MatrixType { + fn rows(&self) -> usize { + self.nrows() + } + + fn cols(&self) -> usize { + self.ncols() + } + + fn access(&self) -> Access { + Access::Sparse(self) + } + } + } +} + +impl_matrix_for_csr_csc!(CsrMatrix); +impl_matrix_for_csr_csc!(CscMatrix); + +impl SparseAccess for CooMatrix { + fn nnz(&self) -> usize { + CooMatrix::nnz(self) + } + + fn fetch_triplets(&self) -> Vec<(usize, usize, T)> { + self.triplet_iter().map(|(i, j, v)| (i, j, v.clone())).collect() + } +} + +impl matrixcompare_core::Matrix for CooMatrix { + fn rows(&self) -> usize { + self.nrows() + } + + fn cols(&self) -> usize { + self.ncols() + } + + fn access(&self) -> Access { + Access::Sparse(self) + } +} \ No newline at end of file From 3453577a16b29b4dda275ea7a7f08a2ec9ff969d Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Wed, 13 Jan 2021 13:28:15 +0100 Subject: [PATCH 062/183] Use matrixcompare 0.2 with prop_assert_matrix_eq in Cholesky test --- nalgebra-sparse/Cargo.toml | 2 +- nalgebra-sparse/tests/unit_tests/cholesky.rs | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/nalgebra-sparse/Cargo.toml b/nalgebra-sparse/Cargo.toml index 8284bc7a..3f9f8c8a 100644 --- a/nalgebra-sparse/Cargo.toml +++ b/nalgebra-sparse/Cargo.toml @@ -19,5 +19,5 @@ matrixcompare-core = { version = "0.1.0", optional = true } [dev-dependencies] itertools = "0.9" -matrixcompare = "0.1.3" +matrixcompare = { version = "0.2.0", features = [ "proptest-support" ] } nalgebra = { version="0.23", path = "../", features = ["compare"] } diff --git a/nalgebra-sparse/tests/unit_tests/cholesky.rs b/nalgebra-sparse/tests/unit_tests/cholesky.rs index 95550278..87517828 100644 --- a/nalgebra-sparse/tests/unit_tests/cholesky.rs +++ b/nalgebra-sparse/tests/unit_tests/cholesky.rs @@ -6,7 +6,7 @@ use nalgebra_sparse::proptest::csc; use nalgebra::{Matrix5, Vector5, Cholesky, DMatrix}; use proptest::prelude::*; -use matrixcompare::assert_matrix_eq; +use matrixcompare::{assert_matrix_eq, prop_assert_matrix_eq}; fn positive_definite() -> impl Strategy> { let csc_f64 = csc(value_strategy::(), @@ -29,11 +29,10 @@ proptest! { let l = cholesky.take_l(); let matrix_reconstructed = &l * l.transpose(); - // TODO: Use matrixcompare instead - let diff = DMatrix::from(&(matrix_reconstructed - matrix)); - prop_assert!(diff.abs().max() < 1e-8); + prop_assert_matrix_eq!(matrix_reconstructed, matrix, comp = abs, tol = 1e-8); - // TODO: Check that L is in fact lower triangular + let is_lower_triangular = l.triplet_iter().all(|(i, j, _)| j <= i); + prop_assert!(is_lower_triangular); } } From 5869f784e5d06833b0ce0bb0731f8cba17595fd7 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Thu, 14 Jan 2021 17:12:08 +0100 Subject: [PATCH 063/183] Implement CsrMatrix/CscMatrix::filter and associated helpers Includes ::lower_triangle(), ::upper_triangle() and ::diagonal_matrix(). --- nalgebra-sparse/src/cs.rs | 34 ++++++++++++++ nalgebra-sparse/src/csc.rs | 51 +++++++++++++++++++++ nalgebra-sparse/src/csr.rs | 49 ++++++++++++++++++++ nalgebra-sparse/tests/unit_tests/csc.rs | 57 +++++++++++++++++++++++ nalgebra-sparse/tests/unit_tests/csr.rs | 60 +++++++++++++++++++++++++ 5 files changed, 251 insertions(+) diff --git a/nalgebra-sparse/src/cs.rs b/nalgebra-sparse/src/cs.rs index c1b4b449..634ce413 100644 --- a/nalgebra-sparse/src/cs.rs +++ b/nalgebra-sparse/src/cs.rs @@ -156,6 +156,40 @@ impl CsMatrix { pub fn lane_iter_mut(&mut self) -> CsLaneIterMut { CsLaneIterMut::new(self.sparsity_pattern.as_ref(), &mut self.values) } + + #[inline] + pub fn filter

(&self, predicate: P) -> Self + where + T: Clone, + P: Fn(usize, usize, &T) -> bool + { + let (major_dim, minor_dim) = (self.pattern().major_dim(), self.pattern().minor_dim()); + let mut new_offsets = Vec::with_capacity(self.pattern().major_dim() + 1); + let mut new_indices = Vec::new(); + let mut new_values = Vec::new(); + + new_offsets.push(0); + for (i, lane) in self.lane_iter().enumerate() { + for (&j, value) in lane.minor_indices().iter().zip(lane.values) { + if predicate(i, j, value) { + new_indices.push(j); + new_values.push(value.clone()); + } + } + + new_offsets.push(new_indices.len()); + } + + // TODO: Avoid checks here + let new_pattern = SparsityPattern::try_from_offsets_and_indices( + major_dim, + minor_dim, + new_offsets, + new_indices) + .expect("Internal error: Sparsity pattern must always be valid."); + + Self::from_pattern_and_values(Arc::new(new_pattern), new_values) + } } impl CsMatrix { diff --git a/nalgebra-sparse/src/csc.rs b/nalgebra-sparse/src/csc.rs index 11b96a6f..1d8b8970 100644 --- a/nalgebra-sparse/src/csc.rs +++ b/nalgebra-sparse/src/csc.rs @@ -324,6 +324,46 @@ impl CscMatrix { pub fn csc_data_mut(&mut self) -> (&[usize], &[usize], &mut [T]) { self.cs.cs_data_mut() } + + /// Creates a sparse matrix that contains only the explicit entries decided by the + /// given predicate. + pub fn filter

(&self, predicate: P) -> Self + where + T: Clone, + P: Fn(usize, usize, &T) -> bool + { + // Note: Predicate uses (row, col, value), so we have to switch around since + // cs uses (major, minor, value) + Self { cs: self.cs.filter(|col_idx, row_idx, v| predicate(row_idx, col_idx, v)) } + } + + /// Returns a new matrix representing the upper triangular part of this matrix. + /// + /// The result includes the diagonal of the matrix. + pub fn upper_triangle(&self) -> Self + where + T: Clone + { + self.filter(|i, j, _| i <= j) + } + + /// Returns a new matrix representing the lower triangular part of this matrix. + /// + /// The result includes the diagonal of the matrix. + pub fn lower_triangle(&self) -> Self + where + T: Clone + { + self.filter(|i, j, _| i >= j) + } + + /// Returns the diagonal of the matrix as a sparse matrix. + pub fn diagonal_as_matrix(&self) -> Self + where + T: Clone + { + self.filter(|i, j, _| i == j) + } } impl CscMatrix @@ -385,6 +425,17 @@ pub struct CscTripletIter<'a, T> { values_iter: Iter<'a, T> } +impl<'a, T: Clone> CscTripletIter<'a, T> { + /// Adapts the triplet iterator to return owned values. + /// + /// The triplet iterator returns references to the values. This method adapts the iterator + /// so that the values are cloned. + #[inline] + pub fn cloned_values(self) -> impl 'a + Iterator { + self.map(|(i, j, v)| (i, j, v.clone())) + } +} + impl<'a, T> Iterator for CscTripletIter<'a, T> { type Item = (usize, usize, &'a T); diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index ca42492c..1f621c86 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -326,6 +326,44 @@ impl CsrMatrix { pub fn csr_data_mut(&mut self) -> (&[usize], &[usize], &mut [T]) { self.cs.cs_data_mut() } + + /// Creates a sparse matrix that contains only the explicit entries decided by the + /// given predicate. + pub fn filter

(&self, predicate: P) -> Self + where + T: Clone, + P: Fn(usize, usize, &T) -> bool + { + Self { cs: self.cs.filter(|row_idx, col_idx, v| predicate(row_idx, col_idx, v)) } + } + + /// Returns a new matrix representing the upper triangular part of this matrix. + /// + /// The result includes the diagonal of the matrix. + pub fn upper_triangle(&self) -> Self + where + T: Clone + { + self.filter(|i, j, _| i <= j) + } + + /// Returns a new matrix representing the lower triangular part of this matrix. + /// + /// The result includes the diagonal of the matrix. + pub fn lower_triangle(&self) -> Self + where + T: Clone + { + self.filter(|i, j, _| i >= j) + } + + /// Returns the diagonal of the matrix as a sparse matrix. + pub fn diagonal_as_matrix(&self) -> Self + where + T: Clone + { + self.filter(|i, j, _| i == j) + } } impl CsrMatrix @@ -387,6 +425,17 @@ pub struct CsrTripletIter<'a, T> { values_iter: Iter<'a, T> } +impl<'a, T: Clone> CsrTripletIter<'a, T> { + /// Adapts the triplet iterator to return owned values. + /// + /// The triplet iterator returns references to the values. This method adapts the iterator + /// so that the values are cloned. + #[inline] + pub fn cloned_values(self) -> impl 'a + Iterator { + self.map(|(i, j, v)| (i, j, v.clone())) + } +} + impl<'a, T> Iterator for CsrTripletIter<'a, T> { type Item = (usize, usize, &'a T); diff --git a/nalgebra-sparse/tests/unit_tests/csc.rs b/nalgebra-sparse/tests/unit_tests/csc.rs index a16e1686..4faa4e12 100644 --- a/nalgebra-sparse/tests/unit_tests/csc.rs +++ b/nalgebra-sparse/tests/unit_tests/csc.rs @@ -3,9 +3,12 @@ use nalgebra_sparse::SparseFormatErrorKind; use nalgebra::DMatrix; use proptest::prelude::*; +use proptest::sample::subsequence; use crate::common::csc_strategy; +use std::collections::HashSet; + #[test] fn csc_matrix_valid_data() { // Construct matrix from valid data and check that selected methods return results @@ -271,4 +274,58 @@ proptest! { prop_assert_eq!(dense_transpose, DMatrix::from(&csc_transpose)); prop_assert_eq!(csc.nnz(), csc_transpose.nnz()); } + + #[test] + fn csc_filter( + (csc, triplet_subset) + in csc_strategy() + .prop_flat_map(|matrix| { + let triplets: Vec<_> = matrix.triplet_iter().cloned_values().collect(); + let subset = subsequence(triplets, 0 ..= matrix.nnz()) + .prop_map(|triplet_subset| { + let set: HashSet<_> = triplet_subset.into_iter().collect(); + set + }); + (Just(matrix), subset) + })) + { + // We generate a CscMatrix and a HashSet corresponding to a subset of the (i, j, v) + // values in the matrix, which we use for filtering the matrix entries. + // The resulting triplets in the filtered matrix must then be exactly equal to + // the subset. + let filtered = csc.filter(|i, j, v| triplet_subset.contains(&(i, j, *v))); + let filtered_triplets: HashSet<_> = filtered + .triplet_iter() + .cloned_values() + .collect(); + + prop_assert_eq!(filtered_triplets, triplet_subset); + } + + #[test] + fn csc_lower_triangle_agrees_with_dense(csc in csc_strategy()) { + let csc_lower_triangle = csc.lower_triangle(); + prop_assert_eq!(DMatrix::from(&csc_lower_triangle), DMatrix::from(&csc).lower_triangle()); + prop_assert!(csc_lower_triangle.nnz() <= csc.nnz()); + } + + #[test] + fn csc_upper_triangle_agrees_with_dense(csc in csc_strategy()) { + let csc_upper_triangle = csc.upper_triangle(); + prop_assert_eq!(DMatrix::from(&csc_upper_triangle), DMatrix::from(&csc).upper_triangle()); + prop_assert!(csc_upper_triangle.nnz() <= csc.nnz()); + } + + #[test] + fn csc_diagonal_as_matrix(csc in csc_strategy()) { + let d = csc.diagonal_as_matrix(); + let d_entries: HashSet<_> = d.triplet_iter().cloned_values().collect(); + let csc_diagonal_entries: HashSet<_> = csc + .triplet_iter() + .cloned_values() + .filter(|&(i, j, _)| i == j) + .collect(); + + prop_assert_eq!(d_entries, csc_diagonal_entries); + } } \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit_tests/csr.rs b/nalgebra-sparse/tests/unit_tests/csr.rs index 424bc2c1..4885d25b 100644 --- a/nalgebra-sparse/tests/unit_tests/csr.rs +++ b/nalgebra-sparse/tests/unit_tests/csr.rs @@ -1,9 +1,15 @@ use nalgebra_sparse::csr::CsrMatrix; use nalgebra_sparse::SparseFormatErrorKind; use nalgebra::DMatrix; + use proptest::prelude::*; +use proptest::sample::subsequence; + use crate::common::csr_strategy; +use std::collections::HashSet; + + #[test] fn csr_matrix_valid_data() { // Construct matrix from valid data and check that selected methods return results @@ -269,4 +275,58 @@ proptest! { prop_assert_eq!(dense_transpose, DMatrix::from(&csr_transpose)); prop_assert_eq!(csr.nnz(), csr_transpose.nnz()); } + + #[test] + fn csr_filter( + (csr, triplet_subset) + in csr_strategy() + .prop_flat_map(|matrix| { + let triplets: Vec<_> = matrix.triplet_iter().cloned_values().collect(); + let subset = subsequence(triplets, 0 ..= matrix.nnz()) + .prop_map(|triplet_subset| { + let set: HashSet<_> = triplet_subset.into_iter().collect(); + set + }); + (Just(matrix), subset) + })) + { + // We generate a CsrMatrix and a HashSet corresponding to a subset of the (i, j, v) + // values in the matrix, which we use for filtering the matrix entries. + // The resulting triplets in the filtered matrix must then be exactly equal to + // the subset. + let filtered = csr.filter(|i, j, v| triplet_subset.contains(&(i, j, *v))); + let filtered_triplets: HashSet<_> = filtered + .triplet_iter() + .cloned_values() + .collect(); + + prop_assert_eq!(filtered_triplets, triplet_subset); + } + + #[test] + fn csr_lower_triangle_agrees_with_dense(csr in csr_strategy()) { + let csr_lower_triangle = csr.lower_triangle(); + prop_assert_eq!(DMatrix::from(&csr_lower_triangle), DMatrix::from(&csr).lower_triangle()); + prop_assert!(csr_lower_triangle.nnz() <= csr.nnz()); + } + + #[test] + fn csr_upper_triangle_agrees_with_dense(csr in csr_strategy()) { + let csr_upper_triangle = csr.upper_triangle(); + prop_assert_eq!(DMatrix::from(&csr_upper_triangle), DMatrix::from(&csr).upper_triangle()); + prop_assert!(csr_upper_triangle.nnz() <= csr.nnz()); + } + + #[test] + fn csr_diagonal_as_matrix(csr in csr_strategy()) { + let d = csr.diagonal_as_matrix(); + let d_entries: HashSet<_> = d.triplet_iter().cloned_values().collect(); + let csr_diagonal_entries: HashSet<_> = csr + .triplet_iter() + .cloned_values() + .filter(|&(i, j, _)| i == j) + .collect(); + + prop_assert_eq!(d_entries, csr_diagonal_entries); + } } \ No newline at end of file From c988ebb4e732be3c85a13fb7694d30eb09cc9d4b Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Thu, 14 Jan 2021 17:17:52 +0100 Subject: [PATCH 064/183] Fail test compilation if compare feature is missing --- nalgebra-sparse/tests/unit.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nalgebra-sparse/tests/unit.rs b/nalgebra-sparse/tests/unit.rs index b7badffd..014a67f5 100644 --- a/nalgebra-sparse/tests/unit.rs +++ b/nalgebra-sparse/tests/unit.rs @@ -1,6 +1,6 @@ //! Unit tests -#[cfg(not(feature = "proptest-support"))] -compile_error!("Tests must be run with feature proptest-support"); +#[cfg(any(not(feature = "proptest-support"), not(feature = "compare")))] +compile_error!("Tests must be run with features `proptest-support` and `compare`"); mod unit_tests; From 3b1303d1e00d2a516a981b6ccdf759a16c17b00b Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Fri, 15 Jan 2021 15:21:50 +0100 Subject: [PATCH 065/183] Implement lower/upper triangular solve for CSC matrices --- nalgebra-sparse/src/ops/mod.rs | 2 +- nalgebra-sparse/src/ops/serial/csc.rs | 156 +++++++++++++++++- .../tests/unit_tests/ops.proptest-regressions | 2 + nalgebra-sparse/tests/unit_tests/ops.rs | 74 ++++++++- 4 files changed, 228 insertions(+), 6 deletions(-) diff --git a/nalgebra-sparse/src/ops/mod.rs b/nalgebra-sparse/src/ops/mod.rs index abf31069..dffcfa22 100644 --- a/nalgebra-sparse/src/ops/mod.rs +++ b/nalgebra-sparse/src/ops/mod.rs @@ -4,7 +4,7 @@ mod impl_std_ops; pub mod serial; /// TODO -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Op { /// TODO NoOp(T), diff --git a/nalgebra-sparse/src/ops/serial/csc.rs b/nalgebra-sparse/src/ops/serial/csc.rs index 584041db..e8f4d4e7 100644 --- a/nalgebra-sparse/src/ops/serial/csc.rs +++ b/nalgebra-sparse/src/ops/serial/csc.rs @@ -2,7 +2,7 @@ use crate::csc::CscMatrix; use crate::ops::Op; use crate::ops::serial::cs::{spmm_cs_prealloc, spmm_cs_dense, spadd_cs_prealloc}; use crate::ops::serial::OperationError; -use nalgebra::{Scalar, ClosedAdd, ClosedMul, DMatrixSliceMut, DMatrixSlice}; +use nalgebra::{Scalar, ClosedAdd, ClosedMul, DMatrixSliceMut, DMatrixSlice, RealField}; use num_traits::{Zero, One}; use std::borrow::Cow; @@ -89,4 +89,158 @@ pub fn spmm_csc_prealloc( spmm_csc_prealloc(beta, c, alpha, NoOp(a.as_ref()), NoOp(b.as_ref())) } } +} + +/// TODO +#[non_exhaustive] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SolveErrorKind { + /// TODO + Singular, +} + +/// TODO +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SolveError { + kind: SolveErrorKind, + message: String +} + +impl SolveError { + fn from_type_and_message(kind: SolveErrorKind, message: String) -> Self { + Self { + kind, + message + } + } +} + +/// Solve the lower triangular system `op(L) X = B`. +/// +/// Only the lower triangular part of L is read, and the result is stored in B. +/// +/// ## Panics +/// +/// Panics if `L` is not square, or if `L` and `B` are not dimensionally compatible. +pub fn spsolve_csc_lower_triangular<'a, T: RealField>( + l: Op<&CscMatrix>, + b: impl Into>) + -> Result<(), SolveError> +{ + let b = b.into(); + let l_matrix = l.into_inner(); + assert_eq!(l_matrix.nrows(), l_matrix.ncols(), "Matrix must be square for triangular solve."); + assert_eq!(l_matrix.nrows(), b.nrows(), "Dimension mismatch in sparse lower triangular solver."); + match l { + Op::NoOp(a) => spsolve_csc_lower_triangular_no_transpose(a, b), + Op::Transpose(a) => spsolve_csc_lower_triangular_transpose(a, b), + } +} + +fn spsolve_csc_lower_triangular_no_transpose<'a, T: RealField>( + l: &CscMatrix, + b: DMatrixSliceMut<'a, T>) + -> Result<(), SolveError> +{ + let mut x = b; + + // Solve column-by-column + for j in 0 .. x.ncols() { + let mut x_col_j = x.column_mut(j); + + for k in 0 .. l.ncols() { + let l_col_k = l.col(k); + + // Skip entries above the diagonal + // TODO: Can use exponential search here to quickly skip entries + // (we'd like to avoid using binary search as it's very cache unfriendly + // and the matrix might actually *be* lower triangular, which would induce + // a severe penalty) + let diag_csc_index = l_col_k.row_indices().iter().position(|&i| i == k); + if let Some(diag_csc_index) = diag_csc_index { + let l_kk = l_col_k.values()[diag_csc_index]; + + if l_kk != T::zero() { + // Update entry associated with diagonal + x_col_j[k] /= l_kk; + // Copy value after updating (so we don't run into the borrow checker) + let x_kj = x_col_j[k]; + + let row_indices = &l_col_k.row_indices()[(diag_csc_index + 1) ..]; + let l_values = &l_col_k.values()[(diag_csc_index + 1) ..]; + + // Note: The remaining entries are below the diagonal + for (&i, l_ik) in row_indices.iter().zip(l_values) { + let x_ij = &mut x_col_j[i]; + *x_ij -= l_ik.inlined_clone() * x_kj; + } + + x_col_j[k] = x_kj; + } else { + return spsolve_encountered_zero_diagonal(); + } + } else { + return spsolve_encountered_zero_diagonal(); + } + } + } + + Ok(()) +} + +fn spsolve_encountered_zero_diagonal() -> Result<(), SolveError> { + let message = "Matrix contains at least one diagonal entry that is zero."; + Err(SolveError::from_type_and_message(SolveErrorKind::Singular, String::from(message))) +} + +fn spsolve_csc_lower_triangular_transpose<'a, T: RealField>( + l: &CscMatrix, + b: DMatrixSliceMut<'a, T>) + -> Result<(), SolveError> +{ + let mut x = b; + + // Solve column-by-column + for j in 0 .. x.ncols() { + let mut x_col_j = x.column_mut(j); + + // Due to the transposition, we're essentially solving an upper triangular system, + // and the columns in our matrix become rows + + for i in (0 .. l.ncols()).rev() { + let l_col_i = l.col(i); + + // Skip entries above the diagonal + // TODO: Can use exponential search here to quickly skip entries + let diag_csc_index = l_col_i.row_indices().iter().position(|&k| i == k); + if let Some(diag_csc_index) = diag_csc_index { + let l_ii = l_col_i.values()[diag_csc_index]; + + if l_ii != T::zero() { + // // Update entry associated with diagonal + // x_col_j[k] /= a_kk; + + // Copy value after updating (so we don't run into the borrow checker) + let mut x_ii = x_col_j[i]; + + let row_indices = &l_col_i.row_indices()[(diag_csc_index + 1) ..]; + let a_values = &l_col_i.values()[(diag_csc_index + 1) ..]; + + // Note: The remaining entries are below the diagonal + for (&k, &l_ki) in row_indices.iter().zip(a_values) { + let x_kj = x_col_j[k]; + x_ii -= l_ki * x_kj; + } + + x_col_j[i] = x_ii / l_ii; + } else { + return spsolve_encountered_zero_diagonal(); + } + } else { + return spsolve_encountered_zero_diagonal(); + } + } + } + + Ok(()) } \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit_tests/ops.proptest-regressions b/nalgebra-sparse/tests/unit_tests/ops.proptest-regressions index 4ea85f39..ae873c3f 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.proptest-regressions +++ b/nalgebra-sparse/tests/unit_tests/ops.proptest-regressions @@ -10,3 +10,5 @@ cc dbaef9886eaad28be7cd48326b857f039d695bc0b19e9ada3304e812e984d2c3 # shrinks to cc 99e312beb498ffa79194f41501ea312dce1911878eba131282904ac97205aaa9 # shrinks to SpmmCsrDenseArgs { c, beta, alpha, trans_a, a, trans_b, b } = SpmmCsrDenseArgs { c: Matrix { data: VecStorage { data: [-1, 4, -1, -4, 2, 1, 4, -2, 1, 3, -2, 5], nrows: Dynamic { value: 2 }, ncols: Dynamic { value: 6 } } }, beta: 0, alpha: 0, trans_a: Transpose, a: CsrMatrix { sparsity_pattern: SparsityPattern { major_offsets: [0, 1, 1, 1, 1, 1, 1], minor_indices: [0], minor_dim: 2 }, values: [0] }, trans_b: Transpose, b: Matrix { data: VecStorage { data: [-1, 1, 0, -5, 4, -5, 2, 2, 4, -4, -3, -1, 1, -1, 0, 1, -3, 4, -5, 0, 1, -5, 0, 1, 1, -3, 5, 3, 5, -3, -5, 3, -1, -4, -4, -3], nrows: Dynamic { value: 6 }, ncols: Dynamic { value: 6 } } } } cc bf74259df2db6eda24eb42098e57ea1c604bb67d6d0023fa308c321027b53a43 # shrinks to (alpha, beta, c, a, b, trans_a, trans_b) = (0, 0, Matrix { data: VecStorage { data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], nrows: Dynamic { value: 4 }, ncols: Dynamic { value: 5 } } }, CsrMatrix { sparsity_pattern: SparsityPattern { major_offsets: [0, 3, 6, 9, 12], minor_indices: [0, 1, 3, 1, 2, 3, 0, 1, 2, 1, 2, 3], minor_dim: 4 }, values: [-3, 3, -3, 1, -3, 0, 2, 1, 3, 0, -4, -1] }, Matrix { data: VecStorage { data: [3, 1, 4, -5, 5, -2, -5, -1, 1, -1, 3, -3, -2, 4, 2, -1, -1, 3, -5, 5], nrows: Dynamic { value: 4 }, ncols: Dynamic { value: 5 } } }, NoTranspose, NoTranspose) cc cbd6dac45a2f610e10cf4c15d4614cdbf7dfedbfcd733e4cc65c2e79829d14b3 # shrinks to SpmmCsrArgs { c, beta, alpha, trans_a, a, trans_b, b } = SpmmCsrArgs { c: CsrMatrix { sparsity_pattern: SparsityPattern { major_offsets: [0, 0, 1, 1, 1, 1], minor_indices: [0], minor_dim: 1 }, values: [0] }, beta: 0, alpha: 1, trans_a: Transpose(true), a: CsrMatrix { sparsity_pattern: SparsityPattern { major_offsets: [0, 0, 0, 1, 1, 1], minor_indices: [1], minor_dim: 5 }, values: [-1] }, trans_b: Transpose(true), b: CsrMatrix { sparsity_pattern: SparsityPattern { major_offsets: [0, 2], minor_indices: [2, 4], minor_dim: 5 }, values: [-1, 0] } } +cc 8af78e2e41087743c8696c4d5563d59464f284662ccf85efc81ac56747d528bb # shrinks to (a, b) = (CscMatrix { cs: CsMatrix { sparsity_pattern: SparsityPattern { major_offsets: [0, 6, 12, 18, 24, 30, 33], minor_indices: [0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 1, 2, 5], minor_dim: 6 }, values: [0.4566433975117654, -0.5109683327713039, 0.0, -3.276901622678194, 0.0, -2.2065487385437095, 0.0, -0.42643054427847016, -2.9232369281581234, 0.0, 1.2913925579441763, 0.0, -1.4073766622090917, -4.795473113569459, 4.681765156869446, -0.821162215887913, 3.0315816068414794, -3.3986924718213407, -3.498903007282241, -3.1488953408335236, 3.458104636152161, -4.774694888508124, 2.603884664757498, 0.0, 0.0, -3.2650988857765535, 4.26699442646613, 0.0, -0.012223422086023561, 3.6899095325779285, -1.4264458042247958, 0.0, 3.4849193883471266] } }, Matrix { data: VecStorage { data: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.9513896933988457, -4.426942420881461, 0.0, 0.0, 0.0, -0.28264084049240257], nrows: Dynamic { value: 6 }, ncols: Dynamic { value: 2 } } }) +cc a4effd988fe352146fca365875e108ecf4f7d41f6ad54683e923ca6ce712e5d0 # shrinks to (a, b) = (CscMatrix { cs: CsMatrix { sparsity_pattern: SparsityPattern { major_offsets: [0, 5, 11, 17, 22, 27, 31], minor_indices: [0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 3, 4, 5, 1, 2, 3, 4, 5, 0, 1, 3, 5], minor_dim: 6 }, values: [-2.24935510943371, -2.2288203680206227, 0.0, -1.029740125494273, 0.0, 0.0, 0.22632926934348507, -0.9123245943877407, 0.0, 3.8564332876991827, 0.0, 0.0, 0.0, -0.8235065737081717, 1.9337984046721566, 0.11003468246027737, -3.422112890579867, -3.7824068893569196, 0.0, -0.021700572247226546, -4.914783069982362, 0.6227245544506541, 0.0, 0.0, -4.411368879922364, -0.00013623178651567258, -2.613658177661417, -2.2783292441548637, 0.0, 1.351859435890189, -0.021345159183605134] } }, Matrix { data: VecStorage { data: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -4.519417607973404, 0.0, 0.0, 0.0, -0.21238483334481817], nrows: Dynamic { value: 6 }, ncols: Dynamic { value: 3 } } }) diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index a8dc248b..a814f094 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -1,8 +1,8 @@ -use crate::common::{csc_strategy, csr_strategy, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ, - PROPTEST_I32_VALUE_STRATEGY, non_zero_i32_value_strategy}; +use crate::common::{csc_strategy, csr_strategy, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ, PROPTEST_I32_VALUE_STRATEGY, non_zero_i32_value_strategy, value_strategy}; use nalgebra_sparse::ops::serial::{spmm_csr_dense, spmm_csc_dense, spadd_pattern, spmm_pattern, spadd_csr_prealloc, spadd_csc_prealloc, - spmm_csr_prealloc, spmm_csc_prealloc}; + spmm_csr_prealloc, spmm_csc_prealloc, + spsolve_csc_lower_triangular}; use nalgebra_sparse::ops::{Op}; use nalgebra_sparse::csr::CsrMatrix; use nalgebra_sparse::csc::CscMatrix; @@ -10,10 +10,12 @@ use nalgebra_sparse::proptest::{csc, csr, sparsity_pattern}; use nalgebra_sparse::pattern::SparsityPattern; use nalgebra::{DMatrix, Scalar, DMatrixSliceMut, DMatrixSlice}; -use nalgebra::proptest::matrix; +use nalgebra::proptest::{matrix, vector}; use proptest::prelude::*; +use matrixcompare::prop_assert_matrix_eq; + use std::panic::catch_unwind; use std::sync::Arc; @@ -259,6 +261,36 @@ fn spmm_csc_prealloc_args_strategy() -> impl Strategy> { }) } +fn csc_invertible_diagonal() -> impl Strategy> { + let non_zero_values = value_strategy::() + .prop_filter("Only non-zeros values accepted", |x| x != &0.0); + + vector(non_zero_values, PROPTEST_MATRIX_DIM) + .prop_map(|d| { + let mut matrix = CscMatrix::identity(d.len()); + matrix.values_mut().clone_from_slice(&d.as_slice()); + matrix + }) +} + +fn csc_square_with_non_zero_diagonals() -> impl Strategy> { + csc_invertible_diagonal() + .prop_flat_map(|d| { + csc(value_strategy::(), Just(d.nrows()), Just(d.nrows()), PROPTEST_MAX_NNZ) + .prop_map(move |mut c| { + for (i, j, v) in c.triplet_iter_mut() { + if i == j { + *v = 0.0; + } + } + + // Return the sum of a matrix with zero diagonals and an invertible diagonal + // matrix + c + &d + }) + }) +} + /// Helper function to help us call dense GEMM with our `Op` type fn dense_gemm<'a>(beta: i32, c: impl Into>, @@ -1115,4 +1147,38 @@ proptest! { prop_assert_eq!(&a * &b, &DMatrix::from(&a) * &b); } + #[test] + fn csc_solve_lower_triangular_no_transpose( + // A CSC matrix `a` and a dimensionally compatible dense matrix `b` + (a, b) + in csc_square_with_non_zero_diagonals() + .prop_flat_map(|a| { + let nrows = a.nrows(); + (Just(a), matrix(value_strategy::(), nrows, PROPTEST_MATRIX_DIM)) + })) + { + let mut x = b.clone(); + spsolve_csc_lower_triangular(Op::NoOp(&a), &mut x).unwrap(); + + let a_lower = a.lower_triangle(); + prop_assert_matrix_eq!(&a_lower * &x, &b, comp = abs, tol = 1e-6); + } + + #[test] + fn csc_solve_lower_triangular_transpose( + // A CSC matrix `a` and a dimensionally compatible dense matrix `b` (with a transposed) + (a, b) + in csc_square_with_non_zero_diagonals() + .prop_flat_map(|a| { + let ncols = a.ncols(); + (Just(a), matrix(value_strategy::(), ncols, PROPTEST_MATRIX_DIM)) + })) + { + let mut x = b.clone(); + spsolve_csc_lower_triangular(Op::Transpose(&a), &mut x).unwrap(); + + let a_lower = a.lower_triangle(); + prop_assert_matrix_eq!(&a_lower.transpose() * &x, &b, comp = abs, tol = 1e-6); + } + } \ No newline at end of file From ef3477f411278c3e97845f0002a4c7138b71a331 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 19 Jan 2021 14:15:19 +0100 Subject: [PATCH 066/183] Remove Zero bound for transpose and impl SparsityPattern::transpose --- nalgebra-sparse/src/convert/impl_std_ops.rs | 4 +- nalgebra-sparse/src/convert/serial.rs | 99 ++++------------- nalgebra-sparse/src/cs.rs | 111 ++++++++++++++++++-- nalgebra-sparse/src/csc.rs | 4 +- nalgebra-sparse/src/csr.rs | 4 +- nalgebra-sparse/src/pattern.rs | 19 ++++ 6 files changed, 151 insertions(+), 90 deletions(-) diff --git a/nalgebra-sparse/src/convert/impl_std_ops.rs b/nalgebra-sparse/src/convert/impl_std_ops.rs index 0a3fbdf6..66f70408 100644 --- a/nalgebra-sparse/src/convert/impl_std_ops.rs +++ b/nalgebra-sparse/src/convert/impl_std_ops.rs @@ -107,7 +107,7 @@ impl<'a, T> From<&'a CscMatrix> for DMatrix impl<'a, T> From<&'a CscMatrix> for CsrMatrix where - T: Scalar + Zero + T: Scalar { fn from(matrix: &'a CscMatrix) -> Self { convert_csc_csr(matrix) @@ -116,7 +116,7 @@ impl<'a, T> From<&'a CscMatrix> for CsrMatrix impl<'a, T> From<&'a CsrMatrix> for CscMatrix where - T: Scalar + Zero + T: Scalar { fn from(matrix: &'a CsrMatrix) -> Self { convert_csr_csc(matrix) diff --git a/nalgebra-sparse/src/convert/serial.rs b/nalgebra-sparse/src/convert/serial.rs index f82020f4..4ddefcca 100644 --- a/nalgebra-sparse/src/convert/serial.rs +++ b/nalgebra-sparse/src/convert/serial.rs @@ -1,12 +1,15 @@ //! TODO -use crate::coo::CooMatrix; -use crate::csr::CsrMatrix; -use nalgebra::{DMatrix, Scalar, Matrix, Dim, ClosedAdd}; -use nalgebra::storage::Storage; +use std::ops::Add; + use num_traits::Zero; -use std::ops::{Add}; +use nalgebra::{ClosedAdd, Dim, DMatrix, Matrix, Scalar}; +use nalgebra::storage::Storage; + +use crate::coo::CooMatrix; +use crate::cs; use crate::csc::CscMatrix; +use crate::csr::CsrMatrix; /// TODO pub fn convert_dense_coo(dense: &Matrix) -> CooMatrix @@ -192,13 +195,13 @@ pub fn convert_dense_csc(dense: &Matrix) -> CscMatrix /// TODO pub fn convert_csr_csc(csr: &CsrMatrix) -> CscMatrix where - T: Scalar + Zero + T: Scalar { - let (offsets, indices, values) = transpose_cs(csr.nrows(), - csr.ncols(), - csr.row_offsets(), - csr.col_indices(), - csr.values()); + let (offsets, indices, values) = cs::transpose_cs(csr.nrows(), + csr.ncols(), + csr.row_offsets(), + csr.col_indices(), + csr.values()); // TODO: Avoid data validity check? CscMatrix::try_from_csc_data(csr.nrows(), csr.ncols(), offsets, indices, values) @@ -208,13 +211,13 @@ where /// TODO pub fn convert_csc_csr(csc: &CscMatrix) -> CsrMatrix where - T: Scalar + Zero + T: Scalar { - let (offsets, indices, values) = transpose_cs(csc.ncols(), - csc.nrows(), - csc.col_offsets(), - csc.row_indices(), - csc.values()); + let (offsets, indices, values) = cs::transpose_cs(csc.ncols(), + csc.nrows(), + csc.col_offsets(), + csc.row_indices(), + csc.values()); // TODO: Avoid data validity check? CsrMatrix::try_from_csr_data(csc.nrows(), csc.ncols(), offsets, indices, values) @@ -326,7 +329,7 @@ fn coo_to_unsorted_cs( major_offsets[*major_idx] += 1; } - convert_counts_to_offsets(major_offsets); + cs::convert_counts_to_offsets(major_offsets); { // TODO: Instead of allocating a whole new vector storing the current counts, @@ -377,66 +380,6 @@ fn sort_lane( apply_permutation(values_result, values, permutation); } -/// Transposes the compressed format. -/// -/// This means that major and minor roles are switched. This is used for converting between CSR -/// and CSC formats. -fn transpose_cs(major_dim: usize, - minor_dim: usize, - source_major_offsets: &[usize], - source_minor_indices: &[usize], - values: &[T]) - -> (Vec, Vec, Vec) -where - T: Scalar + Zero -{ - assert_eq!(source_major_offsets.len(), major_dim + 1); - assert_eq!(source_minor_indices.len(), values.len()); - let nnz = values.len(); - - // Count the number of occurences of each minor index - let mut minor_counts = vec![0; minor_dim]; - for minor_idx in source_minor_indices { - minor_counts[*minor_idx] += 1; - } - convert_counts_to_offsets(&mut minor_counts); - let mut target_offsets = minor_counts; - target_offsets.push(nnz); - let mut target_indices = vec![usize::MAX; nnz]; - let mut target_values = vec![T::zero(); nnz]; - - // Keep track of how many entries we have placed in each target major lane - let mut current_target_major_counts = vec![0; minor_dim]; - - for source_major_idx in 0 .. major_dim { - let source_lane_begin = source_major_offsets[source_major_idx]; - let source_lane_end = source_major_offsets[source_major_idx + 1]; - let source_lane_indices = &source_minor_indices[source_lane_begin .. source_lane_end]; - let source_lane_values = &values[source_lane_begin .. source_lane_end]; - - for (&source_minor_idx, val) in source_lane_indices.iter().zip(source_lane_values) { - // Compute the offset in the target data for this particular source entry - let target_lane_count = &mut current_target_major_counts[source_minor_idx]; - let entry_offset = target_offsets[source_minor_idx] + *target_lane_count; - target_indices[entry_offset] = source_major_idx; - target_values[entry_offset] = val.inlined_clone(); - *target_lane_count += 1; - } - } - - (target_offsets, target_indices, target_values) -} - -fn convert_counts_to_offsets(counts: &mut [usize]) { - // Convert the counts to an offset - let mut offset = 0; - for i_offset in counts.iter_mut() { - let count = *i_offset; - *i_offset = offset; - offset += count; - } -} - // TODO: Move this into `utils` or something? fn apply_permutation(out_slice: &mut [T], in_slice: &[T], permutation: &[usize]) { assert_eq!(out_slice.len(), in_slice.len()); diff --git a/nalgebra-sparse/src/cs.rs b/nalgebra-sparse/src/cs.rs index 634ce413..30d73007 100644 --- a/nalgebra-sparse/src/cs.rs +++ b/nalgebra-sparse/src/cs.rs @@ -1,12 +1,14 @@ -use crate::pattern::SparsityPattern; -use crate::{SparseEntry, SparseEntryMut}; - -use std::sync::Arc; -use std::ops::Range; use std::mem::replace; +use std::ops::Range; +use std::sync::Arc; + use num_traits::One; + use nalgebra::Scalar; +use crate::{SparseEntry, SparseEntryMut}; +use crate::pattern::SparsityPattern; + /// An abstract compressed matrix. /// /// For the time being, this is only used internally to share implementation between @@ -396,4 +398,101 @@ impl<'a, T> CsLaneMut<'a, T> { self.values, global_minor_index) } -} \ No newline at end of file +} + +/// Helper struct for working with uninitialized data in vectors. +/// TODO: This doesn't belong here. +struct UninitVec { + vec: Vec +} + +impl UninitVec { + pub fn from_len(len: usize) -> Self { + Self { + vec: Vec::with_capacity(len) + } + } + + /// Sets the element associated with the given index to the provided value. + /// + /// Must be called exactly once per index, otherwise results in undefined behavior. + pub unsafe fn set(&mut self, index: usize, value: T) { + self.vec.as_mut_ptr().add(index).write(value) + } + + /// Marks the vector data as initialized by returning a full vector. + /// + /// It is undefined behavior to call this function unless *all* elements have been written to + /// exactly once. + pub unsafe fn assume_init(mut self) -> Vec { + self.vec.set_len(self.vec.capacity()); + self.vec + } +} + +/// Transposes the compressed format. +/// +/// This means that major and minor roles are switched. This is used for converting between CSR +/// and CSC formats. +pub fn transpose_cs( + major_dim: usize, + minor_dim: usize, + source_major_offsets: &[usize], + source_minor_indices: &[usize], + values: &[T]) + -> (Vec, Vec, Vec) +where + T: Scalar +{ + assert_eq!(source_major_offsets.len(), major_dim + 1); + assert_eq!(source_minor_indices.len(), values.len()); + let nnz = values.len(); + + // Count the number of occurences of each minor index + let mut minor_counts = vec![0; minor_dim]; + for minor_idx in source_minor_indices { + minor_counts[*minor_idx] += 1; + } + convert_counts_to_offsets(&mut minor_counts); + let mut target_offsets = minor_counts; + target_offsets.push(nnz); + let mut target_indices = vec![usize::MAX; nnz]; + + // We have to use uninitialized storage, because we don't have any kind of "default" value + // available for `T`. Unfortunately this necessitates some small amount of unsafe code + let mut target_values = UninitVec::from_len(nnz); + + // Keep track of how many entries we have placed in each target major lane + let mut current_target_major_counts = vec![0; minor_dim]; + + for source_major_idx in 0 .. major_dim { + let source_lane_begin = source_major_offsets[source_major_idx]; + let source_lane_end = source_major_offsets[source_major_idx + 1]; + let source_lane_indices = &source_minor_indices[source_lane_begin .. source_lane_end]; + let source_lane_values = &values[source_lane_begin .. source_lane_end]; + + for (&source_minor_idx, val) in source_lane_indices.iter().zip(source_lane_values) { + // Compute the offset in the target data for this particular source entry + let target_lane_count = &mut current_target_major_counts[source_minor_idx]; + let entry_offset = target_offsets[source_minor_idx] + *target_lane_count; + target_indices[entry_offset] = source_major_idx; + unsafe { target_values.set(entry_offset, val.inlined_clone()); } + *target_lane_count += 1; + } + } + + // At this point, we should have written to each element in target_values exactly once, + // so initialization should be sound + let target_values = unsafe { target_values.assume_init() }; + (target_offsets, target_indices, target_values) +} + +pub fn convert_counts_to_offsets(counts: &mut [usize]) { + // Convert the counts to an offset + let mut offset = 0; + for i_offset in counts.iter_mut() { + let count = *i_offset; + *i_offset = offset; + offset += count; + } +} diff --git a/nalgebra-sparse/src/csc.rs b/nalgebra-sparse/src/csc.rs index 1d8b8970..c3d79508 100644 --- a/nalgebra-sparse/src/csc.rs +++ b/nalgebra-sparse/src/csc.rs @@ -7,7 +7,7 @@ use crate::cs::{CsMatrix, CsLane, CsLaneMut, CsLaneIter, CsLaneIterMut}; use std::sync::Arc; use std::slice::{IterMut, Iter}; -use num_traits::{Zero, One}; +use num_traits::{One}; use nalgebra::Scalar; /// A CSC representation of a sparse matrix. @@ -368,7 +368,7 @@ impl CscMatrix { impl CscMatrix where - T: Scalar + Zero + T: Scalar { /// Compute the transpose of the matrix. pub fn transpose(&self) -> CscMatrix { diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index 1f621c86..81f8191c 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -5,7 +5,7 @@ use crate::csc::CscMatrix; use crate::cs::{CsMatrix, CsLaneIterMut, CsLaneIter, CsLane, CsLaneMut}; use nalgebra::Scalar; -use num_traits::{Zero, One}; +use num_traits::{One}; use std::sync::Arc; use std::slice::{IterMut, Iter}; @@ -368,7 +368,7 @@ impl CsrMatrix { impl CsrMatrix where - T: Scalar + Zero + T: Scalar { /// Compute the transpose of the matrix. pub fn transpose(&self) -> CsrMatrix { diff --git a/nalgebra-sparse/src/pattern.rs b/nalgebra-sparse/src/pattern.rs index f95f23c8..6a0cf851 100644 --- a/nalgebra-sparse/src/pattern.rs +++ b/nalgebra-sparse/src/pattern.rs @@ -2,6 +2,7 @@ use crate::SparseFormatError; use std::fmt; use std::error::Error; +use crate::cs::transpose_cs; /// A representation of the sparsity pattern of a CSR or CSC matrix. /// @@ -204,6 +205,24 @@ impl SparsityPattern { pub fn disassemble(self) -> (Vec, Vec) { (self.major_offsets, self.minor_indices) } + + /// TODO + pub fn transpose(&self) -> Self { + // By using unit () values, we can use the same routines as for CSR/CSC matrices + let values = vec![(); self.nnz()]; + let (new_offsets, new_indices, _) = transpose_cs( + self.major_dim(), + self.minor_dim(), + self.major_offsets(), + self.minor_indices(), + &values); + // TODO: Skip checks + Self::try_from_offsets_and_indices(self.minor_dim(), + self.major_dim(), + new_offsets, + new_indices) + .expect("Internal error: Transpose should never fail.") + } } /// Error type for `SparsityPattern` format errors. From 4b395523dd54046c9658f3c59f278927203e682e Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 19 Jan 2021 15:02:23 +0100 Subject: [PATCH 067/183] Fix issue with UninitVec and zero-sized types --- nalgebra-sparse/src/cs.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/nalgebra-sparse/src/cs.rs b/nalgebra-sparse/src/cs.rs index 30d73007..e79557b2 100644 --- a/nalgebra-sparse/src/cs.rs +++ b/nalgebra-sparse/src/cs.rs @@ -403,13 +403,17 @@ impl<'a, T> CsLaneMut<'a, T> { /// Helper struct for working with uninitialized data in vectors. /// TODO: This doesn't belong here. struct UninitVec { - vec: Vec + vec: Vec, + len: usize } impl UninitVec { pub fn from_len(len: usize) -> Self { Self { - vec: Vec::with_capacity(len) + vec: Vec::with_capacity(len), + // We need to store len separately, because for zero-sized types, + // Vec::with_capacity(len) does not give vec.capacity() == len + len } } @@ -425,7 +429,7 @@ impl UninitVec { /// It is undefined behavior to call this function unless *all* elements have been written to /// exactly once. pub unsafe fn assume_init(mut self) -> Vec { - self.vec.set_len(self.vec.capacity()); + self.vec.set_len(self.len); self.vec } } From cd9c3baeada0e7442041a3c04e53b741c6d6516b Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 19 Jan 2021 15:20:01 +0100 Subject: [PATCH 068/183] Clean up CscCholesky --- nalgebra-sparse/src/factorization/cholesky.rs | 103 ++++++------------ 1 file changed, 36 insertions(+), 67 deletions(-) diff --git a/nalgebra-sparse/src/factorization/cholesky.rs b/nalgebra-sparse/src/factorization/cholesky.rs index 3e9c032e..9dfd20ff 100644 --- a/nalgebra-sparse/src/factorization/cholesky.rs +++ b/nalgebra-sparse/src/factorization/cholesky.rs @@ -4,10 +4,9 @@ use crate::pattern::SparsityPattern; use crate::csc::CscMatrix; use core::{mem, iter}; -use nalgebra::{U1, VectorN, Dynamic, Scalar, RealField}; -use num_traits::Zero; +use nalgebra::{Scalar, RealField}; use std::sync::Arc; -use std::ops::Add; +use std::fmt::{Display, Formatter}; pub struct CscSymbolicCholesky { // Pattern of the original matrix that was decomposed @@ -21,37 +20,11 @@ impl CscSymbolicCholesky { pub fn factor(pattern: &Arc) -> Self { assert_eq!(pattern.major_dim(), pattern.minor_dim(), "Major and minor dimensions must be the same (square matrix)."); - - // TODO: Temporary stopgap solution to make things work until we can refactor - #[derive(Copy, Clone, PartialEq, Eq, Debug)] - struct DummyVal; - impl Zero for DummyVal { - fn zero() -> Self { - DummyVal - } - - fn is_zero(&self) -> bool { - true - } - } - - impl Add for DummyVal { - type Output = Self; - - fn add(self, rhs: DummyVal) -> Self::Output { - rhs - } - } - - let dummy_vals = vec![DummyVal; pattern.nnz()]; - let dummy_csc = CscMatrix::try_from_pattern_and_values(Arc::clone(pattern), dummy_vals) - .unwrap(); - let (l, u) = nonzero_pattern(&dummy_csc); - // TODO: Don't clone unnecessarily + let (l_pattern, u_pattern) = nonzero_pattern(&*pattern); Self { m_pattern: Arc::clone(pattern), - l_pattern: l.pattern().as_ref().clone(), - u_pattern: u.pattern().as_ref().clone() + l_pattern, + u_pattern, } } @@ -70,10 +43,20 @@ pub struct CscCholesky { } #[derive(Debug, PartialEq, Eq, Clone)] +#[non_exhaustive] pub enum CholeskyError { - + /// The matrix is not positive definite. + NotPositiveDefinite, } +impl Display for CholeskyError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "Matrix is not positive definite") + } +} + +impl std::error::Error for CholeskyError {} + impl CscCholesky { pub fn factor(matrix: &CscMatrix) -> Result { @@ -181,9 +164,7 @@ impl CscCholesky { *self.work_x.get_unchecked_mut(p) = T::zero(); } } else { - // self.ok = false; - // TODO: Return indefinite error (i.e. encountered non-positive diagonal - unimplemented!() + return Err(CholeskyError::NotPositiveDefinite); } } } @@ -196,8 +177,8 @@ impl CscCholesky { -fn reach( - m: &CscMatrix, +fn reach( + pattern: &SparsityPattern, j: usize, max_j: usize, tree: &[usize], @@ -211,7 +192,7 @@ fn reach( let mut tmp = Vec::new(); let mut res = Vec::new(); - for &irow in m.col(j).row_indices() { + for &irow in pattern.lane(j) { let mut curr = irow; while curr != usize::max_value() && curr <= max_j && !marks[curr] { marks[curr] = true; @@ -223,57 +204,45 @@ fn reach( mem::swap(&mut tmp, &mut res); } - // TODO: Is this right? res.sort_unstable(); out.append(&mut res); } -fn nonzero_pattern( - m: &CscMatrix -) -> (CscMatrix, CscMatrix) { - // TODO: In order to stay as faithful as possible to the original implementation, - // we here return full matrices, whereas we actually only need to construct sparsity patterns - +fn nonzero_pattern(m: &SparsityPattern) -> (SparsityPattern, SparsityPattern) { let etree = elimination_tree(m); - let (nrows, ncols) = (m.nrows(), m.ncols()); + // Note: We assume CSC, therefore rows == minor and cols == major + let (nrows, ncols) = (m.minor_dim(), m.major_dim()); let mut rows = Vec::with_capacity(m.nnz()); - // TODO: Use a Vec here instead - let mut cols = unsafe { VectorN::new_uninitialized_generic(Dynamic::new(nrows), U1) }; + let mut col_offsets = Vec::with_capacity(ncols + 1); let mut marks = Vec::new(); // NOTE: the following will actually compute the non-zero pattern of // the transpose of l. + col_offsets.push(0); for i in 0..nrows { - cols[i] = rows.len(); reach(m, i, i, &etree, &mut marks, &mut rows); + col_offsets.push(rows.len()); } - // TODO: Get rid of this in particular - let mut vals = Vec::with_capacity(rows.len()); - unsafe { - vals.set_len(rows.len()); - } - vals.shrink_to_fit(); + let u_pattern = SparsityPattern::try_from_offsets_and_indices(nrows, ncols, col_offsets, rows) + .unwrap(); - // TODO: Remove this unnecessary conversion by using Vec throughout - let mut cols: Vec<_> = cols.iter().cloned().collect(); - cols.push(rows.len()); + // TODO: Avoid this transpose? + let l_pattern = u_pattern.transpose(); - let u = CscMatrix::try_from_csc_data(nrows, ncols, cols, rows, vals).unwrap(); - // TODO: Avoid this transpose - let l = u.transpose(); - - (l, u) + (l_pattern, u_pattern) } -fn elimination_tree(m: &CscMatrix) -> Vec { - let nrows = m.nrows(); +fn elimination_tree(pattern: &SparsityPattern) -> Vec { + // Note: The pattern is assumed to of a CSC matrix, so the number of rows is + // given by the minor dimension + let nrows = pattern.minor_dim(); let mut forest: Vec<_> = iter::repeat(usize::max_value()).take(nrows).collect(); let mut ancestor: Vec<_> = iter::repeat(usize::max_value()).take(nrows).collect(); for k in 0..nrows { - for &irow in m.col(k).row_indices() { + for &irow in pattern.lane(k) { let mut i = irow; while i < k { From d6b4f1ac2fa6a99ba2cf957d628fbf2e58c94393 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 19 Jan 2021 15:27:37 +0100 Subject: [PATCH 069/183] Add CscCholesky::factor_numerical --- nalgebra-sparse/src/factorization/cholesky.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/nalgebra-sparse/src/factorization/cholesky.rs b/nalgebra-sparse/src/factorization/cholesky.rs index 9dfd20ff..7195080f 100644 --- a/nalgebra-sparse/src/factorization/cholesky.rs +++ b/nalgebra-sparse/src/factorization/cholesky.rs @@ -58,31 +58,36 @@ impl Display for CholeskyError { impl std::error::Error for CholeskyError {} impl CscCholesky { - - pub fn factor(matrix: &CscMatrix) -> Result { - let symbolic = CscSymbolicCholesky::factor(&*matrix.pattern()); + pub fn factor_numerical(symbolic: CscSymbolicCholesky, values: &[T]) -> Result { assert_eq!(symbolic.l_pattern.nnz(), symbolic.u_pattern.nnz(), - "u is just the transpose of l, so should have the same nnz"); + "u is just the transpose of l, so should have the same nnz"); let l_nnz = symbolic.l_pattern.nnz(); let l_values = vec![T::zero(); l_nnz]; let l_factor = CscMatrix::try_from_pattern_and_values(Arc::new(symbolic.l_pattern), l_values) .unwrap(); + let (nrows, ncols) = (l_factor.nrows(), l_factor.ncols()); + let mut factorization = CscCholesky { m_pattern: symbolic.m_pattern, l_factor, u_pattern: symbolic.u_pattern, - work_x: vec![T::zero(); matrix.nrows()], + work_x: vec![T::zero(); nrows], // Fill with MAX so that things hopefully totally fail if values are not // overwritten. Might be easier to debug this way - work_c: vec![usize::MAX, matrix.ncols()], + work_c: vec![usize::MAX, ncols], }; - factorization.refactor(matrix.values())?; + factorization.refactor(values)?; Ok(factorization) } + pub fn factor(matrix: &CscMatrix) -> Result { + let symbolic = CscSymbolicCholesky::factor(&*matrix.pattern()); + Self::factor_numerical(symbolic, matrix.values()) + } + pub fn refactor(&mut self, values: &[T]) -> Result<(), CholeskyError> { self.decompose_left_looking(values) } From fc0c22bf78adc8c16f027b70ec14b655c5d3c66e Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 19 Jan 2021 15:52:19 +0100 Subject: [PATCH 070/183] Add CscCholesky::solve and ::solve_mut --- nalgebra-sparse/src/factorization/cholesky.rs | 25 ++++++++++++++++++- nalgebra-sparse/tests/unit_tests/cholesky.rs | 25 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/nalgebra-sparse/src/factorization/cholesky.rs b/nalgebra-sparse/src/factorization/cholesky.rs index 7195080f..2639424c 100644 --- a/nalgebra-sparse/src/factorization/cholesky.rs +++ b/nalgebra-sparse/src/factorization/cholesky.rs @@ -4,9 +4,11 @@ use crate::pattern::SparsityPattern; use crate::csc::CscMatrix; use core::{mem, iter}; -use nalgebra::{Scalar, RealField}; +use nalgebra::{Scalar, RealField, DMatrixSlice, DMatrixSliceMut, DMatrix}; use std::sync::Arc; use std::fmt::{Display, Formatter}; +use crate::ops::serial::spsolve_csc_lower_triangular; +use crate::ops::Op; pub struct CscSymbolicCholesky { // Pattern of the original matrix that was decomposed @@ -177,6 +179,27 @@ impl CscCholesky { Ok(()) } + pub fn solve<'a>(&'a self, b: impl Into>) -> DMatrix { + let b = b.into(); + let mut output = b.clone_owned(); + self.solve_mut(&mut output); + output + } + + pub fn solve_mut<'a>(&'a self, b: impl Into>) + { + let expect_msg = "If the Cholesky factorization succeeded,\ + then the triangular solve should never fail"; + // Solve LY = B + let mut y = b.into(); + spsolve_csc_lower_triangular(Op::NoOp(self.l()), &mut y) + .expect(expect_msg); + + // Solve L^T X = Y + let mut x = y; + spsolve_csc_lower_triangular(Op::Transpose(self.l()), &mut x) + .expect(expect_msg); + } } diff --git a/nalgebra-sparse/tests/unit_tests/cholesky.rs b/nalgebra-sparse/tests/unit_tests/cholesky.rs index 87517828..82cb2c42 100644 --- a/nalgebra-sparse/tests/unit_tests/cholesky.rs +++ b/nalgebra-sparse/tests/unit_tests/cholesky.rs @@ -4,6 +4,7 @@ use nalgebra_sparse::csc::CscMatrix; use nalgebra_sparse::factorization::{CscCholesky}; use nalgebra_sparse::proptest::csc; use nalgebra::{Matrix5, Vector5, Cholesky, DMatrix}; +use nalgebra::proptest::matrix; use proptest::prelude::*; use matrixcompare::{assert_matrix_eq, prop_assert_matrix_eq}; @@ -35,6 +36,30 @@ proptest! { prop_assert!(is_lower_triangular); } + #[test] + fn cholesky_solve_positive_definite( + (matrix, rhs) in positive_definite() + .prop_flat_map(|csc| { + let rhs = matrix(value_strategy::(), csc.nrows(), PROPTEST_MATRIX_DIM); + (Just(csc), rhs) + }) + ) { + let cholesky = CscCholesky::factor(&matrix).unwrap(); + + // solve_mut + { + let mut x = rhs.clone(); + cholesky.solve_mut(&mut x); + prop_assert_matrix_eq!(&matrix * &x, rhs, comp=abs, tol=1e-12); + } + + // solve + { + let x = cholesky.solve(&rhs); + prop_assert_matrix_eq!(&matrix * &x, rhs, comp=abs, tol=1e-12); + } + } + } // This is a test ported from nalgebra's "sparse" module, for the original CsCholesky impl From 9b46a43c7f6819ef8d52b186cbb30cec490fd974 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 19 Jan 2021 16:23:32 +0100 Subject: [PATCH 071/183] Add proptest regressions --- .../tests/unit_tests/cholesky.proptest-regressions | 8 ++++++++ nalgebra-sparse/tests/unit_tests/csc.proptest-regressions | 7 +++++++ 2 files changed, 15 insertions(+) create mode 100644 nalgebra-sparse/tests/unit_tests/cholesky.proptest-regressions create mode 100644 nalgebra-sparse/tests/unit_tests/csc.proptest-regressions diff --git a/nalgebra-sparse/tests/unit_tests/cholesky.proptest-regressions b/nalgebra-sparse/tests/unit_tests/cholesky.proptest-regressions new file mode 100644 index 00000000..c07db97b --- /dev/null +++ b/nalgebra-sparse/tests/unit_tests/cholesky.proptest-regressions @@ -0,0 +1,8 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 3f71c8edc555965e521e3aaf58c736240a0e333c3a9d54e8a836d7768c371215 # shrinks to matrix = CscMatrix { cs: CsMatrix { sparsity_pattern: SparsityPattern { major_offsets: [0], minor_indices: [], minor_dim: 0 }, values: [] } } +cc aef645e3184b814ef39fbb10234f12e6ff502ab515dabefafeedab5895e22b12 # shrinks to (matrix, rhs) = (CscMatrix { cs: CsMatrix { sparsity_pattern: SparsityPattern { major_offsets: [0, 4, 7, 11, 14], minor_indices: [0, 1, 2, 3, 0, 1, 2, 0, 1, 2, 3, 0, 2, 3], minor_dim: 4 }, values: [1.0, 0.0, 0.0, 0.0, 0.0, 40.90124126326177, 36.975170911665906, 0.0, 36.975170911665906, 42.51062858727923, -12.984115201530539, 0.0, -12.984115201530539, 27.73953543265418] } }, Matrix { data: VecStorage { data: [0.0, 0.0, 0.0, -4.05763092330143], nrows: Dynamic { value: 4 }, ncols: Dynamic { value: 1 } } }) diff --git a/nalgebra-sparse/tests/unit_tests/csc.proptest-regressions b/nalgebra-sparse/tests/unit_tests/csc.proptest-regressions new file mode 100644 index 00000000..a9997df0 --- /dev/null +++ b/nalgebra-sparse/tests/unit_tests/csc.proptest-regressions @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc a71b4654827840ed539b82cd7083615b0fb3f75933de6a7d91d8148a2bf34960 # shrinks to (csc, triplet_subset) = (CscMatrix { cs: CsMatrix { sparsity_pattern: SparsityPattern { major_offsets: [0, 1, 1, 1, 1, 1, 1], minor_indices: [0], minor_dim: 4 }, values: [0] } }, {}) From e655fed4fa54486af2c3a9084c44aa0eb6f59d41 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 19 Jan 2021 16:53:39 +0100 Subject: [PATCH 072/183] Replace Arc with SparsityPattern After much deliberation, I have come to the conclusion that the benefits do not really outweigh the added complexity. Even though the added complexity is relatively minor, it makes it somewhat more complicated to inter-op with other sparse linear algebra libraries in the future. --- nalgebra-sparse/src/cs.rs | 40 +++------ nalgebra-sparse/src/csc.rs | 9 +- nalgebra-sparse/src/csr.rs | 9 +- nalgebra-sparse/src/factorization/cholesky.rs | 15 ++-- nalgebra-sparse/src/ops/impl_std_ops.rs | 29 ++----- nalgebra-sparse/src/ops/serial/cs.rs | 85 ++++++++----------- nalgebra-sparse/src/proptest.rs | 5 +- nalgebra-sparse/tests/unit_tests/ops.rs | 30 +++---- 8 files changed, 86 insertions(+), 136 deletions(-) diff --git a/nalgebra-sparse/src/cs.rs b/nalgebra-sparse/src/cs.rs index e79557b2..645df265 100644 --- a/nalgebra-sparse/src/cs.rs +++ b/nalgebra-sparse/src/cs.rs @@ -1,6 +1,5 @@ use std::mem::replace; use std::ops::Range; -use std::sync::Arc; use num_traits::One; @@ -18,7 +17,7 @@ use crate::pattern::SparsityPattern; /// is obtained by associating columns with the major dimension. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CsMatrix { - sparsity_pattern: Arc, + sparsity_pattern: SparsityPattern, values: Vec } @@ -27,13 +26,13 @@ impl CsMatrix { #[inline] pub fn new(major_dim: usize, minor_dim: usize) -> Self { Self { - sparsity_pattern: Arc::new(SparsityPattern::new(major_dim, minor_dim)), + sparsity_pattern: SparsityPattern::new(major_dim, minor_dim), values: vec![], } } #[inline] - pub fn pattern(&self) -> &Arc { + pub fn pattern(&self) -> &SparsityPattern { &self.sparsity_pattern } @@ -50,24 +49,24 @@ impl CsMatrix { /// Returns the raw data represented as a tuple `(major_offsets, minor_indices, values)`. #[inline] pub fn cs_data(&self) -> (&[usize], &[usize], &[T]) { - let pattern = self.pattern().as_ref(); + let pattern = self.pattern(); (pattern.major_offsets(), pattern.minor_indices(), &self.values) } /// Returns the raw data represented as a tuple `(major_offsets, minor_indices, values)`. #[inline] pub fn cs_data_mut(&mut self) -> (&[usize], &[usize], &mut [T]) { - let pattern = self.sparsity_pattern.as_ref(); + let pattern = &mut self.sparsity_pattern; (pattern.major_offsets(), pattern.minor_indices(), &mut self.values) } #[inline] - pub fn pattern_and_values_mut(&mut self) -> (&Arc, &mut [T]) { + pub fn pattern_and_values_mut(&mut self) -> (&SparsityPattern, &mut [T]) { (&self.sparsity_pattern, &mut self.values) } #[inline] - pub fn from_pattern_and_values(pattern: Arc, values: Vec) + pub fn from_pattern_and_values(pattern: SparsityPattern, values: Vec) -> Self { assert_eq!(pattern.nnz(), values.len(), "Internal error: consumers should verify shape compatibility."); Self { @@ -84,25 +83,14 @@ impl CsMatrix { Some(row_begin .. row_end) } - pub fn take_pattern_and_values(self) -> (Arc, Vec) { + pub fn take_pattern_and_values(self) -> (SparsityPattern, Vec) { (self.sparsity_pattern, self.values) } #[inline] pub fn disassemble(self) -> (Vec, Vec, Vec) { - // Take an Arc to the pattern, which might be the sole reference to the data after - // taking the values. This is important, because it might let us avoid cloning the data - // further below. - let pattern = self.sparsity_pattern; - let values = self.values; - - // Try to take the pattern out of the `Arc` if possible, - // otherwise clone the pattern. - let owned_pattern = Arc::try_unwrap(pattern) - .unwrap_or_else(|arc| SparsityPattern::clone(&*arc)); - let (offsets, indices) = owned_pattern.disassemble(); - - (offsets, indices, values) + let (offsets, indices) = self.sparsity_pattern.disassemble(); + (offsets, indices, self.values) } /// Returns an entry for the given major/minor indices, or `None` if the indices are out @@ -151,12 +139,12 @@ impl CsMatrix { #[inline] pub fn lane_iter(&self) -> CsLaneIter { - CsLaneIter::new(self.pattern().as_ref(), self.values()) + CsLaneIter::new(self.pattern(), self.values()) } #[inline] pub fn lane_iter_mut(&mut self) -> CsLaneIterMut { - CsLaneIterMut::new(self.sparsity_pattern.as_ref(), &mut self.values) + CsLaneIterMut::new(&self.sparsity_pattern, &mut self.values) } #[inline] @@ -190,7 +178,7 @@ impl CsMatrix { new_indices) .expect("Internal error: Sparsity pattern must always be valid."); - Self::from_pattern_and_values(Arc::new(new_pattern), new_values) + Self::from_pattern_and_values(new_pattern, new_values) } } @@ -205,7 +193,7 @@ impl CsMatrix { // TODO: We should skip checks here let pattern = SparsityPattern::try_from_offsets_and_indices(n, n, offsets, indices) .unwrap(); - Self::from_pattern_and_values(Arc::new(pattern), values) + Self::from_pattern_and_values(pattern, values) } } diff --git a/nalgebra-sparse/src/csc.rs b/nalgebra-sparse/src/csc.rs index c3d79508..d7883fa3 100644 --- a/nalgebra-sparse/src/csc.rs +++ b/nalgebra-sparse/src/csc.rs @@ -5,7 +5,6 @@ use crate::pattern::{SparsityPattern, SparsityPatternFormatError, SparsityPatter use crate::csr::CsrMatrix; use crate::cs::{CsMatrix, CsLane, CsLaneMut, CsLaneIter, CsLaneIterMut}; -use std::sync::Arc; use std::slice::{IterMut, Iter}; use num_traits::{One}; use nalgebra::Scalar; @@ -95,14 +94,14 @@ impl CscMatrix { let pattern = SparsityPattern::try_from_offsets_and_indices( num_cols, num_rows, col_offsets, row_indices) .map_err(pattern_format_error_to_csc_error)?; - Self::try_from_pattern_and_values(Arc::new(pattern), values) + Self::try_from_pattern_and_values(pattern, values) } /// Try to construct a CSC matrix from a sparsity pattern and associated non-zero values. /// /// Returns an error if the number of values does not match the number of minor indices /// in the pattern. - pub fn try_from_pattern_and_values(pattern: Arc, values: Vec) + pub fn try_from_pattern_and_values(pattern: SparsityPattern, values: Vec) -> Result { if pattern.nnz() == values.len() { Ok(Self { @@ -212,7 +211,7 @@ impl CscMatrix { /// An iterator over columns in the matrix. pub fn col_iter(&self) -> CscColIter { CscColIter { - lane_iter: CsLaneIter::new(self.pattern().as_ref(), self.values()) + lane_iter: CsLaneIter::new(self.pattern(), self.values()) } } @@ -258,7 +257,7 @@ impl CscMatrix { /// The sparsity pattern is stored internally inside an `Arc`. This allows users to re-use /// the same sparsity pattern for multiple matrices without storing the same pattern multiple /// times in memory. - pub fn pattern(&self) -> &Arc { + pub fn pattern(&self) -> &SparsityPattern { self.cs.pattern() } diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index 81f8191c..dceca614 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -7,7 +7,6 @@ use crate::cs::{CsMatrix, CsLaneIterMut, CsLaneIter, CsLane, CsLaneMut}; use nalgebra::Scalar; use num_traits::{One}; -use std::sync::Arc; use std::slice::{IterMut, Iter}; /// A CSR representation of a sparse matrix. @@ -97,14 +96,14 @@ impl CsrMatrix { let pattern = SparsityPattern::try_from_offsets_and_indices( num_rows, num_cols, row_offsets, col_indices) .map_err(pattern_format_error_to_csr_error)?; - Self::try_from_pattern_and_values(Arc::new(pattern), values) + Self::try_from_pattern_and_values(pattern, values) } /// Try to construct a CSR matrix from a sparsity pattern and associated non-zero values. /// /// Returns an error if the number of values does not match the number of minor indices /// in the pattern. - pub fn try_from_pattern_and_values(pattern: Arc, values: Vec) + pub fn try_from_pattern_and_values(pattern: SparsityPattern, values: Vec) -> Result { if pattern.nnz() == values.len() { Ok(Self { @@ -214,7 +213,7 @@ impl CsrMatrix { /// An iterator over rows in the matrix. pub fn row_iter(&self) -> CsrRowIter { CsrRowIter { - lane_iter: CsLaneIter::new(self.pattern().as_ref(), self.values()) + lane_iter: CsLaneIter::new(self.pattern(), self.values()) } } @@ -260,7 +259,7 @@ impl CsrMatrix { /// The sparsity pattern is stored internally inside an `Arc`. This allows users to re-use /// the same sparsity pattern for multiple matrices without storing the same pattern multiple /// times in memory. - pub fn pattern(&self) -> &Arc { + pub fn pattern(&self) -> &SparsityPattern { self.cs.pattern() } diff --git a/nalgebra-sparse/src/factorization/cholesky.rs b/nalgebra-sparse/src/factorization/cholesky.rs index 2639424c..8ffbbe3d 100644 --- a/nalgebra-sparse/src/factorization/cholesky.rs +++ b/nalgebra-sparse/src/factorization/cholesky.rs @@ -5,26 +5,25 @@ use crate::pattern::SparsityPattern; use crate::csc::CscMatrix; use core::{mem, iter}; use nalgebra::{Scalar, RealField, DMatrixSlice, DMatrixSliceMut, DMatrix}; -use std::sync::Arc; use std::fmt::{Display, Formatter}; use crate::ops::serial::spsolve_csc_lower_triangular; use crate::ops::Op; pub struct CscSymbolicCholesky { // Pattern of the original matrix that was decomposed - m_pattern: Arc, + m_pattern: SparsityPattern, l_pattern: SparsityPattern, // u in this context is L^T, so that M = L L^T u_pattern: SparsityPattern } impl CscSymbolicCholesky { - pub fn factor(pattern: &Arc) -> Self { + pub fn factor(pattern: SparsityPattern) -> Self { assert_eq!(pattern.major_dim(), pattern.minor_dim(), "Major and minor dimensions must be the same (square matrix)."); - let (l_pattern, u_pattern) = nonzero_pattern(&*pattern); + let (l_pattern, u_pattern) = nonzero_pattern(&pattern); Self { - m_pattern: Arc::clone(pattern), + m_pattern: pattern, l_pattern, u_pattern, } @@ -37,7 +36,7 @@ impl CscSymbolicCholesky { pub struct CscCholesky { // Pattern of the original matrix - m_pattern: Arc, + m_pattern: SparsityPattern, l_factor: CscMatrix, u_pattern: SparsityPattern, work_x: Vec, @@ -66,7 +65,7 @@ impl CscCholesky { let l_nnz = symbolic.l_pattern.nnz(); let l_values = vec![T::zero(); l_nnz]; - let l_factor = CscMatrix::try_from_pattern_and_values(Arc::new(symbolic.l_pattern), l_values) + let l_factor = CscMatrix::try_from_pattern_and_values(symbolic.l_pattern, l_values) .unwrap(); let (nrows, ncols) = (l_factor.nrows(), l_factor.ncols()); @@ -86,7 +85,7 @@ impl CscCholesky { } pub fn factor(matrix: &CscMatrix) -> Result { - let symbolic = CscSymbolicCholesky::factor(&*matrix.pattern()); + let symbolic = CscSymbolicCholesky::factor(matrix.pattern().clone()); Self::factor_numerical(symbolic, matrix.values()) } diff --git a/nalgebra-sparse/src/ops/impl_std_ops.rs b/nalgebra-sparse/src/ops/impl_std_ops.rs index 12db5264..1b337397 100644 --- a/nalgebra-sparse/src/ops/impl_std_ops.rs +++ b/nalgebra-sparse/src/ops/impl_std_ops.rs @@ -7,7 +7,6 @@ use crate::ops::serial::{spadd_csr_prealloc, spadd_csc_prealloc, spadd_pattern, use nalgebra::{ClosedAdd, ClosedMul, ClosedSub, ClosedDiv, Scalar, Matrix, Dim, DMatrixSlice, DMatrix, Dynamic}; use num_traits::{Zero, One}; -use std::sync::Arc; use crate::ops::{Op}; use nalgebra::base::storage::Storage; @@ -48,11 +47,7 @@ macro_rules! impl_sp_plus_minus { impl_bin_op!($trait, $method, <'a, T>(a: &'a $matrix_type, b: &'a $matrix_type) -> $matrix_type { // If both matrices have the same pattern, then we can immediately re-use it - let pattern = if Arc::ptr_eq(a.pattern(), b.pattern()) { - Arc::clone(a.pattern()) - } else { - Arc::new(spadd_pattern(a.pattern(), b.pattern())) - }; + let pattern = spadd_pattern(a.pattern(), b.pattern()); let values = vec![T::zero(); pattern.nnz()]; // We are giving data that is valid by definition, so it is safe to unwrap below let mut result = $matrix_type::try_from_pattern_and_values(pattern, values) @@ -64,24 +59,12 @@ macro_rules! impl_sp_plus_minus { impl_bin_op!($trait, $method, <'a, T>(a: $matrix_type, b: &'a $matrix_type) -> $matrix_type { - let mut a = a; - if Arc::ptr_eq(a.pattern(), b.pattern()) { - $spadd_fn(T::one(), &mut a, $factor * T::one(), Op::NoOp(b)).unwrap(); - a - } else { - &a $sign b - } + &a $sign b }); impl_bin_op!($trait, $method, <'a, T>(a: &'a $matrix_type, b: $matrix_type) -> $matrix_type { - let mut b = b; - if Arc::ptr_eq(a.pattern(), b.pattern()) { - $spadd_fn($factor * T::one(), &mut b, T::one(), Op::NoOp(a)).unwrap(); - b - } else { - a $sign &b - } + a $sign &b }); impl_bin_op!($trait, $method, (a: $matrix_type, b: $matrix_type) -> $matrix_type { a $sign &b @@ -107,7 +90,7 @@ macro_rules! impl_spmm { impl_mul!(<'a, T>(a: &'a $matrix_type, b: &'a $matrix_type) -> $matrix_type { let pattern = $pattern_fn(a.pattern(), b.pattern()); let values = vec![T::zero(); pattern.nnz()]; - let mut result = $matrix_type::try_from_pattern_and_values(Arc::new(pattern), values) + let mut result = $matrix_type::try_from_pattern_and_values(pattern, values) .unwrap(); $spmm_fn(T::zero(), &mut result, @@ -154,7 +137,7 @@ macro_rules! impl_scalar_mul { .iter() .map(|v_i| v_i.inlined_clone() * b.inlined_clone()) .collect(); - $matrix_type::try_from_pattern_and_values(Arc::clone(a.pattern()), values).unwrap() + $matrix_type::try_from_pattern_and_values(a.pattern().clone(), values).unwrap() }); impl_mul!(<'a, T>(a: &'a $matrix_type, b: T) -> $matrix_type { a * &b @@ -251,7 +234,7 @@ macro_rules! impl_div { .iter() .map(|v_i| v_i.inlined_clone() / scalar.inlined_clone()) .collect(); - $matrix_type::try_from_pattern_and_values(Arc::clone(matrix.pattern()), new_values) + $matrix_type::try_from_pattern_and_values(matrix.pattern().clone(), new_values) .unwrap() }); impl_bin_op!(Div, div, <'a, T: ClosedDiv>(matrix: &'a $matrix_type, scalar: &'a T) -> $matrix_type { diff --git a/nalgebra-sparse/src/ops/serial/cs.rs b/nalgebra-sparse/src/ops/serial/cs.rs index 22f3b524..9a4f7e34 100644 --- a/nalgebra-sparse/src/ops/serial/cs.rs +++ b/nalgebra-sparse/src/ops/serial/cs.rs @@ -4,7 +4,6 @@ use crate::ops::serial::{OperationErrorType, OperationError}; use nalgebra::{Scalar, ClosedAdd, ClosedMul, DMatrixSliceMut, DMatrixSlice}; use num_traits::{Zero, One}; use crate::SparseEntryMut; -use std::sync::Arc; fn spmm_cs_unexpected_entry() -> OperationError { OperationError::from_type_and_message( @@ -73,64 +72,52 @@ pub fn spadd_cs_prealloc(beta: T, where T: Scalar + ClosedAdd + ClosedMul + Zero + One { - - if Arc::ptr_eq(&c.pattern(), &a.inner_ref().pattern()) { - // Special fast path: The two matrices have *exactly* the same sparsity pattern, - // so we only need to sum the value arrays - // TODO: Test this fast path - for (c_ij, a_ij) in c.values_mut().iter_mut().zip(a.inner_ref().values()) { - let (alpha, beta) = (alpha.inlined_clone(), beta.inlined_clone()); - *c_ij = beta * c_ij.inlined_clone() + alpha * a_ij.inlined_clone(); - } - Ok(()) - } else { - match a { - Op::NoOp(a) => { - for (mut c_lane_i, a_lane_i) in c.lane_iter_mut().zip(a.lane_iter()) { - if beta != T::one() { - for c_ij in c_lane_i.values_mut() { - *c_ij *= beta.inlined_clone(); - } - } - - let (mut c_minors, mut c_vals) = c_lane_i.indices_and_values_mut(); - let (a_minors, a_vals) = (a_lane_i.minor_indices(), a_lane_i.values()); - - for (a_col, a_val) in a_minors.iter().zip(a_vals) { - // TODO: Use exponential search instead of linear search. - // If C has substantially more entries in the row than A, then a line search - // will needlessly visit many entries in C. - let (c_idx, _) = c_minors.iter() - .enumerate() - .find(|(_, c_col)| *c_col == a_col) - .ok_or_else(spadd_cs_unexpected_entry)?; - c_vals[c_idx] += alpha.inlined_clone() * a_val.inlined_clone(); - c_minors = &c_minors[c_idx ..]; - c_vals = &mut c_vals[c_idx ..]; - } - } - } - Op::Transpose(a) => { + match a { + Op::NoOp(a) => { + for (mut c_lane_i, a_lane_i) in c.lane_iter_mut().zip(a.lane_iter()) { if beta != T::one() { - for c_ij in c.values_mut() { + for c_ij in c_lane_i.values_mut() { *c_ij *= beta.inlined_clone(); } } - for (i, a_lane_i) in a.lane_iter().enumerate() { - for (&j, a_val) in a_lane_i.minor_indices().iter().zip(a_lane_i.values()) { - let a_val = a_val.inlined_clone(); - let alpha = alpha.inlined_clone(); - match c.get_entry_mut(j, i).unwrap() { - SparseEntryMut::NonZero(c_ji) => { *c_ji += alpha * a_val } - SparseEntryMut::Zero => return Err(spadd_cs_unexpected_entry()), - } + let (mut c_minors, mut c_vals) = c_lane_i.indices_and_values_mut(); + let (a_minors, a_vals) = (a_lane_i.minor_indices(), a_lane_i.values()); + + for (a_col, a_val) in a_minors.iter().zip(a_vals) { + // TODO: Use exponential search instead of linear search. + // If C has substantially more entries in the row than A, then a line search + // will needlessly visit many entries in C. + let (c_idx, _) = c_minors.iter() + .enumerate() + .find(|(_, c_col)| *c_col == a_col) + .ok_or_else(spadd_cs_unexpected_entry)?; + c_vals[c_idx] += alpha.inlined_clone() * a_val.inlined_clone(); + c_minors = &c_minors[c_idx ..]; + c_vals = &mut c_vals[c_idx ..]; + } + } + } + Op::Transpose(a) => { + if beta != T::one() { + for c_ij in c.values_mut() { + *c_ij *= beta.inlined_clone(); + } + } + + for (i, a_lane_i) in a.lane_iter().enumerate() { + for (&j, a_val) in a_lane_i.minor_indices().iter().zip(a_lane_i.values()) { + let a_val = a_val.inlined_clone(); + let alpha = alpha.inlined_clone(); + match c.get_entry_mut(j, i).unwrap() { + SparseEntryMut::NonZero(c_ji) => { *c_ji += alpha * a_val } + SparseEntryMut::Zero => return Err(spadd_cs_unexpected_entry()), } } } } - Ok(()) } + Ok(()) } /// Helper functionality for implementing CSR/CSC SPMM. diff --git a/nalgebra-sparse/src/proptest.rs b/nalgebra-sparse/src/proptest.rs index 80eb88a6..1115693a 100644 --- a/nalgebra-sparse/src/proptest.rs +++ b/nalgebra-sparse/src/proptest.rs @@ -11,7 +11,6 @@ use std::iter::{repeat}; use proptest::sample::{Index}; use crate::csr::CsrMatrix; use crate::pattern::SparsityPattern; -use std::sync::Arc; use crate::csc::CscMatrix; fn dense_row_major_coord_strategy(nrows: usize, ncols: usize, nnz: usize) @@ -291,7 +290,7 @@ where (Just(pattern), values) }) .prop_map(|(pattern, values)| { - CsrMatrix::try_from_pattern_and_values(Arc::new(pattern), values) + CsrMatrix::try_from_pattern_and_values(pattern, values) .expect("Internal error: Generated CsrMatrix is invalid") }) } @@ -313,7 +312,7 @@ pub fn csc(value_strategy: T, (Just(pattern), values) }) .prop_map(|(pattern, values)| { - CscMatrix::try_from_pattern_and_values(Arc::new(pattern), values) + CscMatrix::try_from_pattern_and_values(pattern, values) .expect("Internal error: Generated CscMatrix is invalid") }) } \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index a814f094..5a9aafe7 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -17,12 +17,11 @@ use proptest::prelude::*; use matrixcompare::prop_assert_matrix_eq; use std::panic::catch_unwind; -use std::sync::Arc; /// Represents the sparsity pattern of a CSR matrix as a dense matrix with 0/1 fn dense_csr_pattern(pattern: &SparsityPattern) -> DMatrix { let boolean_csr = CsrMatrix::try_from_pattern_and_values( - Arc::new(pattern.clone()), + pattern.clone(), vec![1; pattern.nnz()]) .unwrap(); DMatrix::from(&boolean_csr) @@ -31,7 +30,7 @@ fn dense_csr_pattern(pattern: &SparsityPattern) -> DMatrix { /// Represents the sparsity pattern of a CSC matrix as a dense matrix with 0/1 fn dense_csc_pattern(pattern: &SparsityPattern) -> DMatrix { let boolean_csc = CscMatrix::try_from_pattern_and_values( - Arc::new(pattern.clone()), + pattern.clone(), vec![1; pattern.nnz()]) .unwrap(); DMatrix::from(&boolean_csc) @@ -137,8 +136,8 @@ fn spadd_csr_prealloc_args_strategy() -> impl Strategy> let beta = value_strategy.clone(); (Just(c_pattern), Just(a_pattern), c_values, a_values, alpha, beta, trans_strategy()) }).prop_map(|(c_pattern, a_pattern, c_values, a_values, alpha, beta, trans_a)| { - let c = CsrMatrix::try_from_pattern_and_values(Arc::new(c_pattern), c_values).unwrap(); - let a = CsrMatrix::try_from_pattern_and_values(Arc::new(a_pattern), a_values).unwrap(); + let c = CsrMatrix::try_from_pattern_and_values(c_pattern, c_values).unwrap(); + let a = CsrMatrix::try_from_pattern_and_values(a_pattern, a_values).unwrap(); let a = if trans_a { Op::Transpose(a.transpose()) } else { Op::NoOp(a) }; SpaddCsrArgs { c, beta, alpha, a } @@ -222,15 +221,12 @@ fn spmm_csr_prealloc_args_strategy() -> impl Strategy> { let b_values = vec![PROPTEST_I32_VALUE_STRATEGY; b_pattern.nnz()]; let c_pattern = spmm_pattern(&a_pattern, &b_pattern); let c_values = vec![PROPTEST_I32_VALUE_STRATEGY; c_pattern.nnz()]; - let a_pattern = Arc::new(a_pattern); - let b_pattern = Arc::new(b_pattern); - let c_pattern = Arc::new(c_pattern); let a = a_values.prop_map(move |values| - CsrMatrix::try_from_pattern_and_values(Arc::clone(&a_pattern), values).unwrap()); + CsrMatrix::try_from_pattern_and_values(a_pattern.clone(), values).unwrap()); let b = b_values.prop_map(move |values| - CsrMatrix::try_from_pattern_and_values(Arc::clone(&b_pattern), values).unwrap()); + CsrMatrix::try_from_pattern_and_values(b_pattern.clone(), values).unwrap()); let c = c_values.prop_map(move |values| - CsrMatrix::try_from_pattern_and_values(Arc::clone(&c_pattern), values).unwrap()); + CsrMatrix::try_from_pattern_and_values(c_pattern.clone(), values).unwrap()); let alpha = PROPTEST_I32_VALUE_STRATEGY; let beta = PROPTEST_I32_VALUE_STRATEGY; (c, beta, alpha, trans_strategy(), a, trans_strategy(), b) @@ -383,16 +379,16 @@ proptest! { // corresponding to a and b, and convert them to dense matrices. // The sum of these dense matrices will then have non-zeros in exactly the same locations // as the result of "adding" the sparsity patterns - let a_csr = CsrMatrix::try_from_pattern_and_values(Arc::new(a.clone()), vec![1; a.nnz()]) + let a_csr = CsrMatrix::try_from_pattern_and_values(a.clone(), vec![1; a.nnz()]) .unwrap(); let a_dense = DMatrix::from(&a_csr); - let b_csr = CsrMatrix::try_from_pattern_and_values(Arc::new(b.clone()), vec![1; b.nnz()]) + let b_csr = CsrMatrix::try_from_pattern_and_values(b.clone(), vec![1; b.nnz()]) .unwrap(); let b_dense = DMatrix::from(&b_csr); let c_dense = a_dense + b_dense; let c_csr = CsrMatrix::from(&c_dense); - prop_assert_eq!(&pattern_result, c_csr.pattern().as_ref()); + prop_assert_eq!(&pattern_result, c_csr.pattern()); } #[test] @@ -492,16 +488,16 @@ proptest! { // corresponding to a and b, and convert them to dense matrices. // The product of these dense matrices will then have non-zeros in exactly the same locations // as the result of "multiplying" the sparsity patterns - let a_csr = CsrMatrix::try_from_pattern_and_values(Arc::new(a.clone()), vec![1; a.nnz()]) + let a_csr = CsrMatrix::try_from_pattern_and_values(a.clone(), vec![1; a.nnz()]) .unwrap(); let a_dense = DMatrix::from(&a_csr); - let b_csr = CsrMatrix::try_from_pattern_and_values(Arc::new(b.clone()), vec![1; b.nnz()]) + let b_csr = CsrMatrix::try_from_pattern_and_values(b.clone(), vec![1; b.nnz()]) .unwrap(); let b_dense = DMatrix::from(&b_csr); let c_dense = a_dense * b_dense; let c_csr = CsrMatrix::from(&c_dense); - prop_assert_eq!(&c_pattern, c_csr.pattern().as_ref()); + prop_assert_eq!(&c_pattern, c_csr.pattern()); } #[test] From c43a2b167985d54231eaca88515a0e3a91447dc5 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 19 Jan 2021 16:56:30 +0100 Subject: [PATCH 073/183] Impl Csr/CscMatrix::into_pattern_and_values --- nalgebra-sparse/src/cs.rs | 5 +++++ nalgebra-sparse/src/csc.rs | 5 +++++ nalgebra-sparse/src/csr.rs | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/nalgebra-sparse/src/cs.rs b/nalgebra-sparse/src/cs.rs index 645df265..cf8fd382 100644 --- a/nalgebra-sparse/src/cs.rs +++ b/nalgebra-sparse/src/cs.rs @@ -93,6 +93,11 @@ impl CsMatrix { (offsets, indices, self.values) } + #[inline] + pub fn into_pattern_and_values(self) -> (SparsityPattern, Vec) { + (self.sparsity_pattern, self.values) + } + /// Returns an entry for the given major/minor indices, or `None` if the indices are out /// of bounds. pub fn get_entry(&self, major_index: usize, minor_index: usize) -> Option> { diff --git a/nalgebra-sparse/src/csc.rs b/nalgebra-sparse/src/csc.rs index d7883fa3..bbb8cd08 100644 --- a/nalgebra-sparse/src/csc.rs +++ b/nalgebra-sparse/src/csc.rs @@ -252,6 +252,11 @@ impl CscMatrix { self.cs.disassemble() } + /// Returns the sparsity pattern and values associated with this matrix. + pub fn into_pattern_and_values(self) -> (SparsityPattern, Vec) { + self.cs.into_pattern_and_values() + } + /// Returns the underlying sparsity pattern. /// /// The sparsity pattern is stored internally inside an `Arc`. This allows users to re-use diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index dceca614..f42b56f9 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -254,6 +254,11 @@ impl CsrMatrix { self.cs.disassemble() } + /// Returns the sparsity pattern and values associated with this matrix. + pub fn into_pattern_and_values(self) -> (SparsityPattern, Vec) { + self.cs.into_pattern_and_values() + } + /// Returns the underlying sparsity pattern. /// /// The sparsity pattern is stored internally inside an `Arc`. This allows users to re-use From cb0f9a5190e361038d170e4a5ef5d109cf90ef6a Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 19 Jan 2021 16:58:45 +0100 Subject: [PATCH 074/183] Add Csr/CscMatrix::pattern_and_values_mut() --- nalgebra-sparse/src/csc.rs | 6 ++++++ nalgebra-sparse/src/csr.rs | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/nalgebra-sparse/src/csc.rs b/nalgebra-sparse/src/csc.rs index bbb8cd08..c48697a8 100644 --- a/nalgebra-sparse/src/csc.rs +++ b/nalgebra-sparse/src/csc.rs @@ -257,6 +257,12 @@ impl CscMatrix { self.cs.into_pattern_and_values() } + /// Returns a reference to the sparsity pattern and a mutable reference to the values. + #[inline] + pub fn pattern_and_values_mut(&mut self) -> (&SparsityPattern, &mut [T]) { + self.cs.pattern_and_values_mut() + } + /// Returns the underlying sparsity pattern. /// /// The sparsity pattern is stored internally inside an `Arc`. This allows users to re-use diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index f42b56f9..acf12e33 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -259,6 +259,12 @@ impl CsrMatrix { self.cs.into_pattern_and_values() } + /// Returns a reference to the sparsity pattern and a mutable reference to the values. + #[inline] + pub fn pattern_and_values_mut(&mut self) -> (&SparsityPattern, &mut [T]) { + self.cs.pattern_and_values_mut() + } + /// Returns the underlying sparsity pattern. /// /// The sparsity pattern is stored internally inside an `Arc`. This allows users to re-use From 3eab45d81b8e3afc0584df5baf15d900aea350ac Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 19 Jan 2021 17:16:56 +0100 Subject: [PATCH 075/183] Replace spmm_pattern with spmm_{csr/csc}_pattern --- nalgebra-sparse/src/ops/impl_std_ops.rs | 7 +++---- nalgebra-sparse/src/ops/serial/pattern.rs | 18 +++++++++++++++++- nalgebra-sparse/tests/unit_tests/ops.rs | 15 ++++++--------- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/nalgebra-sparse/src/ops/impl_std_ops.rs b/nalgebra-sparse/src/ops/impl_std_ops.rs index 1b337397..b8841bf2 100644 --- a/nalgebra-sparse/src/ops/impl_std_ops.rs +++ b/nalgebra-sparse/src/ops/impl_std_ops.rs @@ -2,8 +2,7 @@ use crate::csr::CsrMatrix; use crate::csc::CscMatrix; use std::ops::{Add, Div, DivAssign, Mul, MulAssign, Sub, Neg}; -use crate::ops::serial::{spadd_csr_prealloc, spadd_csc_prealloc, spadd_pattern, spmm_pattern, - spmm_csr_prealloc, spmm_csc_prealloc, spmm_csc_dense, spmm_csr_dense}; +use crate::ops::serial::{spadd_csr_prealloc, spadd_csc_prealloc, spadd_pattern, spmm_csr_pattern, spmm_csr_prealloc, spmm_csc_prealloc, spmm_csc_dense, spmm_csr_dense, spmm_csc_pattern}; use nalgebra::{ClosedAdd, ClosedMul, ClosedSub, ClosedDiv, Scalar, Matrix, Dim, DMatrixSlice, DMatrix, Dynamic}; use num_traits::{Zero, One}; @@ -106,9 +105,9 @@ macro_rules! impl_spmm { } } -impl_spmm!(CsrMatrix, spmm_pattern, spmm_csr_prealloc); +impl_spmm!(CsrMatrix, spmm_csr_pattern, spmm_csr_prealloc); // Need to switch order of operations for CSC pattern -impl_spmm!(CscMatrix, |a, b| spmm_pattern(b, a), spmm_csc_prealloc); +impl_spmm!(CscMatrix, spmm_csc_pattern, spmm_csc_prealloc); /// Implements Scalar * Matrix operations for *concrete* scalar types. The reason this is necessary /// is that we are not able to implement Mul> for all T generically due to orphan rules. diff --git a/nalgebra-sparse/src/ops/serial/pattern.rs b/nalgebra-sparse/src/ops/serial/pattern.rs index 39b8a1c1..276100f3 100644 --- a/nalgebra-sparse/src/ops/serial/pattern.rs +++ b/nalgebra-sparse/src/ops/serial/pattern.rs @@ -36,7 +36,23 @@ pub fn spadd_pattern(a: &SparsityPattern, } /// Sparse matrix multiplication pattern construction, `C <- A * B`. -pub fn spmm_pattern(a: &SparsityPattern, b: &SparsityPattern) -> SparsityPattern { +/// +/// Assumes that the sparsity patterns both represent CSC matrices, and the result is also +/// represented as the sparsity pattern of a CSC matrix. +pub fn spmm_csc_pattern(a: &SparsityPattern, b: &SparsityPattern) -> SparsityPattern { + // Let C = A * B in CSC format. We note that + // C^T = B^T * A^T. + // Since the interpretation of a CSC matrix in CSR format represents the transpose of the + // matrix in CSR, we can compute C^T in *CSR format* by switching the order of a and b, + // which lets us obtain C^T in CSR format. Re-interpreting this as CSC gives us C in CSC format + spmm_csr_pattern(b, a) +} + +/// Sparse matrix multiplication pattern construction, `C <- A * B`. +/// +/// Assumes that the sparsity patterns both represent CSR matrices, and the result is also +/// represented as the sparsity pattern of a CSR matrix. +pub fn spmm_csr_pattern(a: &SparsityPattern, b: &SparsityPattern) -> SparsityPattern { assert_eq!(a.minor_dim(), b.major_dim(), "a and b must have compatible dimensions"); let mut offsets = Vec::new(); diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index 5a9aafe7..cef71378 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -1,8 +1,5 @@ use crate::common::{csc_strategy, csr_strategy, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ, PROPTEST_I32_VALUE_STRATEGY, non_zero_i32_value_strategy, value_strategy}; -use nalgebra_sparse::ops::serial::{spmm_csr_dense, spmm_csc_dense, spadd_pattern, spmm_pattern, - spadd_csr_prealloc, spadd_csc_prealloc, - spmm_csr_prealloc, spmm_csc_prealloc, - spsolve_csc_lower_triangular}; +use nalgebra_sparse::ops::serial::{spmm_csr_dense, spmm_csc_dense, spadd_pattern, spadd_csr_prealloc, spadd_csc_prealloc, spmm_csr_prealloc, spmm_csc_prealloc, spsolve_csc_lower_triangular, spmm_csr_pattern}; use nalgebra_sparse::ops::{Op}; use nalgebra_sparse::csr::CsrMatrix; use nalgebra_sparse::csc::CscMatrix; @@ -188,7 +185,7 @@ fn spadd_pattern_strategy() -> impl Strategy impl Strategy { +fn spmm_csr_pattern_strategy() -> impl Strategy { pattern_strategy() .prop_flat_map(|a| { let b = sparsity_pattern(Just(a.minor_dim()), PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ); @@ -215,11 +212,11 @@ struct SpmmCscArgs { } fn spmm_csr_prealloc_args_strategy() -> impl Strategy> { - spmm_pattern_strategy() + spmm_csr_pattern_strategy() .prop_flat_map(|(a_pattern, b_pattern)| { let a_values = vec![PROPTEST_I32_VALUE_STRATEGY; a_pattern.nnz()]; let b_values = vec![PROPTEST_I32_VALUE_STRATEGY; b_pattern.nnz()]; - let c_pattern = spmm_pattern(&a_pattern, &b_pattern); + let c_pattern = spmm_csr_pattern(&a_pattern, &b_pattern); let c_values = vec![PROPTEST_I32_VALUE_STRATEGY; c_pattern.nnz()]; let a = a_values.prop_map(move |values| CsrMatrix::try_from_pattern_and_values(a_pattern.clone(), values).unwrap()); @@ -479,10 +476,10 @@ proptest! { } #[test] - fn spmm_pattern_test((a, b) in spmm_pattern_strategy()) + fn spmm_csr_pattern_test((a, b) in spmm_csr_pattern_strategy()) { // (a, b) are multiplication-wise dimensionally compatible patterns - let c_pattern = spmm_pattern(&a, &b); + let c_pattern = spmm_csr_pattern(&a, &b); // To verify the pattern, we construct CSR matrices with positive integer entries // corresponding to a and b, and convert them to dense matrices. From 9cd1540496b49ddbee1e77a2cf1979a34a186597 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Wed, 20 Jan 2021 16:07:43 +0100 Subject: [PATCH 076/183] Improve and test proptest generators Due to a bug in proptest, we were required to pull in and modify parts of proptest::strategy::Shuffle. Once the below PR has been merged and released on crates.io, we can remove this code. https://github.com/AltSysrq/proptest/pull/217 --- nalgebra-sparse/src/proptest.rs | 22 ++- .../src/proptest/proptest_patched.rs | 146 ++++++++++++++++++ nalgebra-sparse/tests/unit_tests/proptest.rs | 139 ++++++++++++++--- 3 files changed, 281 insertions(+), 26 deletions(-) create mode 100644 nalgebra-sparse/src/proptest/proptest_patched.rs diff --git a/nalgebra-sparse/src/proptest.rs b/nalgebra-sparse/src/proptest.rs index 1115693a..757c539b 100644 --- a/nalgebra-sparse/src/proptest.rs +++ b/nalgebra-sparse/src/proptest.rs @@ -2,6 +2,10 @@ //! //! TODO: Clarify that this module needs proptest-support feature +// Contains some patched code from proptest that we can remove in the (hopefully near) future. +// See docs in file for more details. +mod proptest_patched; + use crate::coo::CooMatrix; use proptest::prelude::*; use proptest::collection::{vec, hash_map, btree_set}; @@ -16,12 +20,20 @@ use crate::csc::CscMatrix; fn dense_row_major_coord_strategy(nrows: usize, ncols: usize, nnz: usize) -> impl Strategy> { + assert!(nnz <= nrows * ncols); let mut booleans = vec![true; nnz]; booleans.append(&mut vec![false; (nrows * ncols) - nnz]); // Make sure that exactly `nnz` of the booleans are true - Just(booleans) - // Need to shuffle to make sure they are randomly distributed - .prop_shuffle() + + // TODO: We cannot use the below code because of a bug in proptest, see + // https://github.com/AltSysrq/proptest/pull/217 + // so for now we're using a patched version of the Shuffle adapter + // (see also docs in `proptest_patched` + // Just(booleans) + // // Need to shuffle to make sure they are randomly distributed + // .prop_shuffle() + + proptest_patched::Shuffle(Just(booleans)) .prop_map(move |booleans| { booleans .into_iter() @@ -265,8 +277,8 @@ pub fn sparsity_pattern( // If the required number of nonzeros is sufficiently dense, // we instead use a dense sampling dense_row_major_coord_strategy(nmajor, nminor, nnz) - .prop_map(move |triplets| { - let coords = triplets.into_iter(); + .prop_map(move |coords| { + let coords = coords.into_iter(); sparsity_pattern_from_row_major_coords(nmajor, nminor, coords) }).boxed() } diff --git a/nalgebra-sparse/src/proptest/proptest_patched.rs b/nalgebra-sparse/src/proptest/proptest_patched.rs new file mode 100644 index 00000000..fa3d9d25 --- /dev/null +++ b/nalgebra-sparse/src/proptest/proptest_patched.rs @@ -0,0 +1,146 @@ +//! Contains a modified implementation of `proptest::strategy::Shuffle`. +//! +//! The current implementation in `proptest` does not generate all permutations, which is +//! problematic for our proptest generators. The issue has been fixed in +//! https://github.com/AltSysrq/proptest/pull/217 +//! but it has yet to be merged and released. As soon as this fix makes it into a new release, +//! the modified code here can be removed. +//! +/*! + This code has been copied and adapted from + https://github.com/AltSysrq/proptest/blob/master/proptest/src/strategy/shuffle.rs + The original licensing text is: + + //- + // Copyright 2017 Jason Lingle + // + // Licensed under the Apache License, Version 2.0 or the MIT license + // , at your + // option. This file may not be copied, modified, or distributed + // except according to those terms. + +*/ + +use proptest::strategy::{Strategy, Shuffleable, NewTree, ValueTree}; +use proptest::test_runner::{TestRunner, TestRng}; +use std::cell::Cell; +use proptest::num; +use proptest::prelude::Rng; + +#[derive(Clone, Debug)] +#[must_use = "strategies do nothing unless used"] +pub struct Shuffle(pub(super) S); + +impl Strategy for Shuffle + where + S::Value: Shuffleable, +{ + type Tree = ShuffleValueTree; + type Value = S::Value; + + fn new_tree(&self, runner: &mut TestRunner) -> NewTree { + let rng = runner.new_rng(); + + self.0.new_tree(runner).map(|inner| ShuffleValueTree { + inner, + rng, + dist: Cell::new(None), + simplifying_inner: false, + }) + } +} + +#[derive(Clone, Debug)] +pub struct ShuffleValueTree { + inner: V, + rng: TestRng, + dist: Cell>, + simplifying_inner: bool, +} + +impl ShuffleValueTree + where + V::Value: Shuffleable, +{ + fn init_dist(&self, dflt: usize) -> usize { + if self.dist.get().is_none() { + self.dist.set(Some(num::usize::BinarySearch::new(dflt))); + } + + self.dist.get().unwrap().current() + } + + fn force_init_dist(&self) { + if self.dist.get().is_none() { + let _ = self.init_dist(self.current().shuffle_len()); + } + } +} + +impl ValueTree for ShuffleValueTree + where + V::Value: Shuffleable, +{ + type Value = V::Value; + + fn current(&self) -> V::Value { + let mut value = self.inner.current(); + let len = value.shuffle_len(); + // The maximum distance to swap elements. This could be larger than + // `value` if `value` has reduced size during shrinking; that's OK, + // since we only use this to filter swaps. + let max_swap = self.init_dist(len); + + // If empty collection or all swaps will be filtered out, there's + // nothing to shuffle. + if 0 == len || 0 == max_swap { + return value; + } + + let mut rng = self.rng.clone(); + + for start_index in 0..len - 1 { + // Determine the other index to be swapped, then skip the swap if + // it is too far. This ordering is critical, as it ensures that we + // generate the same sequence of random numbers every time. + + // NOTE: The below line is the whole reason for the existence of this adapted code + // We need to be able to swap with the same element, so that some elements remain in + // place rather being swapped + // let end_index = rng.gen_range(start_index + 1, len); + let end_index = rng.gen_range(start_index, len); + if end_index - start_index <= max_swap { + value.shuffle_swap(start_index, end_index); + } + } + + value + } + + fn simplify(&mut self) -> bool { + if self.simplifying_inner { + self.inner.simplify() + } else { + // Ensure that we've initialised `dist` to *something* to give + // consistent non-panicking behaviour even if called in an + // unexpected sequence. + self.force_init_dist(); + if self.dist.get_mut().as_mut().unwrap().simplify() { + true + } else { + self.simplifying_inner = true; + self.inner.simplify() + } + } + } + + fn complicate(&mut self) -> bool { + if self.simplifying_inner { + self.inner.complicate() + } else { + self.force_init_dist(); + self.dist.get_mut().as_mut().unwrap().complicate() + } + } +} \ No newline at end of file diff --git a/nalgebra-sparse/tests/unit_tests/proptest.rs b/nalgebra-sparse/tests/unit_tests/proptest.rs index 8e5731e0..46bc5130 100644 --- a/nalgebra-sparse/tests/unit_tests/proptest.rs +++ b/nalgebra-sparse/tests/unit_tests/proptest.rs @@ -6,7 +6,7 @@ fn coo_no_duplicates_generates_admissible_matrices() { #[cfg(feature = "slow-tests")] mod slow { - use nalgebra_sparse::proptest::{coo_with_duplicates, coo_no_duplicates}; + use nalgebra_sparse::proptest::{coo_with_duplicates, coo_no_duplicates, csr, csc, sparsity_pattern}; use nalgebra::DMatrix; use proptest::test_runner::TestRunner; @@ -18,6 +18,7 @@ mod slow { use std::collections::HashSet; use std::iter::repeat; use std::ops::RangeInclusive; + use nalgebra_sparse::csr::CsrMatrix; fn generate_all_possible_matrices(value_range: RangeInclusive, rows_range: RangeInclusive, @@ -73,19 +74,15 @@ mod slow { let values = -1..=1; let rows = 0..=2; let cols = 0..=3; - let strategy = coo_no_duplicates(values.clone(), rows.clone(), cols.clone(), 2 * 3); + let max_nnz = rows.end() * cols.end(); + let strategy = coo_no_duplicates(values.clone(), rows.clone(), cols.clone(), max_nnz); // Enumerate all possible combinations let all_combinations = generate_all_possible_matrices(values, rows, cols); - let mut visited_combinations = HashSet::new(); - for _ in 0..num_generated_matrices { - let tree = strategy - .new_tree(&mut runner) - .expect("Tree generation should not fail"); - let matrix = tree.current(); - visited_combinations.insert(DMatrix::from(&matrix)); - } + let visited_combinations = sample_matrix_output_space(strategy, + &mut runner, + num_generated_matrices); assert_eq!(visited_combinations.len(), all_combinations.len()); assert_eq!(visited_combinations, all_combinations, "Did not sample all possible values."); @@ -108,21 +105,17 @@ mod slow { let values = -1..=1; let rows = 0..=2; let cols = 0..=3; - let strategy = coo_with_duplicates(values.clone(), rows.clone(), cols.clone(), 2 * 3, 2); + let max_nnz = rows.end() * cols.end(); + let strategy = coo_with_duplicates(values.clone(), rows.clone(), cols.clone(), max_nnz, 2); // Enumerate all possible combinations that fit the constraints // (note: this is only a subset of the matrices that can be generated by // `coo_with_duplicates`) let all_combinations = generate_all_possible_matrices(values, rows, cols); - let mut visited_combinations = HashSet::new(); - for _ in 0..num_generated_matrices { - let tree = strategy - .new_tree(&mut runner) - .expect("Tree generation should not fail"); - let matrix = tree.current(); - visited_combinations.insert(DMatrix::from(&matrix)); - } + let visited_combinations = sample_matrix_output_space(strategy, + &mut runner, + num_generated_matrices); // Here we cannot verify that the set of visited combinations is *equal* to // all possible outcomes with the given constraints, however the @@ -131,6 +124,110 @@ mod slow { // is contained in the set of visited matrices assert!(all_combinations.is_subset(&visited_combinations)); } -} -// TODO: Tests for csr, csc and sparsity_pattern strategies \ No newline at end of file + #[cfg(feature = "slow-tests")] + #[test] + fn csr_samples_all_admissible_outputs() { + // We use a deterministic test runner to make the test "stable". + let mut runner = TestRunner::deterministic(); + + // This number needs to be high enough so that we with high probability sample + // all possible cases + let num_generated_matrices = 500000; + + let values = -1..=1; + let rows = 0..=2; + let cols = 0..=3; + let max_nnz = rows.end() * cols.end(); + let strategy = csr(values.clone(), rows.clone(), cols.clone(), max_nnz); + + let all_combinations = generate_all_possible_matrices(values, rows, cols); + + let visited_combinations = sample_matrix_output_space(strategy, + &mut runner, + num_generated_matrices); + + assert_eq!(visited_combinations.len(), all_combinations.len()); + assert_eq!(visited_combinations, all_combinations, "Did not sample all possible values."); + } + + #[cfg(feature = "slow-tests")] + #[test] + fn csc_samples_all_admissible_outputs() { + // We use a deterministic test runner to make the test "stable". + let mut runner = TestRunner::deterministic(); + + // This number needs to be high enough so that we with high probability sample + // all possible cases + let num_generated_matrices = 500000; + + let values = -1..=1; + let rows = 0..=2; + let cols = 0..=3; + let max_nnz = rows.end() * cols.end(); + let strategy = csc(values.clone(), rows.clone(), cols.clone(), max_nnz); + + let all_combinations = generate_all_possible_matrices(values, rows, cols); + + let visited_combinations = sample_matrix_output_space(strategy, + &mut runner, + num_generated_matrices); + + assert_eq!(visited_combinations.len(), all_combinations.len()); + assert_eq!(visited_combinations, all_combinations, "Did not sample all possible values."); + } + + #[cfg(feature = "slow-tests")] + #[test] + fn sparsity_pattern_samples_all_admissible_outputs() { + let mut runner = TestRunner::deterministic(); + + let num_generated_patterns = 50000; + + let major_dims = 0..=2; + let minor_dims = 0..=3; + let max_nnz = major_dims.end() * minor_dims.end(); + let strategy = sparsity_pattern(major_dims.clone(), minor_dims.clone(), max_nnz); + + let visited_patterns: HashSet<_> = sample_strategy(strategy, &mut runner) + .take(num_generated_patterns) + .map(|pattern| { + // We represent patterns as dense matrices with 1 if an entry is occupied, + // 0 otherwise + let values = vec![1; pattern.nnz()]; + CsrMatrix::try_from_pattern_and_values(pattern, values).unwrap() + }) + .map(|csr| DMatrix::from(&csr)) + .collect(); + + let all_possible_patterns = generate_all_possible_matrices(0..=1, major_dims, minor_dims); + + assert_eq!(visited_patterns.len(), all_possible_patterns.len()); + assert_eq!(visited_patterns, all_possible_patterns); + } + + fn sample_matrix_output_space(strategy: S, + runner: &mut TestRunner, + num_samples: usize) + -> HashSet> + where + S: Strategy, + DMatrix: for<'b> From<&'b S::Value> + { + sample_strategy(strategy, runner) + .take(num_samples) + .map(|matrix| DMatrix::from(&matrix)) + .collect() + } + + fn sample_strategy<'a, S: 'a + Strategy>(strategy: S, runner: &'a mut TestRunner) + -> impl 'a + Iterator { + repeat(()).map(move |_| { + let tree = strategy + .new_tree(runner) + .expect("Tree generation should not fail"); + let value = tree.current(); + value + }) + } +} \ No newline at end of file From 31c911d4fb3a82b37815aebfb5d84157dd1b8a97 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Wed, 20 Jan 2021 17:43:01 +0100 Subject: [PATCH 077/183] Change proptest strategies to use DimRange --- nalgebra-sparse/src/proptest.rs | 35 ++++++++++++++----------- nalgebra-sparse/tests/unit_tests/ops.rs | 20 +++++++------- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/nalgebra-sparse/src/proptest.rs b/nalgebra-sparse/src/proptest.rs index 757c539b..143696ad 100644 --- a/nalgebra-sparse/src/proptest.rs +++ b/nalgebra-sparse/src/proptest.rs @@ -9,13 +9,14 @@ mod proptest_patched; use crate::coo::CooMatrix; use proptest::prelude::*; use proptest::collection::{vec, hash_map, btree_set}; -use nalgebra::Scalar; +use nalgebra::{Scalar, Dim}; use std::cmp::min; use std::iter::{repeat}; use proptest::sample::{Index}; use crate::csr::CsrMatrix; use crate::pattern::SparsityPattern; use crate::csc::CscMatrix; +use nalgebra::proptest::DimRange; fn dense_row_major_coord_strategy(nrows: usize, ncols: usize, nnz: usize) -> impl Strategy> @@ -141,14 +142,14 @@ fn sparse_triplet_strategy(value_strategy: T, /// TODO pub fn coo_no_duplicates( value_strategy: T, - rows: impl Strategy + 'static, - cols: impl Strategy + 'static, + rows: impl Into, + cols: impl Into, max_nonzeros: usize) -> impl Strategy> where T: Strategy + Clone + 'static, T::Value: Scalar, { - (rows, cols) + (rows.into().to_range_inclusive(), cols.into().to_range_inclusive()) .prop_flat_map(move |(nrows, ncols)| { let max_nonzeros = min(max_nonzeros, nrows * ncols); let size_range = 0 ..= max_nonzeros; @@ -182,8 +183,8 @@ where /// for each triplet, but does not consider the sum of triplets pub fn coo_with_duplicates( value_strategy: T, - rows: impl Strategy + 'static, - cols: impl Strategy + 'static, + rows: impl Into, + cols: impl Into, max_nonzeros: usize, max_duplicates: usize) -> impl Strategy> @@ -256,12 +257,12 @@ where /// TODO pub fn sparsity_pattern( - major_lanes: impl Strategy + 'static, - minor_lanes: impl Strategy + 'static, + major_lanes: impl Into, + minor_lanes: impl Into, max_nonzeros: usize) -> impl Strategy { - (major_lanes, minor_lanes) + (major_lanes.into().to_range_inclusive(), minor_lanes.into().to_range_inclusive()) .prop_flat_map(move |(nmajor, nminor)| { let max_nonzeros = min(nmajor * nminor, max_nonzeros); (Just(nmajor), Just(nminor), 0 ..= max_nonzeros) @@ -287,15 +288,17 @@ pub fn sparsity_pattern( /// TODO pub fn csr(value_strategy: T, - rows: impl Strategy + 'static, - cols: impl Strategy + 'static, + rows: impl Into, + cols: impl Into, max_nonzeros: usize) -> impl Strategy> where T: Strategy + Clone + 'static, T::Value: Scalar, { - sparsity_pattern(rows, cols, max_nonzeros) + let rows = rows.into(); + let cols = cols.into(); + sparsity_pattern(rows.lower_bound().value() ..= rows.upper_bound().value(), cols.lower_bound().value() ..= cols.upper_bound().value(), max_nonzeros) .prop_flat_map(move |pattern| { let nnz = pattern.nnz(); let values = vec![value_strategy.clone(); nnz]; @@ -309,15 +312,17 @@ where /// TODO pub fn csc(value_strategy: T, - rows: impl Strategy + 'static, - cols: impl Strategy + 'static, + rows: impl Into, + cols: impl Into, max_nonzeros: usize) -> impl Strategy> where T: Strategy + Clone + 'static, T::Value: Scalar, { - sparsity_pattern(cols, rows, max_nonzeros) + let rows = rows.into(); + let cols = cols.into(); + sparsity_pattern(cols.lower_bound().value() ..= cols.upper_bound().value(), rows.lower_bound().value() ..= rows.upper_bound().value(), max_nonzeros) .prop_flat_map(move |pattern| { let nnz = pattern.nnz(); let values = vec![value_strategy.clone(); nnz]; diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index cef71378..3b91c3be 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -70,7 +70,7 @@ fn spmm_csr_dense_args_strategy() -> impl Strategy> let b_shape = if trans_b { (c.ncols(), common_dim) } else { (common_dim, c.ncols()) }; - let a = csr(value_strategy.clone(), Just(a_shape.0), Just(a_shape.1), max_nnz); + let a = csr(value_strategy.clone(), a_shape.0, a_shape.1, max_nnz); let b = matrix(value_strategy.clone(), b_shape.0, b_shape.1); // We use the same values for alpha, beta parameters as for matrix elements @@ -179,7 +179,7 @@ fn pattern_strategy() -> impl Strategy { fn spadd_pattern_strategy() -> impl Strategy { pattern_strategy() .prop_flat_map(|a| { - let b = sparsity_pattern(Just(a.major_dim()), Just(a.minor_dim()), PROPTEST_MAX_NNZ); + let b = sparsity_pattern(a.major_dim(), a.minor_dim(), PROPTEST_MAX_NNZ); (Just(a), b) }) } @@ -188,7 +188,7 @@ fn spadd_pattern_strategy() -> impl Strategy impl Strategy { pattern_strategy() .prop_flat_map(|a| { - let b = sparsity_pattern(Just(a.minor_dim()), PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ); + let b = sparsity_pattern(a.minor_dim(), PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ); (Just(a), b) }) } @@ -269,7 +269,7 @@ fn csc_invertible_diagonal() -> impl Strategy> { fn csc_square_with_non_zero_diagonals() -> impl Strategy> { csc_invertible_diagonal() .prop_flat_map(|d| { - csc(value_strategy::(), Just(d.nrows()), Just(d.nrows()), PROPTEST_MAX_NNZ) + csc(value_strategy::(), d.nrows(), d.nrows(), PROPTEST_MAX_NNZ) .prop_map(move |mut c| { for (i, j, v) in c.triplet_iter_mut() { if i == j { @@ -412,7 +412,7 @@ proptest! { (a, b) in csr_strategy() .prop_flat_map(|a| { - let b = csr(PROPTEST_I32_VALUE_STRATEGY, Just(a.nrows()), Just(a.ncols()), PROPTEST_MAX_NNZ); + let b = csr(PROPTEST_I32_VALUE_STRATEGY, a.nrows(), a.ncols(), PROPTEST_MAX_NNZ); (Just(a), b) })) { @@ -448,7 +448,7 @@ proptest! { (a, b) in csr_strategy() .prop_flat_map(|a| { - let b = csr(PROPTEST_I32_VALUE_STRATEGY, Just(a.nrows()), Just(a.ncols()), PROPTEST_MAX_NNZ); + let b = csr(PROPTEST_I32_VALUE_STRATEGY, a.nrows(), a.ncols(), PROPTEST_MAX_NNZ); (Just(a), b) })) { @@ -606,7 +606,7 @@ proptest! { .prop_flat_map(|a| { let max_nnz = PROPTEST_MAX_NNZ; let cols = PROPTEST_MATRIX_DIM; - let b = csr(PROPTEST_I32_VALUE_STRATEGY, Just(a.ncols()), cols, max_nnz); + let b = csr(PROPTEST_I32_VALUE_STRATEGY, a.ncols(), cols, max_nnz); (Just(a), b) })) { @@ -713,7 +713,7 @@ proptest! { .prop_flat_map(|a| { let max_nnz = PROPTEST_MAX_NNZ; let cols = PROPTEST_MATRIX_DIM; - let b = csc(PROPTEST_I32_VALUE_STRATEGY, Just(a.ncols()), cols, max_nnz); + let b = csc(PROPTEST_I32_VALUE_STRATEGY, a.ncols(), cols, max_nnz); (Just(a), b) }) .prop_map(|(a, b)| { @@ -865,7 +865,7 @@ proptest! { (a, b) in csc_strategy() .prop_flat_map(|a| { - let b = csc(PROPTEST_I32_VALUE_STRATEGY, Just(a.nrows()), Just(a.ncols()), PROPTEST_MAX_NNZ); + let b = csc(PROPTEST_I32_VALUE_STRATEGY, a.nrows(), a.ncols(), PROPTEST_MAX_NNZ); (Just(a), b) })) { @@ -901,7 +901,7 @@ proptest! { (a, b) in csc_strategy() .prop_flat_map(|a| { - let b = csc(PROPTEST_I32_VALUE_STRATEGY, Just(a.nrows()), Just(a.ncols()), PROPTEST_MAX_NNZ); + let b = csc(PROPTEST_I32_VALUE_STRATEGY, a.nrows(), a.ncols(), PROPTEST_MAX_NNZ); (Just(a), b) })) { From 7a083d50f773b1dc042c03eb6b2bc836d6279827 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Thu, 21 Jan 2021 16:40:05 +0100 Subject: [PATCH 078/183] Increase tolerance to ensure tests pass It's possible that some particularly bad inputs cause severe loss of significance in the triangular solves. This is exacerbated by the fact that the way we test the (residual) error is also prone to loss of significance, so that the error measure itself is problematic. We could maybe improve this in the future by using arbitrary- precision arithmetic to remove some sources of error and testing against appropriate bounds. --- nalgebra-sparse/tests/unit_tests/ops.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index 3b91c3be..0bc6b42a 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -1154,7 +1154,9 @@ proptest! { spsolve_csc_lower_triangular(Op::NoOp(&a), &mut x).unwrap(); let a_lower = a.lower_triangle(); - prop_assert_matrix_eq!(&a_lower * &x, &b, comp = abs, tol = 1e-6); + // We're using a high tolerance here because there are some "bad" inputs that can give + // severe loss of precision. + prop_assert_matrix_eq!(&a_lower * &x, &b, comp = abs, tol = 1e-4); } #[test] @@ -1171,7 +1173,9 @@ proptest! { spsolve_csc_lower_triangular(Op::Transpose(&a), &mut x).unwrap(); let a_lower = a.lower_triangle(); - prop_assert_matrix_eq!(&a_lower.transpose() * &x, &b, comp = abs, tol = 1e-6); + // We're using a high tolerance here because there are some "bad" inputs that can give + // severe loss of precision. + prop_assert_matrix_eq!(&a_lower.transpose() * &x, &b, comp = abs, tol = 1e-4); } } \ No newline at end of file From 15c4382fa9a4e48cc6f7855a90b27cbce57aa848 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Fri, 22 Jan 2021 14:32:13 +0100 Subject: [PATCH 079/183] Docs for most items in nalgebra-sparse --- nalgebra-sparse/Cargo.toml | 4 + nalgebra-sparse/src/convert/mod.rs | 37 ++++++- nalgebra-sparse/src/convert/serial.rs | 32 +++--- nalgebra-sparse/src/factorization/cholesky.rs | 97 +++++++++++++++++-- nalgebra-sparse/src/factorization/mod.rs | 2 + nalgebra-sparse/src/lib.rs | 33 +++++-- nalgebra-sparse/src/ops/mod.rs | 32 ++++-- nalgebra-sparse/src/ops/serial/cs.rs | 10 +- nalgebra-sparse/src/ops/serial/mod.rs | 34 +++++-- nalgebra-sparse/src/proptest.rs | 34 +++++-- 10 files changed, 251 insertions(+), 64 deletions(-) diff --git a/nalgebra-sparse/Cargo.toml b/nalgebra-sparse/Cargo.toml index 3f9f8c8a..27d90a90 100644 --- a/nalgebra-sparse/Cargo.toml +++ b/nalgebra-sparse/Cargo.toml @@ -21,3 +21,7 @@ matrixcompare-core = { version = "0.1.0", optional = true } itertools = "0.9" matrixcompare = { version = "0.2.0", features = [ "proptest-support" ] } nalgebra = { version="0.23", path = "../", features = ["compare"] } + +[package.metadata.docs.rs] +# Enable certain features when building docs for docs.rs +features = [ "proptest-support", "compare" ] \ No newline at end of file diff --git a/nalgebra-sparse/src/convert/mod.rs b/nalgebra-sparse/src/convert/mod.rs index cf627dfa..77388b22 100644 --- a/nalgebra-sparse/src/convert/mod.rs +++ b/nalgebra-sparse/src/convert/mod.rs @@ -1,4 +1,39 @@ -//! TODO +//! Routines for converting between sparse matrix formats. +//! +//! Most users should instead use the provided `From` implementations to convert between matrix +//! formats. Note that `From` implementations may not be available between all combinations of +//! sparse matrices. +//! +//! The following example illustrates how to convert between matrix formats with the `From` +//! implementations. +//! +//! ```rust +//! use nalgebra_sparse::{csr::CsrMatrix, csc::CscMatrix, coo::CooMatrix}; +//! use nalgebra::DMatrix; +//! +//! // Conversion from dense +//! let dense = DMatrix::::identity(9, 8); +//! let csr = CsrMatrix::from(&dense); +//! let csc = CscMatrix::from(&dense); +//! let coo = CooMatrix::from(&dense); +//! +//! // CSR <-> CSC +//! let _ = CsrMatrix::from(&csc); +//! let _ = CscMatrix::from(&csr); +//! +//! // CSR <-> COO +//! let _ = CooMatrix::from(&csr); +//! let _ = CsrMatrix::from(&coo); +//! +//! // CSC <-> COO +//! let _ = CooMatrix::from(&csc); +//! let _ = CscMatrix::from(&coo); +//! ``` +//! +//! The routines available here are able to provide more specialized APIs, giving +//! more control over the conversion process. The routines are organized by backends. +//! Currently, only the [`serial`] backend is available. +//! In the future, backends that offer parallel routines may become available. pub mod serial; diff --git a/nalgebra-sparse/src/convert/serial.rs b/nalgebra-sparse/src/convert/serial.rs index 4ddefcca..67a57966 100644 --- a/nalgebra-sparse/src/convert/serial.rs +++ b/nalgebra-sparse/src/convert/serial.rs @@ -1,4 +1,8 @@ -//! TODO +//! Serial routines for converting between matrix formats. +//! +//! All routines in this module are single-threaded. At present these routines offer no +//! advantage over using the [`From`] trait, but future changes to the API might offer more +//! control to the user. use std::ops::Add; use num_traits::Zero; @@ -11,7 +15,7 @@ use crate::cs; use crate::csc::CscMatrix; use crate::csr::CsrMatrix; -/// TODO +/// Converts a dense matrix to [`CooMatrix`]. pub fn convert_dense_coo(dense: &Matrix) -> CooMatrix where T: Scalar + Zero, @@ -33,9 +37,7 @@ where coo } -/// TODO -/// -/// TODO: What should the actual trait bounds be? +/// Converts a [`CooMatrix`] to a dense matrix. pub fn convert_coo_dense(coo: &CooMatrix) -> DMatrix where T: Scalar + Zero + ClosedAdd, @@ -47,7 +49,7 @@ where output } -/// TODO +/// Converts a [`CooMatrix`] to a [`CsrMatrix`]. pub fn convert_coo_csr(coo: &CooMatrix) -> CsrMatrix where T: Scalar + Zero @@ -63,7 +65,7 @@ where .expect("Internal error: Invalid CSR data during COO->CSR conversion") } -/// TODO +/// Converts a [`CsrMatrix`] to a [`CooMatrix`]. pub fn convert_csr_coo(csr: &CsrMatrix) -> CooMatrix { let mut result = CooMatrix::new(csr.nrows(), csr.ncols()); @@ -73,7 +75,7 @@ pub fn convert_csr_coo(csr: &CsrMatrix) -> CooMatrix result } -/// TODO +/// Converts a [`CsrMatrix`] to a dense matrix. pub fn convert_csr_dense(csr:& CsrMatrix) -> DMatrix where T: Scalar + ClosedAdd + Zero @@ -87,7 +89,7 @@ where output } -/// TODO +/// Converts a dense matrix to a [`CsrMatrix`]. pub fn convert_dense_csr(dense: &Matrix) -> CsrMatrix where T: Scalar + Zero, @@ -120,7 +122,7 @@ where .expect("Internal error: Invalid CsrMatrix format during dense-> CSR conversion") } -/// TODO +/// Converts a [`CooMatrix`] to a [`CscMatrix`]. pub fn convert_coo_csc(coo: &CooMatrix) -> CscMatrix where T: Scalar + Zero @@ -136,7 +138,7 @@ where .expect("Internal error: Invalid CSC data during COO->CSC conversion") } -/// TODO +/// Converts a [`CscMatrix`] to a [`CooMatrix`]. pub fn convert_csc_coo(csc: &CscMatrix) -> CooMatrix where T: Scalar @@ -148,7 +150,7 @@ where coo } -/// TODO +/// Converts a [`CscMatrix`] to a dense matrix. pub fn convert_csc_dense(csc: &CscMatrix) -> DMatrix where T: Scalar + ClosedAdd + Zero @@ -162,7 +164,7 @@ where output } -/// TODO +/// Converts a dense matrix to a [`CscMatrix`]. pub fn convert_dense_csc(dense: &Matrix) -> CscMatrix where T: Scalar + Zero, @@ -192,7 +194,7 @@ pub fn convert_dense_csc(dense: &Matrix) -> CscMatrix .expect("Internal error: Invalid CscMatrix format during dense-> CSC conversion") } -/// TODO +/// Converts a [`CsrMatrix`] to a [`CscMatrix`]. pub fn convert_csr_csc(csr: &CsrMatrix) -> CscMatrix where T: Scalar @@ -208,7 +210,7 @@ where .expect("Internal error: Invalid CSC data during CSR->CSC conversion") } -/// TODO +/// Converts a [`CscMatrix`] to a [`CsrMatrix`]. pub fn convert_csc_csr(csc: &CscMatrix) -> CsrMatrix where T: Scalar diff --git a/nalgebra-sparse/src/factorization/cholesky.rs b/nalgebra-sparse/src/factorization/cholesky.rs index 8ffbbe3d..1c4f95a8 100644 --- a/nalgebra-sparse/src/factorization/cholesky.rs +++ b/nalgebra-sparse/src/factorization/cholesky.rs @@ -1,6 +1,3 @@ -// TODO: Remove this allowance -#![allow(missing_docs)] - use crate::pattern::SparsityPattern; use crate::csc::CscMatrix; use core::{mem, iter}; @@ -9,6 +6,10 @@ use std::fmt::{Display, Formatter}; use crate::ops::serial::spsolve_csc_lower_triangular; use crate::ops::Op; +/// A symbolic sparse Cholesky factorization of a CSC matrix. +/// +/// The symbolic factorization computes the sparsity pattern of `L`, the Cholesky factor. +#[derive(Debug, Clone, PartialEq, Eq)] pub struct CscSymbolicCholesky { // Pattern of the original matrix that was decomposed m_pattern: SparsityPattern, @@ -18,6 +19,14 @@ pub struct CscSymbolicCholesky { } impl CscSymbolicCholesky { + /// Compute the symbolic factorization for a sparsity pattern belonging to a CSC matrix. + /// + /// The sparsity pattern must be symmetric. However, this is not enforced, and it is the + /// responsibility of the user to ensure that this property holds. + /// + /// # Panics + /// + /// Panics if the sparsity pattern is not square. pub fn factor(pattern: SparsityPattern) -> Self { assert_eq!(pattern.major_dim(), pattern.minor_dim(), "Major and minor dimensions must be the same (square matrix)."); @@ -29,11 +38,27 @@ impl CscSymbolicCholesky { } } + /// The pattern of the Cholesky factor `L`. pub fn l_pattern(&self) -> &SparsityPattern { &self.l_pattern } } +/// A sparse Cholesky factorization `A = L L^T` of a [`CscMatrix`]. +/// +/// The factor `L` is a sparse, lower-triangular matrix. See the article on [Wikipedia] for +/// more information. +/// +/// The implementation is a port of the `CsCholesky` implementation in `nalgebra`. It is similar +/// to Tim Davis' [`CSparse`]. The current implementation performs no fill-in reduction, and can +/// therefore be expected to produce much too dense Cholesky factors for many matrices. +/// It is therefore not currently recommended to use this implementation for serious projects. +/// +/// [`CSparse`]: https://epubs.siam.org/doi/book/10.1137/1.9780898718881 +/// [Wikipedia]: https://en.wikipedia.org/wiki/Cholesky_decomposition +// TODO: We should probably implement PartialEq/Eq, but in that case we'd probably need a +// custom implementation, due to the need to exclude the workspace arrays +#[derive(Debug, Clone)] pub struct CscCholesky { // Pattern of the original matrix m_pattern: SparsityPattern, @@ -45,6 +70,7 @@ pub struct CscCholesky { #[derive(Debug, PartialEq, Eq, Clone)] #[non_exhaustive] +/// Possible errors produced by the Cholesky factorization. pub enum CholeskyError { /// The matrix is not positive definite. NotPositiveDefinite, @@ -59,7 +85,24 @@ impl Display for CholeskyError { impl std::error::Error for CholeskyError {} impl CscCholesky { - pub fn factor_numerical(symbolic: CscSymbolicCholesky, values: &[T]) -> Result { + /// Computes the numerical Cholesky factorization associated with the given + /// symbolic factorization and the provided values. + /// + /// The values correspond to the non-zero values of the CSC matrix for which the + /// symbolic factorization was computed. + /// + /// # Errors + /// + /// Returns an error if the numerical factorization fails. This can occur if the matrix is not + /// symmetric positive definite. + /// + /// # Panics + /// + /// Panics if the number of values differ from the number of non-zeros of the sparsity pattern + /// of the matrix that was symbolically factored. + pub fn factor_numerical(symbolic: CscSymbolicCholesky, values: &[T]) + -> Result + { assert_eq!(symbolic.l_pattern.nnz(), symbolic.u_pattern.nnz(), "u is just the transpose of l, so should have the same nnz"); @@ -84,19 +127,50 @@ impl CscCholesky { Ok(factorization) } + /// Computes the Cholesky factorization of the provided matrix. + /// + /// The matrix must be symmetric positive definite. Symmetry is not checked, and it is up + /// to the user to enforce this property. + /// + /// # Errors + /// + /// Returns an error if the numerical factorization fails. This can occur if the matrix is not + /// symmetric positive definite. + /// + /// # Panics + /// + /// Panics if the matrix is not square. + /// + /// TODO: Take matrix by value or not? pub fn factor(matrix: &CscMatrix) -> Result { let symbolic = CscSymbolicCholesky::factor(matrix.pattern().clone()); Self::factor_numerical(symbolic, matrix.values()) } + /// Re-computes the factorization for a new set of non-zero values. + /// + /// This is useful when the values of a matrix changes, but the sparsity pattern remains + /// constant. + /// + /// # Errors + /// + /// Returns an error if the numerical factorization fails. This can occur if the matrix is not + /// symmetric positive definite. + /// + /// # Panics + /// + /// Panics if the number of values does not match the number of non-zeros in the sparsity + /// pattern. pub fn refactor(&mut self, values: &[T]) -> Result<(), CholeskyError> { self.decompose_left_looking(values) } + /// Returns a reference to the Cholesky factor `L`. pub fn l(&self) -> &CscMatrix { &self.l_factor } + /// Returns the Cholesky factor `L`. pub fn take_l(self) -> CscMatrix { self.l_factor } @@ -178,6 +252,11 @@ impl CscCholesky { Ok(()) } + /// Solves the system `A X = B`, where `X` and `B` are dense matrices. + /// + /// # Panics + /// + /// Panics if `B` is not square. pub fn solve<'a>(&'a self, b: impl Into>) -> DMatrix { let b = b.into(); let mut output = b.clone_owned(); @@ -185,6 +264,13 @@ impl CscCholesky { output } + /// Solves the system `AX = B`, where `X` and `B` are dense matrices. + /// + /// The result is stored in-place in `b`. + /// + /// # Panics + /// + /// Panics if `b` is not square. pub fn solve_mut<'a>(&'a self, b: impl Into>) { let expect_msg = "If the Cholesky factorization succeeded,\ @@ -201,9 +287,6 @@ impl CscCholesky { } } - - - fn reach( pattern: &SparsityPattern, j: usize, diff --git a/nalgebra-sparse/src/factorization/mod.rs b/nalgebra-sparse/src/factorization/mod.rs index 6596f530..b4adcba2 100644 --- a/nalgebra-sparse/src/factorization/mod.rs +++ b/nalgebra-sparse/src/factorization/mod.rs @@ -1,4 +1,6 @@ //! Matrix factorization for sparse matrices. +//! +//! Currently, the only factorization provided here is the [`CscCholesky`] factorization. mod cholesky; pub use cholesky::*; \ No newline at end of file diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index fe4255a8..73c0d88b 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -158,17 +158,26 @@ impl fmt::Display for SparseFormatError { impl Error for SparseFormatError {} -/// TODO +/// An entry in a sparse matrix. +/// +/// Sparse matrices do not store all their entries explicitly. Therefore, entry (i, j) in the matrix +/// can either be a reference to an explicitly stored element, or it is implicitly zero. #[derive(Debug, PartialEq, Eq)] pub enum SparseEntry<'a, T> { - /// TODO + /// The entry is a reference to an explicitly stored element. + /// + /// Note that the naming here is a misnomer: The element can still be zero, even though it + /// is explicitly stored (a so-called "explicit zero"). NonZero(&'a T), - /// TODO + /// The entry is implicitly zero, i.e. it is not explicitly stored. Zero } impl<'a, T: Clone + Zero> SparseEntry<'a, T> { - /// TODO + /// Returns the value represented by this entry. + /// + /// Either clones the underlying reference or returns zero if the entry is not explicitly + /// stored. pub fn to_value(self) -> T { match self { SparseEntry::NonZero(value) => value.clone(), @@ -177,17 +186,25 @@ impl<'a, T: Clone + Zero> SparseEntry<'a, T> { } } -/// TODO +/// A mutable entry in a sparse matrix. +/// +/// See also `SparseEntry`. #[derive(Debug, PartialEq, Eq)] pub enum SparseEntryMut<'a, T> { - /// TODO + /// The entry is a mutable reference to an explicitly stored element. + /// + /// Note that the naming here is a misnomer: The element can still be zero, even though it + /// is explicitly stored (a so-called "explicit zero"). NonZero(&'a mut T), - /// TODO + /// The entry is implicitly zero i.e. it is not explicitly stored. Zero } impl<'a, T: Clone + Zero> SparseEntryMut<'a, T> { - /// TODO + /// Returns the value represented by this entry. + /// + /// Either clones the underlying reference or returns zero if the entry is not explicitly + /// stored. pub fn to_value(self) -> T { match self { SparseEntryMut::NonZero(value) => value.clone(), diff --git a/nalgebra-sparse/src/ops/mod.rs b/nalgebra-sparse/src/ops/mod.rs index dffcfa22..2857533f 100644 --- a/nalgebra-sparse/src/ops/mod.rs +++ b/nalgebra-sparse/src/ops/mod.rs @@ -1,24 +1,37 @@ -//! TODO +//! Sparse matrix arithmetic operations. +//! +//! TODO: Explain that users should prefer to use std ops unless they need to get more performance +//! +//! The available operations are organized by backend. Currently, only the [`serial`] backend +//! is available. In the future, backends that expose parallel operations may become available. +//! +//! Many routines are able to implicitly transpose matrices involved in the operation. +//! For example, the routine [`spadd_csr_prealloc`](serial::spadd_csr_prealloc) performs the +//! operation `C <- beta * C + alpha * op(A)`. Here `op(A)` indicates that the matrix `A` can +//! either be used as-is or transposed. The notation `op(A)` is represented in code by the +//! [`Op`] enum. mod impl_std_ops; pub mod serial; -/// TODO +/// Determines whether a matrix should be transposed in a given operation. +/// +/// See the [module-level documentation](crate::ops) for the purpose of this enum. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Op { - /// TODO + /// Indicates that the matrix should be used as-is. NoOp(T), - /// TODO + /// Indicates that the matrix should be transposed. Transpose(T), } impl Op { - /// TODO + /// Returns a reference to the inner value that the operation applies to. pub fn inner_ref(&self) -> &T { self.as_ref().into_inner() } - /// TODO + /// Returns an `Op` applied to a reference of the inner value. pub fn as_ref(&self) -> Op<&T> { match self { Op::NoOp(obj) => Op::NoOp(&obj), @@ -26,15 +39,14 @@ impl Op { } } - /// TODO + /// Converts the underlying data type. pub fn convert(self) -> Op where T: Into { self.map_same_op(T::into) } - /// TODO - /// TODO: Rewrite the other functions by leveraging this one + /// Transforms the inner value with the provided function, but preserves the operation. pub fn map_same_op U>(self, f: F) -> Op { match self { Op::NoOp(obj) => Op::NoOp(f(obj)), @@ -42,7 +54,7 @@ impl Op { } } - /// TODO + /// Consumes the `Op` and returns the inner value. pub fn into_inner(self) -> T { match self { Op::NoOp(obj) | Op::Transpose(obj) => obj, diff --git a/nalgebra-sparse/src/ops/serial/cs.rs b/nalgebra-sparse/src/ops/serial/cs.rs index 9a4f7e34..dd73c304 100644 --- a/nalgebra-sparse/src/ops/serial/cs.rs +++ b/nalgebra-sparse/src/ops/serial/cs.rs @@ -1,13 +1,13 @@ use crate::cs::CsMatrix; use crate::ops::Op; -use crate::ops::serial::{OperationErrorType, OperationError}; +use crate::ops::serial::{OperationErrorKind, OperationError}; use nalgebra::{Scalar, ClosedAdd, ClosedMul, DMatrixSliceMut, DMatrixSlice}; use num_traits::{Zero, One}; use crate::SparseEntryMut; fn spmm_cs_unexpected_entry() -> OperationError { - OperationError::from_type_and_message( - OperationErrorType::InvalidPattern, + OperationError::from_kind_and_message( + OperationErrorKind::InvalidPattern, String::from("Found unexpected entry that is not present in `c`.")) } @@ -58,8 +58,8 @@ pub fn spmm_cs_prealloc( } fn spadd_cs_unexpected_entry() -> OperationError { - OperationError::from_type_and_message( - OperationErrorType::InvalidPattern, + OperationError::from_kind_and_message( + OperationErrorKind::InvalidPattern, String::from("Found entry in `op(a)` that is not present in `c`.")) } diff --git a/nalgebra-sparse/src/ops/serial/mod.rs b/nalgebra-sparse/src/ops/serial/mod.rs index ea6e6d2b..38ee266a 100644 --- a/nalgebra-sparse/src/ops/serial/mod.rs +++ b/nalgebra-sparse/src/ops/serial/mod.rs @@ -1,4 +1,12 @@ -//! TODO +//! Serial sparse matrix arithmetic routines. +//! +//! All routines are single-threaded. +//! +//! Some operations have the `prealloc` suffix. This means that they expect that the sparsity +//! pattern of the output matrix has already been pre-allocated: that is, the pattern of the result +//! of the operation fits entirely in the output pattern. In the future, there will also be +//! some operations which will be able to dynamically adapt the output pattern to fit the +//! result, but these have yet to be implemented. #[macro_use] macro_rules! assert_compatible_spmm_dims { @@ -58,24 +66,32 @@ pub use csc::*; pub use csr::*; pub use pattern::*; -/// TODO +/// A description of the error that occurred during an arithmetic operation. #[derive(Clone, Debug)] pub struct OperationError { - error_type: OperationErrorType, + error_kind: OperationErrorKind, message: String } -/// TODO +/// The different kinds of operation errors that may occur. #[non_exhaustive] #[derive(Clone, Debug)] -pub enum OperationErrorType { - /// TODO +pub enum OperationErrorKind { + /// Indicates that one or more sparsity patterns involved in the operation violate the + /// expectations of the routine. + /// + /// For example, this could indicate that the sparsity pattern of the output is not able to + /// contain the result of the operation. InvalidPattern, } impl OperationError { - /// TODO - pub fn from_type_and_message(error_type: OperationErrorType, message: String) -> Self { - Self { error_type, message } + fn from_kind_and_message(error_type: OperationErrorKind, message: String) -> Self { + Self { error_kind: error_type, message } + } + + /// The operation error kind. + pub fn kind(&self) -> &OperationErrorKind { + &self.error_kind } } \ No newline at end of file diff --git a/nalgebra-sparse/src/proptest.rs b/nalgebra-sparse/src/proptest.rs index 143696ad..43b80896 100644 --- a/nalgebra-sparse/src/proptest.rs +++ b/nalgebra-sparse/src/proptest.rs @@ -1,6 +1,10 @@ -//! TODO +//! Functionality for integrating `nalgebra-sparse` with `proptest`. //! -//! TODO: Clarify that this module needs proptest-support feature +//! **This module is only available if the `proptest-support` feature is enabled**. +//! +//! The strategies provided here are generally expected to be able to generate the entire range +//! of possible outputs given the constraints on dimensions and values. However, there are no +//! particular guarantees on the distribution of possible values. // Contains some patched code from proptest that we can remove in the (hopefully near) future. // See docs in file for more details. @@ -139,7 +143,12 @@ fn sparse_triplet_strategy(value_strategy: T, .prop_shuffle() } -/// TODO +/// A strategy for producing COO matrices without duplicate entries. +/// +/// The values of the matrix are picked from the provided `value_strategy`, while the size of the +/// generated matrices is determined by the ranges `rows` and `cols`. The number of explicitly +/// stored entries is bounded from above by `max_nonzeros`. Note that the matrix might still +/// contain explicitly stored zeroes if the value strategy is capable of generating zero values. pub fn coo_no_duplicates( value_strategy: T, rows: impl Into, @@ -177,10 +186,17 @@ where }) } -/// TODO +/// A strategy for producing COO matrices with duplicate entries. /// -/// TODO: Write note on how this strategy only maintains the constraints on values -/// for each triplet, but does not consider the sum of triplets +/// The values of the matrix are picked from the provided `value_strategy`, while the size of the +/// generated matrices is determined by the ranges `rows` and `cols`. Note that the values +/// only apply to individual entries, and since this strategy can generate duplicate entries, +/// the matrix will generally have values outside the range determined by `value_strategy` when +/// converted to other formats, since the duplicate entries are summed together in this case. +/// +/// The number of explicitly stored entries is bounded from above by `max_nonzeros`. The maximum +/// number of duplicate entries is determined by `max_duplicates`. Note that the matrix might still +/// contain explicitly stored zeroes if the value strategy is capable of generating zero values. pub fn coo_with_duplicates( value_strategy: T, rows: impl Into, @@ -255,7 +271,7 @@ where .expect("Internal error: Generated sparsity pattern is invalid") } -/// TODO +/// A strategy for generating sparsity patterns. pub fn sparsity_pattern( major_lanes: impl Into, minor_lanes: impl Into, @@ -286,7 +302,7 @@ pub fn sparsity_pattern( }) } -/// TODO +/// A strategy for generating CSR matrices. pub fn csr(value_strategy: T, rows: impl Into, cols: impl Into, @@ -310,7 +326,7 @@ where }) } -/// TODO +/// A strategy for generating CSC matrices. pub fn csc(value_strategy: T, rows: impl Into, cols: impl Into, From 1fa0de92ae0218d10b716fd2bbcc99c18977b835 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Fri, 22 Jan 2021 17:56:26 +0100 Subject: [PATCH 080/183] Preserve column dim type in CSR * Dense This is necessary so that CSR * Vector == Vector (before it would also yield a DMatrix). --- nalgebra-sparse/src/ops/impl_std_ops.rs | 31 +++++++++++++++++++------ 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/nalgebra-sparse/src/ops/impl_std_ops.rs b/nalgebra-sparse/src/ops/impl_std_ops.rs index b8841bf2..9309ff93 100644 --- a/nalgebra-sparse/src/ops/impl_std_ops.rs +++ b/nalgebra-sparse/src/ops/impl_std_ops.rs @@ -3,8 +3,10 @@ use crate::csc::CscMatrix; use std::ops::{Add, Div, DivAssign, Mul, MulAssign, Sub, Neg}; use crate::ops::serial::{spadd_csr_prealloc, spadd_csc_prealloc, spadd_pattern, spmm_csr_pattern, spmm_csr_prealloc, spmm_csc_prealloc, spmm_csc_dense, spmm_csr_dense, spmm_csc_pattern}; -use nalgebra::{ClosedAdd, ClosedMul, ClosedSub, ClosedDiv, Scalar, Matrix, Dim, - DMatrixSlice, DMatrix, Dynamic}; +use nalgebra::{ClosedAdd, ClosedMul, ClosedSub, ClosedDiv, Scalar, Matrix, MatrixMN, Dim, + DMatrixSlice, DMatrixSliceMut, DMatrix, Dynamic, DefaultAllocator, U1}; +use nalgebra::allocator::{Allocator}; +use nalgebra::constraint::{DimEq, ShapeConstraint}; use num_traits::{Zero, One}; use crate::ops::{Op}; use nalgebra::base::storage::Storage; @@ -265,20 +267,35 @@ macro_rules! impl_spmm_cs_dense { ($matrix_type:ident, $spmm_fn:ident) => { impl<'a, T, R, C, S> Mul<&'a Matrix> for &'a $matrix_type where - &'a Matrix: Into>, T: Scalar + ClosedMul + ClosedAdd + ClosedSub + ClosedDiv + Neg + Zero + One, R: Dim, C: Dim, S: Storage, + DefaultAllocator: Allocator, + // TODO: Is it possible to simplify these bounds? + ShapeConstraint: + // Bounds so that we can turn MatrixMN into a DMatrixSliceMut + DimEq>::Buffer as Storage>::RStride> + + DimEq + + DimEq>::Buffer as Storage>::CStride> + // Bounds so that we can turn &Matrix into a DMatrixSlice + + DimEq + + DimEq + + DimEq { - type Output = DMatrix; + // We need the column dimension to be generic, so that if RHS is a vector, then + // we also get a vector (and not a matrix) + type Output = MatrixMN; fn mul(self, rhs: &'a Matrix) -> Self::Output { - let rhs = rhs.into(); + // let rhs = rhs.into(); let (_, ncols) = rhs.data.shape(); let nrows = Dynamic::new(self.nrows()); - let mut result = Matrix::zeros_generic(nrows, ncols); - $spmm_fn(T::zero(), &mut result, T::one(), Op::NoOp(self), Op::NoOp(rhs)); + let mut result = MatrixMN::::zeros_generic(nrows, ncols); + { + // let result: DMatrixSliceMut<_> = (&mut result).into(); + $spmm_fn(T::zero(), &mut result, T::one(), Op::NoOp(self), Op::NoOp(rhs)); + } result } } From 74cd0283ebede38fc4c51dde56015c75f8718c32 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Fri, 22 Jan 2021 17:57:03 +0100 Subject: [PATCH 081/183] Partial top-level documentation --- nalgebra-sparse/src/lib.rs | 188 ++++++++++++++++++++++++------------- 1 file changed, 123 insertions(+), 65 deletions(-) diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index 73c0d88b..62d96092 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -1,75 +1,133 @@ -//! Sparse matrices and algorithms for nalgebra. +//! Sparse matrices and algorithms for [nalgebra](https://www.nalgebra.org). //! -//! TODO: Docs +//! This crate extends `nalgebra` with sparse matrix formats and operations on sparse matrices. //! +//! ## Goals +//! The long-term goals for this crate are listed below. //! -//! ### Planned functionality +//! - Provide proven sparse matrix formats in an easy-to-use and idiomatic Rust API that +//! naturally integrates with `nalgebra`. +//! - Provide additional expert-level APIs for fine-grained control over operations. +//! - Integrate well with external sparse matrix libraries. +//! - Provide native Rust high-performance routines, including parallel matrix operations. //! -//! Below we list desired functionality. This further needs to be refined into what is needed -//! for an initial contribution, and what can be added in future contributions. +//! ## Highlighted current features //! -//! - Sparsity pattern type. Functionality: -//! - [x] Access to offsets, indices as slices. -//! - [x] Return number of nnz -//! - [x] Access a given lane as a slice of minor indices -//! - [x] Construct from valid offset + index data -//! - [ ] Construct from unsorted (but otherwise valid) offset + index data -//! - [x] Iterate over entries (i, j) in the pattern -//! - [x] "Disassemble" the sparsity pattern into the raw index data arrays. -//! - CSR matrix type. Functionality: -//! - [x] Access to CSR data as slices. -//! - [x] Return number of nnz -//! - [x] Access a given row, which gives convenient access to the data associated -//! with a particular row -//! - [x] Construct from valid CSR data -//! - [ ] Construct from unsorted CSR data -//! - [x] Iterate over entries (i, j, v) in the matrix (+mutable). -//! - [x] Iterate over rows in the matrix (+ mutable). -//! - [x] "Disassemble" the CSR matrix into the raw CSR data arrays. +//! - [CSR](csr::CsrMatrix), [CSC](csc::CscMatrix) and [COO](coo::CooMatrix) formats, and +//! [conversions](`convert`) between them. +//! - Common arithmetic operations are implemented. See the [`ops`] module. +//! - 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. +//! - [proptest strategies](`proptest`) for sparse matrices when the feature +//! `proptest-support` is enabled. +//! - [matrixcompare support](https://crates.io/crates/matrixcompare) for effortless +//! (approximate) comparison of matrices in test code (requires the `compare` feature). //! -//! - CSC matrix type. Functionality: -//! - [x] Access to CSC data as slices. -//! - [x] Return number of nnz -//! - [x] Access a given column, which gives convenient access to the data associated -//! with a particular column -//! - [x] Construct from valid CSC data -//! - [ ] Construct from unsorted CSC data -//! - [x] Iterate over entries (i, j, v) in the matrix (+mutable). -//! - [x] Iterate over rows in the matrix (+ mutable). -//! - [x] "Disassemble" the CSC matrix into the raw CSC data arrays. -//! - COO matrix type. Functionality: -//! - [x] Construct new "empty" COO matrix -//! - [x] Construct from triplet arrays. -//! - [x] Push new triplets to the matrix. -//! - [x] Iterate over triplets. -//! - [x] "Disassemble" the COO matrix into its underlying triplet arrays. -//! - Format conversion: -//! - [x] COO -> Dense -//! - [x] CSR -> Dense -//! - [x] CSC -> Dense -//! - [x] COO -> CSR -//! - [x] COO -> CSC -//! - [x] CSR -> CSC -//! - [x] CSC -> CSR -//! - [x] CSR -> COO -//! - [x] CSC -> COO -//! - [x] Dense -> COO -//! - [x] Dense -> CSR -//! - [x] Dense -> CSC -//! - Arithmetic. In general arithmetic is only implemented between instances of the same format, -//! or between dense and instances of a given format. For example, we do not implement -//! CSR * CSC, only CSR * CSR and CSC * CSC. -//! - CSR: -//! - [ ] Dense = CSR * Dense (the other way around is not particularly useful) -//! - [ ] CSR = CSR * CSR -//! - [ ] CSR = CSR +- CSR -//! - [ ] CSR +=/-= CSR -//! - COO: -//! - [ ] Dense = COO * Dense (sometimes useful for very sparse matrices) -//! - CSC: -//! - Same as CSR -//! - Cholesky factorization (port existing factorization from nalgebra's sparse module) +//! ## Current state //! +//! The library is in an early, but usable state. The API has been designed to be extensible, +//! but breaking changes will be necessary to implement several planned features. While it is +//! backed by an extensive test suite, it has yet to be thoroughly battle-tested in real +//! applications. Moreover, the focus so far has been on correctness and API design, with little +//! focus on performance. Future improvements will include incremental performance enhancements. +//! +//! Current limitations: +//! +//! - Limited or no availability of sparse system solvers. +//! - Limited support for complex numbers. Currently only arithmetic operations that do not +//! rely on particular properties of complex numbers, such as e.g. conjugation, are +//! supported. +//! - No integration with external libraries. +//! +//! # Usage +//! +//! Add the following to your `Cargo.toml` file: +//! +//! ```toml +//! [dependencies] +//! nalgebra_sparse = "0.1" +//! ``` +//! +//! # Supported matrix formats +//! +//! | Format | Notes | +//! | ------------------------|--------------------------------------------- | +//! | [COO](`coo::CooMatrix`) | Well-suited for matrix construction.
Ill-suited for algebraic operations. | +//! | [CSR](`csr::CsrMatrix`) | Immutable sparsity pattern, suitable for algebraic operations.
Fast row access. | +//! | [CSC](`csr::CscMatrix`) | Immutable sparsity pattern, suitable for algebraic operations.
Fast column access. | +//! +//! What format is best to use depends on the application. The most common use case for sparse +//! matrices in science is the solution of sparse linear systems. Here we can differentiate between +//! two common cases: +//! +//! - Direct solvers. Typically, direct solvers take their input in CSR or CSC format. +//! - Iterative solvers. Many iterative solvers require only matrix-vector products, +//! for which the CSR or CSC formats are suitable. +//! +//! The [COO](coo::CooMatrix) format is primarily intended for matrix construction. +//! A common pattern is to use COO for construction, before converting to CSR or CSC for use +//! in a direct solver or for computing matrix-vector products in an iterative solver. +//! Some high-performance applications might also directly manipulate the CSR and/or CSC +//! formats. +//! +//! # Example: COO -> CSR -> matrix-vector product +//! +//! ```rust +//! use nalgebra_sparse::{coo::CooMatrix, csr::CsrMatrix}; +//! use nalgebra::{DMatrix, DVector}; +//! use matrixcompare::assert_matrix_eq; +//! +//! // The dense representation of the matrix +//! let dense = DMatrix::from_row_slice(3, 3, +//! &[1.0, 0.0, 3.0, +//! 2.0, 0.0, 1.3, +//! 0.0, 0.0, 4.1]); +//! +//! // Build the equivalent COO representation. We only add the non-zero values +//! let mut coo = CooMatrix::new(3, 3); +//! // We can add elements in any order. For clarity, we do so in row-major order here. +//! coo.push(0, 0, 1.0); +//! coo.push(0, 2, 3.0); +//! coo.push(1, 0, 2.0); +//! coo.push(1, 2, 1.3); +//! coo.push(2, 2, 4.1); +//! +//! // The simplest way to construct a CSR matrix is to first construct a COO matrix, and +//! // then convert it to CSR. The `From` trait is implemented for conversions between different +//! // sparse matrix types. +//! // Alternatively, we can construct a matrix directly from the CSR data. +//! // See the docs for CsrMatrix for how to do that. +//! let csr = CsrMatrix::from(&coo); +//! +//! // Let's check that the CSR matrix and the dense matrix represent the same matrix. +//! // We can use macros from the `matrixcompare` crate to easily do this, despite the fact that +//! // we're comparing across two different matrix formats. Note that these macros are only really +//! // appropriate for writing tests, however. +//! assert_matrix_eq!(csr, dense); +//! +//! let x = DVector::from_column_slice(&[1.3, -4.0, 3.5]); +//! +//! // Compute the matrix-vector product y = A * x. We don't need to specify the type here, +//! // but let's just do it to make sure we get what we expect +//! let y: DVector<_> = &csr * &x; +//! +//! // Verify the result with a small element-wise absolute tolerance +//! let y_expected = DVector::from_column_slice(&[11.8, 7.15, 14.35]); +//! assert_matrix_eq!(y, y_expected, comp = abs, tol = 1e-9); +//! +//! // The above expression is simple, and gives easy to read code, but if we're doing this in a +//! // loop, we'll have to keep allocating new vectors. If we determine that this is a bottleneck, +//! // then we can resort to the lower level APIs for more control over the operations +//! { +//! use nalgebra_sparse::ops::{Op, serial::spmm_csr_dense}; +//! let mut y = y; +//! // Compute y <- 0.0 * y + 1.0 * csr * dense. We store the result directly in `y`, without +//! // any immediate allocations +//! spmm_csr_dense(0.0, &mut y, 1.0, Op::NoOp(&csr), Op::NoOp(&x)); +//! assert_matrix_eq!(y, y_expected, comp = abs, tol = 1e-9); +//! } +//! ``` //! //! TODO: Write docs on the following: //! From 0bee9be6c78928600d060c1fd3b216e9d20f0846 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 25 Jan 2021 12:08:57 +0100 Subject: [PATCH 082/183] Extend CSC/CSR * Dense to work for combinations of ref and owned --- nalgebra-sparse/src/ops/impl_std_ops.rs | 46 ++++++++++++++++++------- nalgebra-sparse/tests/unit_tests/ops.rs | 12 +++++-- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/nalgebra-sparse/src/ops/impl_std_ops.rs b/nalgebra-sparse/src/ops/impl_std_ops.rs index 9309ff93..d62519e9 100644 --- a/nalgebra-sparse/src/ops/impl_std_ops.rs +++ b/nalgebra-sparse/src/ops/impl_std_ops.rs @@ -4,7 +4,7 @@ use crate::csc::CscMatrix; use std::ops::{Add, Div, DivAssign, Mul, MulAssign, Sub, Neg}; use crate::ops::serial::{spadd_csr_prealloc, spadd_csc_prealloc, spadd_pattern, spmm_csr_pattern, spmm_csr_prealloc, spmm_csc_prealloc, spmm_csc_dense, spmm_csr_dense, spmm_csc_pattern}; use nalgebra::{ClosedAdd, ClosedMul, ClosedSub, ClosedDiv, Scalar, Matrix, MatrixMN, Dim, - DMatrixSlice, DMatrixSliceMut, DMatrix, Dynamic, DefaultAllocator, U1}; + Dynamic, DefaultAllocator, U1}; use nalgebra::allocator::{Allocator}; use nalgebra::constraint::{DimEq, ShapeConstraint}; use num_traits::{Zero, One}; @@ -264,8 +264,34 @@ impl_div!(CsrMatrix); impl_div!(CscMatrix); macro_rules! impl_spmm_cs_dense { - ($matrix_type:ident, $spmm_fn:ident) => { - impl<'a, T, R, C, S> Mul<&'a Matrix> for &'a $matrix_type + ($matrix_type_name:ident, $spmm_fn:ident) => { + // Implement ref-ref + impl_spmm_cs_dense!(&'a $matrix_type_name, &'a Matrix, $spmm_fn, |lhs, rhs| { + let (_, ncols) = rhs.data.shape(); + let nrows = Dynamic::new(lhs.nrows()); + let mut result = MatrixMN::::zeros_generic(nrows, ncols); + $spmm_fn(T::zero(), &mut result, T::one(), Op::NoOp(lhs), Op::NoOp(rhs)); + result + }); + + // Implement the other combinations by deferring to ref-ref + impl_spmm_cs_dense!(&'a $matrix_type_name, Matrix, $spmm_fn, |lhs, rhs| { + lhs * &rhs + }); + impl_spmm_cs_dense!($matrix_type_name, &'a Matrix, $spmm_fn, |lhs, rhs| { + &lhs * rhs + }); + impl_spmm_cs_dense!($matrix_type_name, Matrix, $spmm_fn, |lhs, rhs| { + &lhs * &rhs + }); + }; + + // Main body of the macro. The first pattern just forwards to this pattern but with + // different arguments + ($sparse_matrix_type:ty, $dense_matrix_type:ty, $spmm_fn:ident, + |$lhs:ident, $rhs:ident| $body:tt) => + { + impl<'a, T, R, C, S> Mul<$dense_matrix_type> for $sparse_matrix_type where T: Scalar + ClosedMul + ClosedAdd + ClosedSub + ClosedDiv + Neg + Zero + One, R: Dim, @@ -287,16 +313,10 @@ macro_rules! impl_spmm_cs_dense { // we also get a vector (and not a matrix) type Output = MatrixMN; - fn mul(self, rhs: &'a Matrix) -> Self::Output { - // let rhs = rhs.into(); - let (_, ncols) = rhs.data.shape(); - let nrows = Dynamic::new(self.nrows()); - let mut result = MatrixMN::::zeros_generic(nrows, ncols); - { - // let result: DMatrixSliceMut<_> = (&mut result).into(); - $spmm_fn(T::zero(), &mut result, T::one(), Op::NoOp(self), Op::NoOp(rhs)); - } - result + fn mul(self, rhs: $dense_matrix_type) -> Self::Output { + let $lhs = self; + let $rhs = rhs; + $body } } } diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index 0bc6b42a..4e789eb3 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -1123,7 +1123,11 @@ proptest! { (Just(a), b) })) { - prop_assert_eq!(&a * &b, &DMatrix::from(&a) * &b); + let expected = DMatrix::from(&a) * &b; + prop_assert_eq!(&a * &b, expected.clone()); + prop_assert_eq!(&a * b.clone(), expected.clone()); + prop_assert_eq!(a.clone() * &b, expected.clone()); + prop_assert_eq!(a.clone() * b.clone(), expected.clone()); } #[test] @@ -1137,7 +1141,11 @@ proptest! { (Just(a), b) })) { - prop_assert_eq!(&a * &b, &DMatrix::from(&a) * &b); + let expected = DMatrix::from(&a) * &b; + prop_assert_eq!(&a * &b, expected.clone()); + prop_assert_eq!(&a * b.clone(), expected.clone()); + prop_assert_eq!(a.clone() * &b, expected.clone()); + prop_assert_eq!(a.clone() * b.clone(), expected.clone()); } #[test] From afcad0ccc87a55895df34f1f54d4d78bbdbe25d5 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 25 Jan 2021 12:09:16 +0100 Subject: [PATCH 083/183] Documentation for CsrMatrix --- nalgebra-sparse/src/csr.rs | 100 ++++++++++++++++++++++++++++++++++++- nalgebra-sparse/src/lib.rs | 2 +- 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index acf12e33..caf33b58 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -14,8 +14,106 @@ use std::slice::{IterMut, Iter}; /// The Compressed Sparse Row (CSR) format is well-suited as a general-purpose storage format /// for many sparse matrix applications. /// -/// TODO: Storage explanation and examples +/// # Usage /// +/// ```rust +/// use nalgebra_sparse::csr::CsrMatrix; +/// use nalgebra::{DMatrix, Matrix3x4}; +/// use matrixcompare::assert_matrix_eq; +/// +/// // The sparsity patterns of CSR matrices are immutable. This means that you cannot dynamically +/// // change the sparsity pattern of the matrix after it has been constructed. The easiest +/// // way to construct a CSR matrix is to first incrementally construct a COO matrix, +/// // and then convert it to CSR. +/// # use nalgebra_sparse::coo::CooMatrix; +/// # let coo = CooMatrix::::new(3, 3); +/// let csr = CsrMatrix::from(&coo); +/// +/// // Alternatively, a CSR matrix can be constructed directly from raw CSR data. +/// // Here, we construct a 3x4 matrix +/// let row_offsets = vec![0, 3, 3, 5]; +/// let col_indices = vec![0, 1, 3, 1, 2]; +/// let values = vec![1.0, 2.0, 3.0, 4.0, 5.0]; +/// +/// // The dense representation of the CSR data, for comparison +/// let dense = Matrix3x4::new(1.0, 2.0, 0.0, 3.0, +/// 0.0, 0.0, 0.0, 0.0, +/// 0.0, 4.0, 5.0, 0.0); +/// +/// // The constructor validates the raw CSR data and returns an error if it is invalid. +/// let csr = CsrMatrix::try_from_csr_data(3, 4, row_offsets, col_indices, values) +/// .expect("CSR data must conform to format specifications"); +/// assert_matrix_eq!(csr, dense); +/// +/// // A third approach is to construct a CSR matrix from a pattern and values. Sometimes this is +/// // useful if the sparsity pattern is constructed separately from the values of the matrix. +/// let (pattern, values) = csr.into_pattern_and_values(); +/// let csr = CsrMatrix::try_from_pattern_and_values(pattern, values) +/// .expect("The pattern and values must be compatible"); +/// +/// // Once we have constructed our matrix, we can use it for arithmetic operations together with +/// // other CSR matrices and dense matrices/vectors. +/// let x = csr; +/// # #[allow(non_snake_case)] +/// let xTx = x.transpose() * &x; +/// +/// let z = DMatrix::from_fn(4, 8, |i, j| (i as f64) * (j as f64)); +/// let w = 3.0 * xTx * z; +/// +/// // Although the sparsity pattern of a CSR matrix cannot be changed, its values can. +/// // Here are two different ways to scale all values by a constant: +/// let mut x = x; +/// x *= 5.0; +/// x.values_mut().iter_mut().for_each(|x_i| *x_i *= 5.0); +/// ``` +/// +/// # Format +/// +/// An `m x n` sparse matrix with `nnz` non-zeros consists of the following three arrays: +/// +/// - `row_offsets`, an array of integers with length `m + 1`. +/// - `col_indices`, an array of integers with length `nnz`. +/// - `values`, an array of values with length `nnz`. +/// +/// The relationship between the arrays is described below. +/// +/// - Each consecutive pair of entries `row_offsets[i] .. row_offsets[i + 1]` corresponds to an +/// offset range in `col_indices` that holds the column indices in row `i`. +/// - For an entry represented by the index `idx`, `col_indices[idx]` stores its column index and +/// `values[idx]` stores its value. +/// +/// The following invariants must be upheld and are enforced by the data structure: +/// +/// - `row_offsets[0] == 0` +/// - `row_offsets[m] == nnz` +/// - `row_offsets` is monotonically increasing. +/// - The column indices associated with each row are monotonically increasing (see below). +/// +/// The CSR format is a standard sparse matrix format (see [Wikipedia article]). The format +/// represents the matrix in a row-by-row fashion. The entries associated with row `i` are +/// determined as follows: +/// +/// ```rust +/// # let row_offsets: Vec = vec![0, 0]; +/// # let col_indices: Vec = vec![]; +/// # let values: Vec = vec![]; +/// # let i = 0; +/// let range = row_offsets[i] .. row_offsets[i + 1]; +/// let row_i_cols = &col_indices[range.clone()]; +/// let row_i_vals = &values[range]; +/// +/// // For each pair (j, v) in (row_i_cols, row_i_vals), we obtain a corresponding entry +/// // (i, j, v) in the matrix. +/// assert_eq!(row_i_cols.len(), row_i_vals.len()); +/// ``` +/// +/// In the above example, for each row `i`, the column indices `row_i_cols` must appear in +/// monotonically increasing order. In other words, they must be *sorted*. This criterion is not +/// standard among all sparse matrix libraries, but we enforce this property as it is a crucial +/// assumption for both correctness and performance for many algorithms. +/// +/// +/// [Wikipedia article]: https://en.wikipedia.org/wiki/Sparse_matrix#Compressed_sparse_row_(CSR,_CRS_or_Yale_format) #[derive(Debug, Clone, PartialEq, Eq)] pub struct CsrMatrix { // Rows are major, cols are minor in the sparsity pattern diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index 62d96092..0ff74035 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -123,7 +123,7 @@ //! use nalgebra_sparse::ops::{Op, serial::spmm_csr_dense}; //! let mut y = y; //! // Compute y <- 0.0 * y + 1.0 * csr * dense. We store the result directly in `y`, without -//! // any immediate allocations +//! // any intermediate allocations //! spmm_csr_dense(0.0, &mut y, 1.0, Op::NoOp(&csr), Op::NoOp(&x)); //! assert_matrix_eq!(y, y_expected, comp = abs, tol = 1e-9); //! } From e8a35ddb62d89b30d9aa8e69a6ec61b8139cb69f Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 25 Jan 2021 14:02:45 +0100 Subject: [PATCH 084/183] CSC docs and improved CSR docs --- nalgebra-sparse/src/csc.rs | 103 ++++++++++++++++++++++++++++++++++++- nalgebra-sparse/src/csr.rs | 7 ++- 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/nalgebra-sparse/src/csc.rs b/nalgebra-sparse/src/csc.rs index c48697a8..39022894 100644 --- a/nalgebra-sparse/src/csc.rs +++ b/nalgebra-sparse/src/csc.rs @@ -14,8 +14,109 @@ use nalgebra::Scalar; /// The Compressed Sparse Column (CSC) format is well-suited as a general-purpose storage format /// for many sparse matrix applications. /// -/// TODO: Storage explanation and examples +/// # Usage /// +/// ```rust +/// use nalgebra_sparse::csc::CscMatrix; +/// use nalgebra::{DMatrix, Matrix3x4}; +/// use matrixcompare::assert_matrix_eq; +/// +/// // The sparsity patterns of CSC matrices are immutable. This means that you cannot dynamically +/// // change the sparsity pattern of the matrix after it has been constructed. The easiest +/// // way to construct a CSC matrix is to first incrementally construct a COO matrix, +/// // and then convert it to CSC. +/// # use nalgebra_sparse::coo::CooMatrix; +/// # let coo = CooMatrix::::new(3, 3); +/// let csc = CscMatrix::from(&coo); +/// +/// // Alternatively, a CSC matrix can be constructed directly from raw CSC data. +/// // Here, we construct a 3x4 matrix +/// let col_offsets = vec![0, 1, 3, 4, 5]; +/// let row_indices = vec![0, 0, 2, 2, 0]; +/// let values = vec![1.0, 2.0, 3.0, 4.0, 5.0]; +/// +/// // The dense representation of the CSC data, for comparison +/// let dense = Matrix3x4::new(1.0, 2.0, 0.0, 5.0, +/// 0.0, 0.0, 0.0, 0.0, +/// 0.0, 3.0, 4.0, 0.0); +/// +/// // The constructor validates the raw CSC data and returns an error if it is invalid. +/// let csc = CscMatrix::try_from_csc_data(3, 4, col_offsets, row_indices, values) +/// .expect("CSC data must conform to format specifications"); +/// assert_matrix_eq!(csc, dense); +/// +/// // A third approach is to construct a CSC matrix from a pattern and values. Sometimes this is +/// // useful if the sparsity pattern is constructed separately from the values of the matrix. +/// let (pattern, values) = csc.into_pattern_and_values(); +/// let csc = CscMatrix::try_from_pattern_and_values(pattern, values) +/// .expect("The pattern and values must be compatible"); +/// +/// // Once we have constructed our matrix, we can use it for arithmetic operations together with +/// // other CSC matrices and dense matrices/vectors. +/// let x = csc; +/// # #[allow(non_snake_case)] +/// let xTx = x.transpose() * &x; +/// let z = DMatrix::from_fn(4, 8, |i, j| (i as f64) * (j as f64)); +/// let w = 3.0 * xTx * z; +/// +/// // Although the sparsity pattern of a CSC matrix cannot be changed, its values can. +/// // Here are two different ways to scale all values by a constant: +/// let mut x = x; +/// x *= 5.0; +/// x.values_mut().iter_mut().for_each(|x_i| *x_i *= 5.0); +/// ``` +/// +/// # Format +/// +/// An `m x n` sparse matrix with `nnz` non-zeros in CSC format is represented by the +/// following three arrays: +/// +/// - `col_offsets`, an array of integers with length `n + 1`. +/// - `row_indices`, an array of integers with length `nnz`. +/// - `values`, an array of values with length `nnz`. +/// +/// The relationship between the arrays is described below. +/// +/// - Each consecutive pair of entries `col_offsets[j] .. col_offsets[j + 1]` corresponds to an +/// offset range in `row_indices` that holds the row indices in column `j`. +/// - For an entry represented by the index `idx`, `row_indices[idx]` stores its column index and +/// `values[idx]` stores its value. +/// +/// The following invariants must be upheld and are enforced by the data structure: +/// +/// - `col_offsets[0] == 0` +/// - `col_offsets[m] == nnz` +/// - `col_offsets` is monotonically increasing. +/// - `0 <= row_indices[idx] < m` for all `idx < nnz`. +/// - The row indices associated with each column are monotonically increasing (see below). +/// +/// The CSC format is a standard sparse matrix format (see [Wikipedia article]). The format +/// represents the matrix in a column-by-column fashion. The entries associated with column `j` are +/// determined as follows: +/// +/// ```rust +/// # let col_offsets: Vec = vec![0, 0]; +/// # let row_indices: Vec = vec![]; +/// # let values: Vec = vec![]; +/// # let j = 0; +/// let range = col_offsets[j] .. col_offsets[j + 1]; +/// let col_j_rows = &row_indices[range.clone()]; +/// let col_j_vals = &values[range]; +/// +/// // For each pair (i, v) in (col_j_rows, col_j_vals), we obtain a corresponding entry +/// // (i, j, v) in the matrix. +/// assert_eq!(col_j_rows.len(), col_j_vals.len()); +/// ``` +/// +/// In the above example, for each column `j`, the row indices `col_j_cols` must appear in +/// monotonically increasing order. In other words, they must be *sorted*. This criterion is not +/// standard among all sparse matrix libraries, but we enforce this property as it is a crucial +/// assumption for both correctness and performance for many algorithms. +/// +/// Note that the CSR and CSC formats are essentially identical, except that CSC stores the matrix +/// column-by-column instead of row-by-row like CSR. +/// +/// [Wikipedia article]: https://en.wikipedia.org/wiki/Sparse_matrix#Compressed_sparse_column_(CSC_or_CCS) #[derive(Debug, Clone, PartialEq, Eq)] pub struct CscMatrix { // Cols are major, rows are minor in the sparsity pattern diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index caf33b58..3c0a08ca 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -56,7 +56,6 @@ use std::slice::{IterMut, Iter}; /// let x = csr; /// # #[allow(non_snake_case)] /// let xTx = x.transpose() * &x; -/// /// let z = DMatrix::from_fn(4, 8, |i, j| (i as f64) * (j as f64)); /// let w = 3.0 * xTx * z; /// @@ -69,7 +68,8 @@ use std::slice::{IterMut, Iter}; /// /// # Format /// -/// An `m x n` sparse matrix with `nnz` non-zeros consists of the following three arrays: +/// An `m x n` sparse matrix with `nnz` non-zeros in CSR format is represented by the +/// following three arrays: /// /// - `row_offsets`, an array of integers with length `m + 1`. /// - `col_indices`, an array of integers with length `nnz`. @@ -87,6 +87,7 @@ use std::slice::{IterMut, Iter}; /// - `row_offsets[0] == 0` /// - `row_offsets[m] == nnz` /// - `row_offsets` is monotonically increasing. +/// - `0 <= col_indices[idx] < n` for all `idx < nnz`. /// - The column indices associated with each row are monotonically increasing (see below). /// /// The CSR format is a standard sparse matrix format (see [Wikipedia article]). The format @@ -112,6 +113,8 @@ use std::slice::{IterMut, Iter}; /// standard among all sparse matrix libraries, but we enforce this property as it is a crucial /// assumption for both correctness and performance for many algorithms. /// +/// Note that the CSR and CSC formats are essentially identical, except that CSC stores the matrix +/// column-by-column instead of row-by-row like CSR. /// /// [Wikipedia article]: https://en.wikipedia.org/wiki/Sparse_matrix#Compressed_sparse_row_(CSR,_CRS_or_Yale_format) #[derive(Debug, Clone, PartialEq, Eq)] From cf220c9d2b565660c6851497b6d473ef77d17e35 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 25 Jan 2021 14:17:48 +0100 Subject: [PATCH 085/183] Improve docs for CooMatrix --- nalgebra-sparse/src/coo.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/nalgebra-sparse/src/coo.rs b/nalgebra-sparse/src/coo.rs index 43662da3..6867a327 100644 --- a/nalgebra-sparse/src/coo.rs +++ b/nalgebra-sparse/src/coo.rs @@ -6,13 +6,11 @@ use crate::SparseFormatError; /// /// A COO matrix stores entries in coordinate-form, that is triplets `(i, j, v)`, where `i` and `j` /// correspond to row and column indices of the entry, and `v` to the value of the entry. -/// With the rare exception of matrix-vector multiplication of certain extremely sparse matrices, -/// it is of limited use for standard matrix operations. Its main purpose is to facilitate +/// The format is of limited use for standard matrix operations. Its main purpose is to facilitate /// easy construction of other, more efficient matrix formats (such as CSR/COO), and the /// conversion between different formats. /// -/// Representation -/// -------------- +/// # Format /// /// For given dimensions `nrows` and `ncols`, the matrix is represented by three same-length /// arrays `row_indices`, `col_indices` and `values` that constitute the coordinate triplets @@ -20,20 +18,23 @@ use crate::SparseFormatError; /// Upon conversion to other formats, the duplicate entries may be summed together. See the /// documentation for the respective conversion functions. /// -/// Example -/// ------- +/// # Examples /// /// ```rust -/// # use nalgebra_sparse::coo::CooMatrix; -/// // Create a zero matrix +/// use nalgebra_sparse::{coo::CooMatrix, csr::CsrMatrix, csc::CscMatrix}; +/// +/// // Initialize a matrix with all zeros (no explicitly stored entries). /// let mut coo = CooMatrix::new(4, 4); /// // Or initialize it with a set of triplets /// coo = CooMatrix::try_from_triplets(4, 4, vec![1, 2], vec![0, 1], vec![3.0, 4.0]).unwrap(); /// -/// // Push a single triplet +/// // Push a few triplets /// coo.push(2, 0, 1.0); +/// coo.push(0, 1, 2.0); /// -/// // TODO: Convert to CSR +/// // Convert to other matrix formats +/// let csr = CsrMatrix::from(&coo); +/// let csc = CscMatrix::from(&coo); /// ``` #[derive(Debug, Clone, PartialEq, Eq)] pub struct CooMatrix { From f98e64aafd5897238ededa04cb850d7b7aeb32a3 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 25 Jan 2021 14:57:36 +0100 Subject: [PATCH 086/183] Improve docs for SparsityPattern --- nalgebra-sparse/src/pattern.rs | 48 ++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/nalgebra-sparse/src/pattern.rs b/nalgebra-sparse/src/pattern.rs index 6a0cf851..e513108e 100644 --- a/nalgebra-sparse/src/pattern.rs +++ b/nalgebra-sparse/src/pattern.rs @@ -6,16 +6,39 @@ use crate::cs::transpose_cs; /// A representation of the sparsity pattern of a CSR or CSC matrix. /// -/// ## Format specification +/// CSR and CSC matrices store matrices in a very similar fashion. In fact, in a certain sense, +/// they are transposed. More precisely, when reinterpreting the three data arrays of a CSR +/// matrix as a CSC matrix, we obtain the CSC representation of its transpose. /// -/// TODO: Write this out properly +/// [`SparsityPattern`] is an abstraction built on this observation. Whereas CSR matrices +/// store a matrix row-by-row, and a CSC matrix stores a matrix column-by-column, a +/// `SparsityPattern` represents only the index data structure of a matrix *lane-by-lane*. +/// Here, a *lane* is a generalization of rows and columns. We further define *major lanes* +/// and *minor lanes*. The sparsity pattern of a CSR matrix is then obtained by interpreting +/// major/minor as row/column. Conversely, we obtain the sparsity pattern of a CSC matrix by +/// interpreting major/minor as column/row. /// -/// - offsets[0] == 0 -/// - Major offsets must be monotonically increasing -/// - major_offsets.len() == major_dim + 1 -/// - Column indices within each lane must be sorted -/// - Column indices must be in-bounds -/// - The last entry in major offsets must correspond to the number of minor indices +/// This allows us to use a common abstraction to talk about sparsity patterns of CSR and CSC +/// matrices. This is convenient, because at the abstract level, the invariants of the formats +/// are the same. Hence we may encode the invariants of the index data structure separately from +/// the scalar values of the matrix. This is especially useful in applications where the +/// sparsity pattern is built ahead of the matrix values, or the same sparsity pattern is re-used +/// between different matrices. Finally, we can use `SparsityPattern` to encode adjacency +/// information in graphs. +/// +/// # Format +/// +/// The format is exactly the same as for the index data structures of CSR and CSC matrices. +/// This means that the sparsity pattern of an `m x n` sparse matrix with `nnz` non-zeros, +/// where in this case `m x n` does *not* mean `rows x columns`, but rather `majors x minors`, +/// is represented by the following two arrays: +/// +/// - `major_offsets`, an array of integers with length `m + 1`. +/// - `minor_indices`, an array of integers with length `nnz`. +/// +/// The invariants and relationship between `major_offsets` and `minor_indices` remain the same +/// as for `row_offsets` and `col_indices` in the [CSR](`crate::csr::CsrMatrix`) format +/// specification. #[derive(Debug, Clone, PartialEq, Eq)] // TODO: Make SparsityPattern parametrized by index type // (need a solid abstraction for index types though) @@ -47,14 +70,14 @@ impl SparsityPattern { &self.minor_indices } - /// The major dimension. + /// The number of major lanes in the pattern. #[inline] pub fn major_dim(&self) -> usize { assert!(self.major_offsets.len() > 0); self.major_offsets.len() - 1 } - /// The minor dimension. + /// The number of minor lanes in the pattern. #[inline] pub fn minor_dim(&self) -> usize { self.minor_dim @@ -206,7 +229,10 @@ impl SparsityPattern { (self.major_offsets, self.minor_indices) } - /// TODO + /// Computes the transpose of the sparsity pattern. + /// + /// This is analogous to matrix transposition, i.e. an entry `(i, j)` becomes `(j, i)` in the + /// new pattern. pub fn transpose(&self) -> Self { // By using unit () values, we can use the same routines as for CSR/CSC matrices let values = vec![(); self.nnz()]; From cf1bd284f11812d6f97f892ddaea7cd5eb60fe2d Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 25 Jan 2021 15:54:25 +0100 Subject: [PATCH 087/183] Improve ops docs --- nalgebra-sparse/src/ops/mod.rs | 115 ++++++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-) diff --git a/nalgebra-sparse/src/ops/mod.rs b/nalgebra-sparse/src/ops/mod.rs index 2857533f..4b51d2ee 100644 --- a/nalgebra-sparse/src/ops/mod.rs +++ b/nalgebra-sparse/src/ops/mod.rs @@ -1,15 +1,128 @@ //! Sparse matrix arithmetic operations. //! -//! TODO: Explain that users should prefer to use std ops unless they need to get more performance +//! This module contains a number of routines for sparse matrix arithmetic. These routines are +//! primarily intended for "expert usage". Most users should prefer to use standard +//! `std::ops` operations for simple and readable code when possible. The routines provided here +//! offer more control over allocation, and allow fusing some low-level operations for higher +//! performance. //! //! The available operations are organized by backend. Currently, only the [`serial`] backend //! is available. In the future, backends that expose parallel operations may become available. +//! All `std::ops` implementations will remain single-threaded and powered by the +//! `serial` backend. //! //! Many routines are able to implicitly transpose matrices involved in the operation. //! For example, the routine [`spadd_csr_prealloc`](serial::spadd_csr_prealloc) performs the //! operation `C <- beta * C + alpha * op(A)`. Here `op(A)` indicates that the matrix `A` can //! either be used as-is or transposed. The notation `op(A)` is represented in code by the //! [`Op`] enum. +//! +//! # Available `std::ops` implementations +//! +//! ## Binary operators +//! +//! The below table summarizes the currently supported binary operators between matrices. +//! In general, binary operators between sparse matrices are only supported if both matrices +//! are stored in the same format. All supported binary operators are implemented for +//! all four combinations of values and references. +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//!
LHS (down) \ RHS (right)COOCSRCSCDense
COO
CSR+ - **
CSC+ - **
Dense+ - *
+//! +//! As can be seen from the table, only `CSR * Dense` and `CSC * Dense` are supported. +//! The other way around, i.e. `Dense * CSR` and `Dense * CSC` are not implemented. +//! +//! Additionally, [CsrMatrix](`crate::csr::CsrMatrix`) and [CooMatrix](`crate::coo::CooMatrix`) +//! support multiplication with scalars, in addition to division by a scalar. +//! Note that only `Matrix * Scalar` works in a generic context, although `Scalar * Matrix` +//! has been implemented for many of the built-in arithmetic types. This is due to a fundamental +//! restriction of the Rust type system. Therefore, in generic code you will need to always place +//! the matrix on the left-hand side of the multiplication. +//! +//! ## Unary operators +//! +//! The following table lists currently supported unary operators. +//! +//! | Format | AddAssign\ | MulAssign\ | MulAssign\ | Neg | +//! | -------- | ----------------- | ----------------- | ------------------- | ------ | +//! | COO | | | | | +//! | CSR | | | x | x | +//! | CSC | | | x | x | +//! | +//! # Example usage +//! +//! For example, consider the case where you want to compute the expression +//! `C <- 3.0 * C + 2.0 * A^T * B`, where `A`, `B`, `C` are matrices and `A^T` is the transpose +//! of `A`. The simplest way to write this is: +//! +//! ```rust +//! # use nalgebra_sparse::csr::CsrMatrix; +//! # let a = CsrMatrix::identity(10); let b = CsrMatrix::identity(10); +//! # let mut c = CsrMatrix::identity(10); +//! c = 3.0 * c + 2.0 * a.transpose() * b; +//! ``` +//! This is simple and straightforward to read, and therefore the recommended way to implement +//! it. However, if you have determined that this is a performance bottleneck of your application, +//! it may be possible to speed things up. First, let's see what's going on here. The `std` +//! operations are evaluated eagerly. This means that the following steps take place: +//! +//! 1. Evaluate `let c_temp = 3.0 * c`. This requires scaling all values of the matrix. +//! 2. Evaluate `let a_t = a.transpose()` into a new temporary matrix. +//! 3. Evaluate `let a_t_b = a_t * b` into a new temporary matrix. +//! 4. Evaluate `let a_t_b_scaled = 2.0 * a_t_b`. This requires scaling all values of the matrix. +//! 5. Evaluate `c = c_temp + a_t_b_scaled`. +//! +//! An alternative way to implement this expression (here using CSR matrices) is: +//! +//! ```rust +//! # use nalgebra_sparse::csr::CsrMatrix; +//! # let a = CsrMatrix::identity(10); let b = CsrMatrix::identity(10); +//! # let mut c = CsrMatrix::identity(10); +//! use nalgebra_sparse::ops::{Op, serial::spmm_csr_prealloc}; +//! +//! // Evaluate the expression `c <- 3.0 * c + 2.0 * a^T * b +//! spmm_csr_prealloc(3.0, &mut c, 2.0, Op::Transpose(&a), Op::NoOp(&b)) +//! .expect("We assume that the pattern of C is able to accommodate the result."); +//! ``` +//! Compared to the simpler example, this snippet is harder to read, but it calls a single +//! computational kernel that avoids many of the intermediate steps listed out before. Therefore +//! directly calling kernels may sometimes lead to better performance. However, this should +//! always be verified by performance profiling! mod impl_std_ops; pub mod serial; From 7b6333e9d116465b349538293fa9aa20601ae45c Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 25 Jan 2021 16:04:29 +0100 Subject: [PATCH 088/183] Rename some Csr/Csc/SparsityPattern methods --- nalgebra-sparse/src/cs.rs | 11 ++++++++++- nalgebra-sparse/src/csc.rs | 9 ++++++--- nalgebra-sparse/src/csr.rs | 9 ++++++--- nalgebra-sparse/src/lib.rs | 7 ------- nalgebra-sparse/src/pattern.rs | 2 +- nalgebra-sparse/tests/unit_tests/csc.rs | 6 +++--- nalgebra-sparse/tests/unit_tests/csr.rs | 6 +++--- nalgebra-sparse/tests/unit_tests/pattern.rs | 2 +- 8 files changed, 30 insertions(+), 22 deletions(-) diff --git a/nalgebra-sparse/src/cs.rs b/nalgebra-sparse/src/cs.rs index cf8fd382..e9137aa5 100644 --- a/nalgebra-sparse/src/cs.rs +++ b/nalgebra-sparse/src/cs.rs @@ -26,7 +26,7 @@ impl CsMatrix { #[inline] pub fn new(major_dim: usize, minor_dim: usize) -> Self { Self { - sparsity_pattern: SparsityPattern::new(major_dim, minor_dim), + sparsity_pattern: SparsityPattern::zeros(major_dim, minor_dim), values: vec![], } } @@ -185,6 +185,15 @@ impl CsMatrix { Self::from_pattern_and_values(new_pattern, new_values) } + + /// Returns the diagonal of the matrix as a sparse matrix. + pub fn diagonal_as_matrix(&self) -> Self + where + T: Clone + { + // TODO: This might be faster with a binary search for each diagonal entry + self.filter(|i, j, _| i == j) + } } impl CsMatrix { diff --git a/nalgebra-sparse/src/csc.rs b/nalgebra-sparse/src/csc.rs index 39022894..f7a6b71b 100644 --- a/nalgebra-sparse/src/csc.rs +++ b/nalgebra-sparse/src/csc.rs @@ -1,4 +1,7 @@ //! An implementation of the CSC sparse matrix format. +//! +//! This is the module-level documentation. See [`CscMatrix`] for the main documentation of the +//! CSC implementation. use crate::{SparseFormatError, SparseFormatErrorKind, SparseEntry, SparseEntryMut}; use crate::pattern::{SparsityPattern, SparsityPatternFormatError, SparsityPatternIter}; @@ -125,7 +128,7 @@ pub struct CscMatrix { impl CscMatrix { /// Create a zero CSC matrix with no explicitly stored entries. - pub fn new(nrows: usize, ncols: usize) -> Self { + pub fn zeros(nrows: usize, ncols: usize) -> Self { Self { cs: CsMatrix::new(ncols, nrows) } @@ -469,11 +472,11 @@ impl CscMatrix { } /// Returns the diagonal of the matrix as a sparse matrix. - pub fn diagonal_as_matrix(&self) -> Self + pub fn diagonal_as_csc(&self) -> Self where T: Clone { - self.filter(|i, j, _| i == j) + Self { cs: self.cs.diagonal_as_matrix() } } } diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index 3c0a08ca..bdfb38e9 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -1,4 +1,7 @@ //! An implementation of the CSR sparse matrix format. +//! +//! This is the module-level documentation. See [`CsrMatrix`] for the main documentation of the +//! CSC implementation. use crate::{SparseFormatError, SparseFormatErrorKind, SparseEntry, SparseEntryMut}; use crate::pattern::{SparsityPattern, SparsityPatternFormatError, SparsityPatternIter}; use crate::csc::CscMatrix; @@ -125,7 +128,7 @@ pub struct CsrMatrix { impl CsrMatrix { /// Create a zero CSR matrix with no explicitly stored entries. - pub fn new(nrows: usize, ncols: usize) -> Self { + pub fn zeros(nrows: usize, ncols: usize) -> Self { Self { cs: CsMatrix::new(nrows, ncols) } @@ -469,11 +472,11 @@ impl CsrMatrix { } /// Returns the diagonal of the matrix as a sparse matrix. - pub fn diagonal_as_matrix(&self) -> Self + pub fn diagonal_as_csr(&self) -> Self where T: Clone { - self.filter(|i, j, _| i == j) + Self { cs: self.cs.diagonal_as_matrix() } } } diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index 0ff74035..3436c147 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -128,13 +128,6 @@ //! assert_matrix_eq!(y, y_expected, comp = abs, tol = 1e-9); //! } //! ``` -//! -//! TODO: Write docs on the following: -//! -//! - Overall design ("easy API" vs. "expert" API etc.) -//! - Conversions (From, explicit "expert" API etc.) -//! - Matrix ops design -//! - Proptest and matrixcompare integrations #![deny(non_camel_case_types)] #![deny(unused_parens)] #![deny(non_upper_case_globals)] diff --git a/nalgebra-sparse/src/pattern.rs b/nalgebra-sparse/src/pattern.rs index e513108e..3a59a6e7 100644 --- a/nalgebra-sparse/src/pattern.rs +++ b/nalgebra-sparse/src/pattern.rs @@ -50,7 +50,7 @@ pub struct SparsityPattern { impl SparsityPattern { /// Create a sparsity pattern of the given dimensions without explicitly stored entries. - pub fn new(major_dim: usize, minor_dim: usize) -> Self { + pub fn zeros(major_dim: usize, minor_dim: usize) -> Self { Self { major_offsets: vec![0; major_dim + 1], minor_indices: vec![], diff --git a/nalgebra-sparse/tests/unit_tests/csc.rs b/nalgebra-sparse/tests/unit_tests/csc.rs index 4faa4e12..47181987 100644 --- a/nalgebra-sparse/tests/unit_tests/csc.rs +++ b/nalgebra-sparse/tests/unit_tests/csc.rs @@ -21,7 +21,7 @@ fn csc_matrix_valid_data() { let values = Vec::::new(); let mut matrix = CscMatrix::try_from_csc_data(2, 3, offsets, indices, values).unwrap(); - assert_eq!(matrix, CscMatrix::new(2, 3)); + assert_eq!(matrix, CscMatrix::zeros(2, 3)); assert_eq!(matrix.nrows(), 2); assert_eq!(matrix.ncols(), 3); @@ -317,8 +317,8 @@ proptest! { } #[test] - fn csc_diagonal_as_matrix(csc in csc_strategy()) { - let d = csc.diagonal_as_matrix(); + fn csc_diagonal_as_csc(csc in csc_strategy()) { + let d = csc.diagonal_as_csc(); let d_entries: HashSet<_> = d.triplet_iter().cloned_values().collect(); let csc_diagonal_entries: HashSet<_> = csc .triplet_iter() diff --git a/nalgebra-sparse/tests/unit_tests/csr.rs b/nalgebra-sparse/tests/unit_tests/csr.rs index 4885d25b..698fb5f1 100644 --- a/nalgebra-sparse/tests/unit_tests/csr.rs +++ b/nalgebra-sparse/tests/unit_tests/csr.rs @@ -22,7 +22,7 @@ fn csr_matrix_valid_data() { let values = Vec::::new(); let mut matrix = CsrMatrix::try_from_csr_data(3, 2, offsets, indices, values).unwrap(); - assert_eq!(matrix, CsrMatrix::new(3, 2)); + assert_eq!(matrix, CsrMatrix::zeros(3, 2)); assert_eq!(matrix.nrows(), 3); assert_eq!(matrix.ncols(), 2); @@ -318,8 +318,8 @@ proptest! { } #[test] - fn csr_diagonal_as_matrix(csr in csr_strategy()) { - let d = csr.diagonal_as_matrix(); + fn csr_diagonal_as_csr(csr in csr_strategy()) { + let d = csr.diagonal_as_csr(); let d_entries: HashSet<_> = d.triplet_iter().cloned_values().collect(); let csr_diagonal_entries: HashSet<_> = csr .triplet_iter() diff --git a/nalgebra-sparse/tests/unit_tests/pattern.rs b/nalgebra-sparse/tests/unit_tests/pattern.rs index 4664ad74..ee6b2328 100644 --- a/nalgebra-sparse/tests/unit_tests/pattern.rs +++ b/nalgebra-sparse/tests/unit_tests/pattern.rs @@ -23,7 +23,7 @@ fn sparsity_pattern_valid_data() { assert_eq!(pattern.lane(2), &[]); assert!(pattern.entries().next().is_none()); - assert_eq!(pattern, SparsityPattern::new(3, 2)); + assert_eq!(pattern, SparsityPattern::zeros(3, 2)); let (offsets, indices) = pattern.disassemble(); assert_eq!(offsets, vec![0, 0, 0, 0]); From ccf1f18991cf7d97be09608890b69e3ba769607f Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 25 Jan 2021 17:05:13 +0100 Subject: [PATCH 089/183] Merge SolveError into OperationError --- nalgebra-sparse/src/ops/serial/csc.rs | 44 ++++++--------------------- nalgebra-sparse/src/ops/serial/mod.rs | 25 ++++++++++++++- 2 files changed, 34 insertions(+), 35 deletions(-) diff --git a/nalgebra-sparse/src/ops/serial/csc.rs b/nalgebra-sparse/src/ops/serial/csc.rs index e8f4d4e7..5765e094 100644 --- a/nalgebra-sparse/src/ops/serial/csc.rs +++ b/nalgebra-sparse/src/ops/serial/csc.rs @@ -1,7 +1,7 @@ use crate::csc::CscMatrix; use crate::ops::Op; use crate::ops::serial::cs::{spmm_cs_prealloc, spmm_cs_dense, spadd_cs_prealloc}; -use crate::ops::serial::OperationError; +use crate::ops::serial::{OperationError, OperationErrorKind}; use nalgebra::{Scalar, ClosedAdd, ClosedMul, DMatrixSliceMut, DMatrixSlice, RealField}; use num_traits::{Zero, One}; @@ -91,30 +91,6 @@ pub fn spmm_csc_prealloc( } } -/// TODO -#[non_exhaustive] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum SolveErrorKind { - /// TODO - Singular, -} - -/// TODO -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SolveError { - kind: SolveErrorKind, - message: String -} - -impl SolveError { - fn from_type_and_message(kind: SolveErrorKind, message: String) -> Self { - Self { - kind, - message - } - } -} - /// Solve the lower triangular system `op(L) X = B`. /// /// Only the lower triangular part of L is read, and the result is stored in B. @@ -125,7 +101,7 @@ impl SolveError { pub fn spsolve_csc_lower_triangular<'a, T: RealField>( l: Op<&CscMatrix>, b: impl Into>) - -> Result<(), SolveError> + -> Result<(), OperationError> { let b = b.into(); let l_matrix = l.into_inner(); @@ -137,10 +113,10 @@ pub fn spsolve_csc_lower_triangular<'a, T: RealField>( } } -fn spsolve_csc_lower_triangular_no_transpose<'a, T: RealField>( +fn spsolve_csc_lower_triangular_no_transpose( l: &CscMatrix, - b: DMatrixSliceMut<'a, T>) - -> Result<(), SolveError> + b: DMatrixSliceMut) + -> Result<(), OperationError> { let mut x = b; @@ -188,15 +164,15 @@ fn spsolve_csc_lower_triangular_no_transpose<'a, T: RealField>( Ok(()) } -fn spsolve_encountered_zero_diagonal() -> Result<(), SolveError> { +fn spsolve_encountered_zero_diagonal() -> Result<(), OperationError> { let message = "Matrix contains at least one diagonal entry that is zero."; - Err(SolveError::from_type_and_message(SolveErrorKind::Singular, String::from(message))) + Err(OperationError::from_kind_and_message(OperationErrorKind::Singular, String::from(message))) } -fn spsolve_csc_lower_triangular_transpose<'a, T: RealField>( +fn spsolve_csc_lower_triangular_transpose( l: &CscMatrix, - b: DMatrixSliceMut<'a, T>) - -> Result<(), SolveError> + b: DMatrixSliceMut) + -> Result<(), OperationError> { let mut x = b; diff --git a/nalgebra-sparse/src/ops/serial/mod.rs b/nalgebra-sparse/src/ops/serial/mod.rs index 38ee266a..1497bcb0 100644 --- a/nalgebra-sparse/src/ops/serial/mod.rs +++ b/nalgebra-sparse/src/ops/serial/mod.rs @@ -65,6 +65,8 @@ mod cs; pub use csc::*; pub use csr::*; pub use pattern::*; +use std::fmt::Formatter; +use std::fmt; /// A description of the error that occurred during an arithmetic operation. #[derive(Clone, Debug)] @@ -83,6 +85,9 @@ pub enum OperationErrorKind { /// For example, this could indicate that the sparsity pattern of the output is not able to /// contain the result of the operation. InvalidPattern, + + /// Indicates that a matrix is singular when it is expected to be invertible. + Singular, } impl OperationError { @@ -94,4 +99,22 @@ impl OperationError { pub fn kind(&self) -> &OperationErrorKind { &self.error_kind } -} \ No newline at end of file + + /// The underlying error message. + pub fn message(&self) -> &str { + self.message.as_str() + } +} + +impl fmt::Display for OperationError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "Sparse matrix operation error: ")?; + match self.kind() { + OperationErrorKind::InvalidPattern => { write!(f, "InvalidPattern")?; } + OperationErrorKind::Singular => { write!(f, "Singular")?; } + } + write!(f, " Message: {}", self.message) + } +} + +impl std::error::Error for OperationError {} \ No newline at end of file From 5d5ed5be0b2e716f2a1b477625c1a7747c17db3d Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 25 Jan 2021 17:12:46 +0100 Subject: [PATCH 090/183] Various minor doc and comment fixes --- nalgebra-sparse/src/cs.rs | 1 - nalgebra-sparse/src/csc.rs | 2 +- nalgebra-sparse/src/csr.rs | 2 +- nalgebra-sparse/src/factorization/cholesky.rs | 2 -- nalgebra-sparse/src/ops/serial/pattern.rs | 15 ++++++++++++++- nalgebra-sparse/src/pattern.rs | 5 ----- 6 files changed, 16 insertions(+), 11 deletions(-) diff --git a/nalgebra-sparse/src/cs.rs b/nalgebra-sparse/src/cs.rs index e9137aa5..f3c0a46d 100644 --- a/nalgebra-sparse/src/cs.rs +++ b/nalgebra-sparse/src/cs.rs @@ -197,7 +197,6 @@ impl CsMatrix { } impl CsMatrix { - /// TODO #[inline] pub fn identity(n: usize) -> Self { let offsets: Vec<_> = (0 ..= n).collect(); diff --git a/nalgebra-sparse/src/csc.rs b/nalgebra-sparse/src/csc.rs index f7a6b71b..8b5d0589 100644 --- a/nalgebra-sparse/src/csc.rs +++ b/nalgebra-sparse/src/csc.rs @@ -491,7 +491,7 @@ impl CscMatrix } impl CscMatrix { - /// TODO + /// Constructs a CSC representation of the (square) `n x n` identity matrix. #[inline] pub fn identity(n: usize) -> Self { Self { diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index bdfb38e9..d1e9ab1f 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -491,7 +491,7 @@ where } impl CsrMatrix { - /// TODO + /// Constructs a CSR representation of the (square) `n x n` identity matrix. #[inline] pub fn identity(n: usize) -> Self { Self { diff --git a/nalgebra-sparse/src/factorization/cholesky.rs b/nalgebra-sparse/src/factorization/cholesky.rs index 1c4f95a8..65a3c02f 100644 --- a/nalgebra-sparse/src/factorization/cholesky.rs +++ b/nalgebra-sparse/src/factorization/cholesky.rs @@ -140,8 +140,6 @@ impl CscCholesky { /// # Panics /// /// Panics if the matrix is not square. - /// - /// TODO: Take matrix by value or not? pub fn factor(matrix: &CscMatrix) -> Result { let symbolic = CscSymbolicCholesky::factor(matrix.pattern().clone()); Self::factor_numerical(symbolic, matrix.values()) diff --git a/nalgebra-sparse/src/ops/serial/pattern.rs b/nalgebra-sparse/src/ops/serial/pattern.rs index 276100f3..168a4d61 100644 --- a/nalgebra-sparse/src/ops/serial/pattern.rs +++ b/nalgebra-sparse/src/ops/serial/pattern.rs @@ -8,10 +8,13 @@ use std::iter; /// The patterns are assumed to have the same major and minor dimensions. In other words, /// both patterns `A` and `B` must both stem from the same kind of compressed matrix: /// CSR or CSC. +/// +/// # Panics +/// +/// Panics if the patterns don't have the same major and minor dimensions. pub fn spadd_pattern(a: &SparsityPattern, b: &SparsityPattern) -> SparsityPattern { - // TODO: Proper error messages assert_eq!(a.major_dim(), b.major_dim(), "Patterns must have identical major dimensions."); assert_eq!(a.minor_dim(), b.minor_dim(), "Patterns must have identical minor dimensions."); @@ -39,6 +42,11 @@ pub fn spadd_pattern(a: &SparsityPattern, /// /// Assumes that the sparsity patterns both represent CSC matrices, and the result is also /// represented as the sparsity pattern of a CSC matrix. +/// +/// # Panics +/// +/// Panics if the patterns, when interpreted as CSC patterns, are not compatible for +/// matrix multiplication. pub fn spmm_csc_pattern(a: &SparsityPattern, b: &SparsityPattern) -> SparsityPattern { // Let C = A * B in CSC format. We note that // C^T = B^T * A^T. @@ -52,6 +60,11 @@ pub fn spmm_csc_pattern(a: &SparsityPattern, b: &SparsityPattern) -> SparsityPat /// /// Assumes that the sparsity patterns both represent CSR matrices, and the result is also /// represented as the sparsity pattern of a CSR matrix. +/// +/// # Panics +/// +/// Panics if the patterns, when interpreted as CSR patterns, are not compatible for +/// matrix multiplication. pub fn spmm_csr_pattern(a: &SparsityPattern, b: &SparsityPattern) -> SparsityPattern { assert_eq!(a.minor_dim(), b.major_dim(), "a and b must have compatible dimensions"); diff --git a/nalgebra-sparse/src/pattern.rs b/nalgebra-sparse/src/pattern.rs index 3a59a6e7..ee7edd42 100644 --- a/nalgebra-sparse/src/pattern.rs +++ b/nalgebra-sparse/src/pattern.rs @@ -118,11 +118,6 @@ impl SparsityPattern { major_offsets: Vec, minor_indices: Vec, ) -> Result { - // TODO: If these errors are *directly* propagated to errors from e.g. - // CSR construction, the error messages will be confusing to users, - // as the error messages refer to "major" and "minor" lanes, as opposed to - // rows and columns - use SparsityPatternFormatError::*; if major_offsets.len() != major_dim + 1 { From 795d818ae508bf345ff9691ccc69557faaee7a1c Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 25 Jan 2021 17:19:20 +0100 Subject: [PATCH 091/183] Improve documentation of errors and panics --- nalgebra-sparse/src/ops/serial/csc.rs | 23 ++++++++++++++++++++++- nalgebra-sparse/src/ops/serial/csr.rs | 14 ++++++++++++++ nalgebra-sparse/src/ops/serial/pattern.rs | 2 +- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/nalgebra-sparse/src/ops/serial/csc.rs b/nalgebra-sparse/src/ops/serial/csc.rs index 5765e094..bcfb8108 100644 --- a/nalgebra-sparse/src/ops/serial/csc.rs +++ b/nalgebra-sparse/src/ops/serial/csc.rs @@ -8,6 +8,10 @@ use num_traits::{Zero, One}; use std::borrow::Cow; /// Sparse-dense matrix-matrix multiplication `C <- beta * C + alpha * op(A) * op(B)`. +/// +/// # Panics +/// +/// Panics if the dimensions of the matrices involved are not compatible with the expression. pub fn spmm_csc_dense<'a, T>(beta: T, c: impl Into>, alpha: T, @@ -38,6 +42,10 @@ fn spmm_csc_dense_(beta: T, /// /// If the pattern of `c` does not accommodate all the non-zero entries in `a`, an error is /// returned. +/// +/// # Panics +/// +/// Panics if the dimensions of the matrices involved are not compatible with the expression. pub fn spadd_csc_prealloc(beta: T, c: &mut CscMatrix, alpha: T, @@ -52,6 +60,15 @@ pub fn spadd_csc_prealloc(beta: T, /// Sparse-sparse matrix multiplication, `C <- beta * C + alpha * op(A) * op(B)`. +/// +/// # Errors +/// +/// If the sparsity pattern of `C` is not able to store the result of the operation, +/// an error is returned. +/// +/// # Panics +/// +/// Panics if the dimensions of the matrices involved are not compatible with the expression. pub fn spmm_csc_prealloc( beta: T, c: &mut CscMatrix, @@ -95,7 +112,11 @@ pub fn spmm_csc_prealloc( /// /// Only the lower triangular part of L is read, and the result is stored in B. /// -/// ## Panics +/// # Errors +/// +/// An error is returned if the system can not be solved due to the matrix being singular. +/// +/// # Panics /// /// Panics if `L` is not square, or if `L` and `B` are not dimensionally compatible. pub fn spsolve_csc_lower_triangular<'a, T: RealField>( diff --git a/nalgebra-sparse/src/ops/serial/csr.rs b/nalgebra-sparse/src/ops/serial/csr.rs index cec051d5..fc632a35 100644 --- a/nalgebra-sparse/src/ops/serial/csr.rs +++ b/nalgebra-sparse/src/ops/serial/csr.rs @@ -33,8 +33,14 @@ where /// Sparse matrix addition `C <- beta * C + alpha * op(A)`. /// +/// # Errors +/// /// If the pattern of `c` does not accommodate all the non-zero entries in `a`, an error is /// returned. +/// +/// # Panics +/// +/// Panics if the dimensions of the matrices involved are not compatible with the expression. pub fn spadd_csr_prealloc(beta: T, c: &mut CsrMatrix, alpha: T, @@ -48,6 +54,14 @@ where } /// Sparse-sparse matrix multiplication, `C <- beta * C + alpha * op(A) * op(B)`. +/// +/// # Errors +/// +/// If the pattern of `C` is not able to hold the result of the operation, an error is returned. +/// +/// # Panics +/// +/// Panics if the dimensions of the matrices involved are not compatible with the expression. pub fn spmm_csr_prealloc( beta: T, c: &mut CsrMatrix, diff --git a/nalgebra-sparse/src/ops/serial/pattern.rs b/nalgebra-sparse/src/ops/serial/pattern.rs index 168a4d61..2569bad3 100644 --- a/nalgebra-sparse/src/ops/serial/pattern.rs +++ b/nalgebra-sparse/src/ops/serial/pattern.rs @@ -11,7 +11,7 @@ use std::iter; /// /// # Panics /// -/// Panics if the patterns don't have the same major and minor dimensions. +/// Panics if the patterns do not have the same major and minor dimensions. pub fn spadd_pattern(a: &SparsityPattern, b: &SparsityPattern) -> SparsityPattern { From 7473d54d7431d309180073ba92779d2ae5d07c0e Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 25 Jan 2021 17:26:27 +0100 Subject: [PATCH 092/183] rustfmt --- nalgebra-sparse/src/convert/impl_std_ops.rs | 46 +-- nalgebra-sparse/src/convert/serial.rs | 104 +++--- nalgebra-sparse/src/coo.rs | 19 +- nalgebra-sparse/src/cs.rs | 137 +++++--- nalgebra-sparse/src/csc.rs | 141 ++++---- nalgebra-sparse/src/csr.rs | 147 ++++---- nalgebra-sparse/src/factorization/cholesky.rs | 58 ++-- nalgebra-sparse/src/factorization/mod.rs | 2 +- nalgebra-sparse/src/lib.rs | 25 +- nalgebra-sparse/src/matrixcompare.rs | 50 +-- nalgebra-sparse/src/ops/impl_std_ops.rs | 33 +- nalgebra-sparse/src/ops/mod.rs | 10 +- nalgebra-sparse/src/ops/serial/cs.rs | 89 ++--- nalgebra-sparse/src/ops/serial/csc.rs | 118 ++++--- nalgebra-sparse/src/ops/serial/csr.rs | 68 ++-- nalgebra-sparse/src/ops/serial/mod.rs | 74 ++-- nalgebra-sparse/src/ops/serial/pattern.rs | 47 ++- nalgebra-sparse/src/pattern.rs | 82 +++-- nalgebra-sparse/src/proptest.rs | 291 ++++++++-------- .../src/proptest/proptest_patched.rs | 20 +- nalgebra-sparse/tests/common/mod.rs | 55 ++- nalgebra-sparse/tests/unit.rs | 2 +- .../tests/unit_tests/convert_serial.rs | 122 ++++--- nalgebra-sparse/tests/unit_tests/coo.rs | 115 +++++-- nalgebra-sparse/tests/unit_tests/csc.rs | 113 +++++-- nalgebra-sparse/tests/unit_tests/csr.rs | 114 +++++-- nalgebra-sparse/tests/unit_tests/mod.rs | 10 +- nalgebra-sparse/tests/unit_tests/ops.rs | 316 ++++++++++-------- nalgebra-sparse/tests/unit_tests/pattern.rs | 58 +++- nalgebra-sparse/tests/unit_tests/proptest.rs | 80 +++-- 30 files changed, 1477 insertions(+), 1069 deletions(-) diff --git a/nalgebra-sparse/src/convert/impl_std_ops.rs b/nalgebra-sparse/src/convert/impl_std_ops.rs index 66f70408..ba4c015b 100644 --- a/nalgebra-sparse/src/convert/impl_std_ops.rs +++ b/nalgebra-sparse/src/convert/impl_std_ops.rs @@ -1,17 +1,17 @@ -use crate::coo::CooMatrix; use crate::convert::serial::*; -use nalgebra::{Matrix, Scalar, Dim, ClosedAdd, DMatrix}; -use nalgebra::storage::{Storage}; -use num_traits::Zero; -use crate::csr::CsrMatrix; +use crate::coo::CooMatrix; use crate::csc::CscMatrix; +use crate::csr::CsrMatrix; +use nalgebra::storage::Storage; +use nalgebra::{ClosedAdd, DMatrix, Dim, Matrix, Scalar}; +use num_traits::Zero; impl<'a, T, R, C, S> From<&'a Matrix> for CooMatrix where T: Scalar + Zero, R: Dim, C: Dim, - S: Storage + S: Storage, { fn from(matrix: &'a Matrix) -> Self { convert_dense_coo(matrix) @@ -29,7 +29,7 @@ where impl<'a, T> From<&'a CooMatrix> for CsrMatrix where - T: Scalar + Zero + ClosedAdd + T: Scalar + Zero + ClosedAdd, { fn from(matrix: &'a CooMatrix) -> Self { convert_coo_csr(matrix) @@ -38,7 +38,7 @@ where impl<'a, T> From<&'a CsrMatrix> for CooMatrix where - T: Scalar + Zero + ClosedAdd + T: Scalar + Zero + ClosedAdd, { fn from(matrix: &'a CsrMatrix) -> Self { convert_csr_coo(matrix) @@ -50,7 +50,7 @@ where T: Scalar + Zero, R: Dim, C: Dim, - S: Storage + S: Storage, { fn from(matrix: &'a Matrix) -> Self { convert_dense_csr(matrix) @@ -59,7 +59,7 @@ where impl<'a, T> From<&'a CsrMatrix> for DMatrix where - T: Scalar + Zero + ClosedAdd + T: Scalar + Zero + ClosedAdd, { fn from(matrix: &'a CsrMatrix) -> Self { convert_csr_dense(matrix) @@ -68,7 +68,7 @@ where impl<'a, T> From<&'a CooMatrix> for CscMatrix where - T: Scalar + Zero + ClosedAdd + T: Scalar + Zero + ClosedAdd, { fn from(matrix: &'a CooMatrix) -> Self { convert_coo_csc(matrix) @@ -77,7 +77,7 @@ where impl<'a, T> From<&'a CscMatrix> for CooMatrix where - T: Scalar + Zero + T: Scalar + Zero, { fn from(matrix: &'a CscMatrix) -> Self { convert_csc_coo(matrix) @@ -85,11 +85,11 @@ where } impl<'a, T, R, C, S> From<&'a Matrix> for CscMatrix - where - T: Scalar + Zero, - R: Dim, - C: Dim, - S: Storage +where + T: Scalar + Zero, + R: Dim, + C: Dim, + S: Storage, { fn from(matrix: &'a Matrix) -> Self { convert_dense_csc(matrix) @@ -97,8 +97,8 @@ impl<'a, T, R, C, S> From<&'a Matrix> for CscMatrix } impl<'a, T> From<&'a CscMatrix> for DMatrix - where - T: Scalar + Zero + ClosedAdd +where + T: Scalar + Zero + ClosedAdd, { fn from(matrix: &'a CscMatrix) -> Self { convert_csc_dense(matrix) @@ -106,8 +106,8 @@ impl<'a, T> From<&'a CscMatrix> for DMatrix } impl<'a, T> From<&'a CscMatrix> for CsrMatrix - where - T: Scalar +where + T: Scalar, { fn from(matrix: &'a CscMatrix) -> Self { convert_csc_csr(matrix) @@ -116,9 +116,9 @@ impl<'a, T> From<&'a CscMatrix> for CsrMatrix impl<'a, T> From<&'a CsrMatrix> for CscMatrix where - T: Scalar + T: Scalar, { fn from(matrix: &'a CsrMatrix) -> Self { convert_csr_csc(matrix) } -} \ No newline at end of file +} diff --git a/nalgebra-sparse/src/convert/serial.rs b/nalgebra-sparse/src/convert/serial.rs index 67a57966..7e0da7bc 100644 --- a/nalgebra-sparse/src/convert/serial.rs +++ b/nalgebra-sparse/src/convert/serial.rs @@ -7,8 +7,8 @@ use std::ops::Add; use num_traits::Zero; -use nalgebra::{ClosedAdd, Dim, DMatrix, Matrix, Scalar}; use nalgebra::storage::Storage; +use nalgebra::{ClosedAdd, DMatrix, Dim, Matrix, Scalar}; use crate::coo::CooMatrix; use crate::cs; @@ -21,7 +21,7 @@ where T: Scalar + Zero, R: Dim, C: Dim, - S: Storage + S: Storage, { let mut coo = CooMatrix::new(dense.nrows(), dense.ncols()); @@ -52,12 +52,14 @@ where /// Converts a [`CooMatrix`] to a [`CsrMatrix`]. pub fn convert_coo_csr(coo: &CooMatrix) -> CsrMatrix where - T: Scalar + Zero + T: Scalar + Zero, { - let (offsets, indices, values) = convert_coo_cs(coo.nrows(), - coo.row_indices(), - coo.col_indices(), - coo.values()); + let (offsets, indices, values) = convert_coo_cs( + coo.nrows(), + coo.row_indices(), + coo.col_indices(), + coo.values(), + ); // TODO: Avoid "try_from" since it validates the data? (requires unsafe, should benchmark // to see if it can be justified for performance reasons) @@ -66,8 +68,7 @@ where } /// Converts a [`CsrMatrix`] to a [`CooMatrix`]. -pub fn convert_csr_coo(csr: &CsrMatrix) -> CooMatrix -{ +pub fn convert_csr_coo(csr: &CsrMatrix) -> CooMatrix { let mut result = CooMatrix::new(csr.nrows(), csr.ncols()); for (i, j, v) in csr.triplet_iter() { result.push(i, j, v.inlined_clone()); @@ -76,9 +77,9 @@ pub fn convert_csr_coo(csr: &CsrMatrix) -> CooMatrix } /// Converts a [`CsrMatrix`] to a dense matrix. -pub fn convert_csr_dense(csr:& CsrMatrix) -> DMatrix +pub fn convert_csr_dense(csr: &CsrMatrix) -> DMatrix where - T: Scalar + ClosedAdd + Zero + T: Scalar + ClosedAdd + Zero, { let mut output = DMatrix::zeros(csr.nrows(), csr.ncols()); @@ -95,7 +96,7 @@ where T: Scalar + Zero, R: Dim, C: Dim, - S: Storage + S: Storage, { let mut row_offsets = Vec::with_capacity(dense.nrows() + 1); let mut col_idx = Vec::new(); @@ -105,8 +106,8 @@ where // nalgebra's column-major storage. The alternative would be to perform an initial sweep // to count number of non-zeros per row. row_offsets.push(0); - for i in 0 .. dense.nrows() { - for j in 0 .. dense.ncols() { + for i in 0..dense.nrows() { + for j in 0..dense.ncols() { let v = dense.index((i, j)); if v != &T::zero() { col_idx.push(j); @@ -125,12 +126,14 @@ where /// Converts a [`CooMatrix`] to a [`CscMatrix`]. pub fn convert_coo_csc(coo: &CooMatrix) -> CscMatrix where - T: Scalar + Zero + T: Scalar + Zero, { - let (offsets, indices, values) = convert_coo_cs(coo.ncols(), - coo.col_indices(), - coo.row_indices(), - coo.values()); + let (offsets, indices, values) = convert_coo_cs( + coo.ncols(), + coo.col_indices(), + coo.row_indices(), + coo.values(), + ); // TODO: Avoid "try_from" since it validates the data? (requires unsafe, should benchmark // to see if it can be justified for performance reasons) @@ -141,7 +144,7 @@ where /// Converts a [`CscMatrix`] to a [`CooMatrix`]. pub fn convert_csc_coo(csc: &CscMatrix) -> CooMatrix where - T: Scalar + T: Scalar, { let mut coo = CooMatrix::new(csc.nrows(), csc.ncols()); for (i, j, v) in csc.triplet_iter() { @@ -153,7 +156,7 @@ where /// Converts a [`CscMatrix`] to a dense matrix. pub fn convert_csc_dense(csc: &CscMatrix) -> DMatrix where - T: Scalar + ClosedAdd + Zero + T: Scalar + ClosedAdd + Zero, { let mut output = DMatrix::zeros(csc.nrows(), csc.ncols()); @@ -166,19 +169,19 @@ where /// Converts a dense matrix to a [`CscMatrix`]. pub fn convert_dense_csc(dense: &Matrix) -> CscMatrix - where - T: Scalar + Zero, - R: Dim, - C: Dim, - S: Storage +where + T: Scalar + Zero, + R: Dim, + C: Dim, + S: Storage, { let mut col_offsets = Vec::with_capacity(dense.ncols() + 1); let mut row_idx = Vec::new(); let mut values = Vec::new(); col_offsets.push(0); - for j in 0 .. dense.ncols() { - for i in 0 .. dense.nrows() { + for j in 0..dense.ncols() { + for i in 0..dense.nrows() { let v = dense.index((i, j)); if v != &T::zero() { row_idx.push(i); @@ -197,13 +200,15 @@ pub fn convert_dense_csc(dense: &Matrix) -> CscMatrix /// Converts a [`CsrMatrix`] to a [`CscMatrix`]. pub fn convert_csr_csc(csr: &CsrMatrix) -> CscMatrix where - T: Scalar + T: Scalar, { - let (offsets, indices, values) = cs::transpose_cs(csr.nrows(), - csr.ncols(), - csr.row_offsets(), - csr.col_indices(), - csr.values()); + let (offsets, indices, values) = cs::transpose_cs( + csr.nrows(), + csr.ncols(), + csr.row_offsets(), + csr.col_indices(), + csr.values(), + ); // TODO: Avoid data validity check? CscMatrix::try_from_csc_data(csr.nrows(), csr.ncols(), offsets, indices, values) @@ -212,27 +217,30 @@ where /// Converts a [`CscMatrix`] to a [`CsrMatrix`]. pub fn convert_csc_csr(csc: &CscMatrix) -> CsrMatrix - where - T: Scalar +where + T: Scalar, { - let (offsets, indices, values) = cs::transpose_cs(csc.ncols(), - csc.nrows(), - csc.col_offsets(), - csc.row_indices(), - csc.values()); + let (offsets, indices, values) = cs::transpose_cs( + csc.ncols(), + csc.nrows(), + csc.col_offsets(), + csc.row_indices(), + csc.values(), + ); // TODO: Avoid data validity check? CsrMatrix::try_from_csr_data(csc.nrows(), csc.ncols(), offsets, indices, values) .expect("Internal error: Invalid CSR data during CSC->CSR conversion") } -fn convert_coo_cs(major_dim: usize, - major_indices: &[usize], - minor_indices: &[usize], - values: &[T]) - -> (Vec, Vec, Vec) +fn convert_coo_cs( + major_dim: usize, + major_indices: &[usize], + minor_indices: &[usize], + values: &[T], +) -> (Vec, Vec, Vec) where - T: Scalar + Zero + T: Scalar + Zero, { assert_eq!(major_indices.len(), minor_indices.len()); assert_eq!(minor_indices.len(), values.len()); @@ -416,4 +424,4 @@ fn combine_duplicates( produce_value(combined_value); i = j; } -} \ No newline at end of file +} diff --git a/nalgebra-sparse/src/coo.rs b/nalgebra-sparse/src/coo.rs index 6867a327..6b641513 100644 --- a/nalgebra-sparse/src/coo.rs +++ b/nalgebra-sparse/src/coo.rs @@ -45,8 +45,7 @@ pub struct CooMatrix { values: Vec, } -impl CooMatrix -{ +impl CooMatrix { /// Construct a zero COO matrix of the given dimensions. /// /// Specifically, the collection of triplets - corresponding to explicitly stored entries - @@ -78,11 +77,13 @@ impl CooMatrix use crate::SparseFormatErrorKind::*; if row_indices.len() != col_indices.len() { return Err(SparseFormatError::from_kind_and_msg( - InvalidStructure, "Number of row and col indices must be the same." + InvalidStructure, + "Number of row and col indices must be the same.", )); } else if col_indices.len() != values.len() { return Err(SparseFormatError::from_kind_and_msg( - InvalidStructure, "Number of col indices and values must be the same." + InvalidStructure, + "Number of col indices and values must be the same.", )); } @@ -90,9 +91,15 @@ impl CooMatrix let col_indices_in_bounds = col_indices.iter().all(|j| *j < ncols); if !row_indices_in_bounds { - Err(SparseFormatError::from_kind_and_msg(IndexOutOfBounds, "Row index out of bounds.")) + Err(SparseFormatError::from_kind_and_msg( + IndexOutOfBounds, + "Row index out of bounds.", + )) } else if !col_indices_in_bounds { - Err(SparseFormatError::from_kind_and_msg(IndexOutOfBounds, "Col index out of bounds.")) + Err(SparseFormatError::from_kind_and_msg( + IndexOutOfBounds, + "Col index out of bounds.", + )) } else { Ok(Self { nrows, diff --git a/nalgebra-sparse/src/cs.rs b/nalgebra-sparse/src/cs.rs index f3c0a46d..d6f9b229 100644 --- a/nalgebra-sparse/src/cs.rs +++ b/nalgebra-sparse/src/cs.rs @@ -5,8 +5,8 @@ use num_traits::One; use nalgebra::Scalar; -use crate::{SparseEntry, SparseEntryMut}; use crate::pattern::SparsityPattern; +use crate::{SparseEntry, SparseEntryMut}; /// An abstract compressed matrix. /// @@ -18,7 +18,7 @@ use crate::pattern::SparsityPattern; #[derive(Debug, Clone, PartialEq, Eq)] pub struct CsMatrix { sparsity_pattern: SparsityPattern, - values: Vec + values: Vec, } impl CsMatrix { @@ -50,14 +50,22 @@ impl CsMatrix { #[inline] pub fn cs_data(&self) -> (&[usize], &[usize], &[T]) { let pattern = self.pattern(); - (pattern.major_offsets(), pattern.minor_indices(), &self.values) + ( + pattern.major_offsets(), + pattern.minor_indices(), + &self.values, + ) } /// Returns the raw data represented as a tuple `(major_offsets, minor_indices, values)`. #[inline] pub fn cs_data_mut(&mut self) -> (&[usize], &[usize], &mut [T]) { let pattern = &mut self.sparsity_pattern; - (pattern.major_offsets(), pattern.minor_indices(), &mut self.values) + ( + pattern.major_offsets(), + pattern.minor_indices(), + &mut self.values, + ) } #[inline] @@ -66,9 +74,12 @@ impl CsMatrix { } #[inline] - pub fn from_pattern_and_values(pattern: SparsityPattern, values: Vec) - -> Self { - assert_eq!(pattern.nnz(), values.len(), "Internal error: consumers should verify shape compatibility."); + pub fn from_pattern_and_values(pattern: SparsityPattern, values: Vec) -> Self { + assert_eq!( + pattern.nnz(), + values.len(), + "Internal error: consumers should verify shape compatibility." + ); Self { sparsity_pattern: pattern, values, @@ -80,7 +91,7 @@ impl CsMatrix { pub fn get_index_range(&self, row_index: usize) -> Option> { let row_begin = *self.sparsity_pattern.major_offsets().get(row_index)?; let row_end = *self.sparsity_pattern.major_offsets().get(row_index + 1)?; - Some(row_begin .. row_end) + Some(row_begin..row_end) } pub fn take_pattern_and_values(self) -> (SparsityPattern, Vec) { @@ -105,13 +116,21 @@ impl CsMatrix { let (_, minor_indices, values) = self.cs_data(); let minor_indices = &minor_indices[row_range.clone()]; let values = &values[row_range]; - get_entry_from_slices(self.pattern().minor_dim(), minor_indices, values, minor_index) + get_entry_from_slices( + self.pattern().minor_dim(), + minor_indices, + values, + minor_index, + ) } /// Returns a mutable entry for the given major/minor indices, or `None` if the indices are out /// of bounds. - pub fn get_entry_mut(&mut self, major_index: usize, minor_index: usize) - -> Option> { + pub fn get_entry_mut( + &mut self, + major_index: usize, + minor_index: usize, + ) -> Option> { let row_range = self.get_index_range(major_index)?; let minor_dim = self.pattern().minor_dim(); let (_, minor_indices, values) = self.cs_data_mut(); @@ -126,7 +145,7 @@ impl CsMatrix { Some(CsLane { minor_indices: &minor_indices[range.clone()], values: &values[range], - minor_dim: self.pattern().minor_dim() + minor_dim: self.pattern().minor_dim(), }) } @@ -138,7 +157,7 @@ impl CsMatrix { Some(CsLaneMut { minor_dim, minor_indices: &minor_indices[range.clone()], - values: &mut values[range] + values: &mut values[range], }) } @@ -156,7 +175,7 @@ impl CsMatrix { pub fn filter

(&self, predicate: P) -> Self where T: Clone, - P: Fn(usize, usize, &T) -> bool + P: Fn(usize, usize, &T) -> bool, { let (major_dim, minor_dim) = (self.pattern().major_dim(), self.pattern().minor_dim()); let mut new_offsets = Vec::with_capacity(self.pattern().major_dim() + 1); @@ -180,16 +199,17 @@ impl CsMatrix { major_dim, minor_dim, new_offsets, - new_indices) - .expect("Internal error: Sparsity pattern must always be valid."); + new_indices, + ) + .expect("Internal error: Sparsity pattern must always be valid."); Self::from_pattern_and_values(new_pattern, new_values) } /// Returns the diagonal of the matrix as a sparse matrix. pub fn diagonal_as_matrix(&self) -> Self - where - T: Clone + where + T: Clone, { // TODO: This might be faster with a binary search for each diagonal entry self.filter(|i, j, _| i == j) @@ -199,13 +219,13 @@ impl CsMatrix { impl CsMatrix { #[inline] pub fn identity(n: usize) -> Self { - let offsets: Vec<_> = (0 ..= n).collect(); - let indices: Vec<_> = (0 .. n).collect(); + let offsets: Vec<_> = (0..=n).collect(); + let indices: Vec<_> = (0..n).collect(); let values = vec![T::one(); n]; // TODO: We should skip checks here - let pattern = SparsityPattern::try_from_offsets_and_indices(n, n, offsets, indices) - .unwrap(); + let pattern = + SparsityPattern::try_from_offsets_and_indices(n, n, offsets, indices).unwrap(); Self::from_pattern_and_values(pattern, values) } } @@ -214,7 +234,8 @@ fn get_entry_from_slices<'a, T>( minor_dim: usize, minor_indices: &'a [usize], values: &'a [T], - global_minor_index: usize) -> Option> { + global_minor_index: usize, +) -> Option> { let local_index = minor_indices.binary_search(&global_minor_index); if let Ok(local_index) = local_index { Some(SparseEntry::NonZero(&values[local_index])) @@ -229,7 +250,8 @@ fn get_mut_entry_from_slices<'a, T>( minor_dim: usize, minor_indices: &'a [usize], values: &'a mut [T], - global_minor_indices: usize) -> Option> { + global_minor_indices: usize, +) -> Option> { let local_index = minor_indices.binary_search(&global_minor_indices); if let Ok(local_index) = local_index { Some(SparseEntryMut::NonZero(&mut values[local_index])) @@ -244,14 +266,14 @@ fn get_mut_entry_from_slices<'a, T>( pub struct CsLane<'a, T> { minor_dim: usize, minor_indices: &'a [usize], - values: &'a [T] + values: &'a [T], } #[derive(Debug, PartialEq, Eq)] pub struct CsLaneMut<'a, T> { minor_dim: usize, minor_indices: &'a [usize], - values: &'a mut [T] + values: &'a mut [T], } pub struct CsLaneIter<'a, T> { @@ -266,14 +288,14 @@ impl<'a, T> CsLaneIter<'a, T> { Self { current_lane_idx: 0, pattern, - remaining_values: values + remaining_values: values, } } } impl<'a, T> Iterator for CsLaneIter<'a, T> - where - T: 'a +where + T: 'a, { type Item = CsLane<'a, T>; @@ -284,13 +306,13 @@ impl<'a, T> Iterator for CsLaneIter<'a, T> if let Some(minor_indices) = lane { let count = minor_indices.len(); let values_in_lane = &self.remaining_values[..count]; - self.remaining_values = &self.remaining_values[count ..]; + self.remaining_values = &self.remaining_values[count..]; self.current_lane_idx += 1; Some(CsLane { minor_dim, minor_indices, - values: values_in_lane + values: values_in_lane, }) } else { None @@ -310,14 +332,14 @@ impl<'a, T> CsLaneIterMut<'a, T> { Self { current_lane_idx: 0, pattern, - remaining_values: values + remaining_values: values, } } } impl<'a, T> Iterator for CsLaneIterMut<'a, T> - where - T: 'a +where + T: 'a, { type Item = CsLaneMut<'a, T>; @@ -336,7 +358,7 @@ impl<'a, T> Iterator for CsLaneIterMut<'a, T> Some(CsLaneMut { minor_dim, minor_indices, - values: values_in_lane + values: values_in_lane, }) } else { None @@ -375,10 +397,11 @@ macro_rules! impl_cs_lane_common_methods { self.minor_dim, self.minor_indices, self.values, - global_col_index) + global_col_index, + ) } } - } + }; } impl_cs_lane_common_methods!(CsLane<'a, T>); @@ -394,10 +417,12 @@ impl<'a, T> CsLaneMut<'a, T> { } pub fn get_entry_mut(&mut self, global_minor_index: usize) -> Option> { - get_mut_entry_from_slices(self.minor_dim, - self.minor_indices, - self.values, - global_minor_index) + get_mut_entry_from_slices( + self.minor_dim, + self.minor_indices, + self.values, + global_minor_index, + ) } } @@ -405,7 +430,7 @@ impl<'a, T> CsLaneMut<'a, T> { /// TODO: This doesn't belong here. struct UninitVec { vec: Vec, - len: usize + len: usize, } impl UninitVec { @@ -414,7 +439,7 @@ impl UninitVec { vec: Vec::with_capacity(len), // We need to store len separately, because for zero-sized types, // Vec::with_capacity(len) does not give vec.capacity() == len - len + len, } } @@ -440,14 +465,14 @@ impl UninitVec { /// This means that major and minor roles are switched. This is used for converting between CSR /// and CSC formats. pub fn transpose_cs( - major_dim: usize, - minor_dim: usize, - source_major_offsets: &[usize], - source_minor_indices: &[usize], - values: &[T]) - -> (Vec, Vec, Vec) + major_dim: usize, + minor_dim: usize, + source_major_offsets: &[usize], + source_minor_indices: &[usize], + values: &[T], +) -> (Vec, Vec, Vec) where - T: Scalar + T: Scalar, { assert_eq!(source_major_offsets.len(), major_dim + 1); assert_eq!(source_minor_indices.len(), values.len()); @@ -470,18 +495,20 @@ where // Keep track of how many entries we have placed in each target major lane let mut current_target_major_counts = vec![0; minor_dim]; - for source_major_idx in 0 .. major_dim { + for source_major_idx in 0..major_dim { let source_lane_begin = source_major_offsets[source_major_idx]; let source_lane_end = source_major_offsets[source_major_idx + 1]; - let source_lane_indices = &source_minor_indices[source_lane_begin .. source_lane_end]; - let source_lane_values = &values[source_lane_begin .. source_lane_end]; + let source_lane_indices = &source_minor_indices[source_lane_begin..source_lane_end]; + let source_lane_values = &values[source_lane_begin..source_lane_end]; for (&source_minor_idx, val) in source_lane_indices.iter().zip(source_lane_values) { // Compute the offset in the target data for this particular source entry - let target_lane_count = &mut current_target_major_counts[source_minor_idx]; + let target_lane_count = &mut current_target_major_counts[source_minor_idx]; let entry_offset = target_offsets[source_minor_idx] + *target_lane_count; target_indices[entry_offset] = source_major_idx; - unsafe { target_values.set(entry_offset, val.inlined_clone()); } + unsafe { + target_values.set(entry_offset, val.inlined_clone()); + } *target_lane_count += 1; } } diff --git a/nalgebra-sparse/src/csc.rs b/nalgebra-sparse/src/csc.rs index 8b5d0589..89cb88cd 100644 --- a/nalgebra-sparse/src/csc.rs +++ b/nalgebra-sparse/src/csc.rs @@ -3,14 +3,14 @@ //! This is the module-level documentation. See [`CscMatrix`] for the main documentation of the //! CSC implementation. -use crate::{SparseFormatError, SparseFormatErrorKind, SparseEntry, SparseEntryMut}; -use crate::pattern::{SparsityPattern, SparsityPatternFormatError, SparsityPatternIter}; +use crate::cs::{CsLane, CsLaneIter, CsLaneIterMut, CsLaneMut, CsMatrix}; use crate::csr::CsrMatrix; -use crate::cs::{CsMatrix, CsLane, CsLaneMut, CsLaneIter, CsLaneIterMut}; +use crate::pattern::{SparsityPattern, SparsityPatternFormatError, SparsityPatternIter}; +use crate::{SparseEntry, SparseEntryMut, SparseFormatError, SparseFormatErrorKind}; -use std::slice::{IterMut, Iter}; -use num_traits::{One}; use nalgebra::Scalar; +use num_traits::One; +use std::slice::{Iter, IterMut}; /// A CSC representation of a sparse matrix. /// @@ -130,7 +130,7 @@ impl CscMatrix { /// Create a zero CSC matrix with no explicitly stored entries. pub fn zeros(nrows: usize, ncols: usize) -> Self { Self { - cs: CsMatrix::new(ncols, nrows) + cs: CsMatrix::new(ncols, nrows), } } @@ -196,8 +196,12 @@ impl CscMatrix { values: Vec, ) -> Result { let pattern = SparsityPattern::try_from_offsets_and_indices( - num_cols, num_rows, col_offsets, row_indices) - .map_err(pattern_format_error_to_csc_error)?; + num_cols, + num_rows, + col_offsets, + row_indices, + ) + .map_err(pattern_format_error_to_csc_error)?; Self::try_from_pattern_and_values(pattern, values) } @@ -205,16 +209,19 @@ impl CscMatrix { /// /// Returns an error if the number of values does not match the number of minor indices /// in the pattern. - pub fn try_from_pattern_and_values(pattern: SparsityPattern, values: Vec) - -> Result { + pub fn try_from_pattern_and_values( + pattern: SparsityPattern, + values: Vec, + ) -> Result { if pattern.nnz() == values.len() { Ok(Self { - cs: CsMatrix::from_pattern_and_values(pattern, values) + cs: CsMatrix::from_pattern_and_values(pattern, values), }) } else { Err(SparseFormatError::from_kind_and_msg( SparseFormatErrorKind::InvalidStructure, - "Number of values and row indices must be the same")) + "Number of values and row indices must be the same", + )) } } @@ -239,7 +246,7 @@ impl CscMatrix { pub fn triplet_iter(&self) -> CscTripletIter { CscTripletIter { pattern_iter: self.pattern().entries(), - values_iter: self.values().iter() + values_iter: self.values().iter(), } } @@ -270,7 +277,7 @@ impl CscMatrix { let (pattern, values) = self.cs.pattern_and_values_mut(); CscTripletIterMut { pattern_iter: pattern.entries(), - values_mut_iter: values.iter_mut() + values_mut_iter: values.iter_mut(), } } @@ -281,8 +288,7 @@ impl CscMatrix { /// Panics if column index is out of bounds. #[inline] pub fn col(&self, index: usize) -> CscCol { - self.get_col(index) - .expect("Row index must be in bounds") + self.get_col(index).expect("Row index must be in bounds") } /// Mutable column access for the given column index. @@ -299,23 +305,19 @@ impl CscMatrix { /// Return the column at the given column index, or `None` if out of bounds. #[inline] pub fn get_col(&self, index: usize) -> Option> { - self.cs - .get_lane(index) - .map(|lane| CscCol { lane }) + self.cs.get_lane(index).map(|lane| CscCol { lane }) } /// Mutable column access for the given column index, or `None` if out of bounds. #[inline] pub fn get_col_mut(&mut self, index: usize) -> Option> { - self.cs - .get_lane_mut(index) - .map(|lane| CscColMut { lane }) + self.cs.get_lane_mut(index).map(|lane| CscColMut { lane }) } /// An iterator over columns in the matrix. pub fn col_iter(&self) -> CscColIter { CscColIter { - lane_iter: CsLaneIter::new(self.pattern(), self.values()) + lane_iter: CsLaneIter::new(self.pattern(), self.values()), } } @@ -323,7 +325,7 @@ impl CscMatrix { pub fn col_iter_mut(&mut self) -> CscColIterMut { let (pattern, values) = self.cs.pattern_and_values_mut(); CscColIterMut { - lane_iter: CsLaneIterMut::new(pattern, values) + lane_iter: CsLaneIterMut::new(pattern, values), } } @@ -397,8 +399,11 @@ impl CscMatrix { /// /// Each call to this function incurs the cost of a binary search among the explicitly /// stored row entries for the given column. - pub fn get_entry_mut(&mut self, row_index: usize, col_index: usize) - -> Option> { + pub fn get_entry_mut( + &mut self, + row_index: usize, + col_index: usize, + ) -> Option> { self.cs.get_entry_mut(col_index, row_index) } @@ -444,11 +449,15 @@ impl CscMatrix { pub fn filter

(&self, predicate: P) -> Self where T: Clone, - P: Fn(usize, usize, &T) -> bool + P: Fn(usize, usize, &T) -> bool, { // Note: Predicate uses (row, col, value), so we have to switch around since // cs uses (major, minor, value) - Self { cs: self.cs.filter(|col_idx, row_idx, v| predicate(row_idx, col_idx, v)) } + Self { + cs: self + .cs + .filter(|col_idx, row_idx, v| predicate(row_idx, col_idx, v)), + } } /// Returns a new matrix representing the upper triangular part of this matrix. @@ -456,7 +465,7 @@ impl CscMatrix { /// The result includes the diagonal of the matrix. pub fn upper_triangle(&self) -> Self where - T: Clone + T: Clone, { self.filter(|i, j, _| i <= j) } @@ -466,7 +475,7 @@ impl CscMatrix { /// The result includes the diagonal of the matrix. pub fn lower_triangle(&self) -> Self where - T: Clone + T: Clone, { self.filter(|i, j, _| i >= j) } @@ -474,15 +483,17 @@ impl CscMatrix { /// Returns the diagonal of the matrix as a sparse matrix. pub fn diagonal_as_csc(&self) -> Self where - T: Clone + T: Clone, { - Self { cs: self.cs.diagonal_as_matrix() } + Self { + cs: self.cs.diagonal_as_matrix(), + } } } impl CscMatrix - where - T: Scalar +where + T: Scalar, { /// Compute the transpose of the matrix. pub fn transpose(&self) -> CscMatrix { @@ -495,7 +506,7 @@ impl CscMatrix { #[inline] pub fn identity(n: usize) -> Self { Self { - cs: CsMatrix::identity(n) + cs: CsMatrix::identity(n), } } } @@ -505,30 +516,34 @@ impl CscMatrix { /// This ensures that the terminology is consistent: we are talking about rows and columns, /// not lanes, major and minor dimensions. fn pattern_format_error_to_csc_error(err: SparsityPatternFormatError) -> SparseFormatError { - use SparsityPatternFormatError::*; - use SparsityPatternFormatError::DuplicateEntry as PatternDuplicateEntry; use SparseFormatError as E; use SparseFormatErrorKind as K; + use SparsityPatternFormatError::DuplicateEntry as PatternDuplicateEntry; + use SparsityPatternFormatError::*; match err { InvalidOffsetArrayLength => E::from_kind_and_msg( K::InvalidStructure, - "Length of col offset array is not equal to ncols + 1."), + "Length of col offset array is not equal to ncols + 1.", + ), InvalidOffsetFirstLast => E::from_kind_and_msg( K::InvalidStructure, - "First or last col offset is inconsistent with format specification."), + "First or last col offset is inconsistent with format specification.", + ), NonmonotonicOffsets => E::from_kind_and_msg( K::InvalidStructure, - "Col offsets are not monotonically increasing."), + "Col offsets are not monotonically increasing.", + ), NonmonotonicMinorIndices => E::from_kind_and_msg( K::InvalidStructure, - "Row indices are not monotonically increasing (sorted) within each column."), - MinorIndexOutOfBounds => E::from_kind_and_msg( - K::IndexOutOfBounds, - "Row indices are out of bounds."), - PatternDuplicateEntry => E::from_kind_and_msg( - K::DuplicateEntry, - "Matrix data contains duplicate entries."), + "Row indices are not monotonically increasing (sorted) within each column.", + ), + MinorIndexOutOfBounds => { + E::from_kind_and_msg(K::IndexOutOfBounds, "Row indices are out of bounds.") + } + PatternDuplicateEntry => { + E::from_kind_and_msg(K::DuplicateEntry, "Matrix data contains duplicate entries.") + } } } @@ -536,7 +551,7 @@ fn pattern_format_error_to_csc_error(err: SparsityPatternFormatError) -> SparseF #[derive(Debug)] pub struct CscTripletIter<'a, T> { pattern_iter: SparsityPatternIter<'a>, - values_iter: Iter<'a, T> + values_iter: Iter<'a, T>, } impl<'a, T: Clone> CscTripletIter<'a, T> { @@ -545,7 +560,7 @@ impl<'a, T: Clone> CscTripletIter<'a, T> { /// The triplet iterator returns references to the values. This method adapts the iterator /// so that the values are cloned. #[inline] - pub fn cloned_values(self) -> impl 'a + Iterator { + pub fn cloned_values(self) -> impl 'a + Iterator { self.map(|(i, j, v)| (i, j, v.clone())) } } @@ -559,7 +574,7 @@ impl<'a, T> Iterator for CscTripletIter<'a, T> { match (next_entry, next_value) { (Some((i, j)), Some(v)) => Some((j, i, v)), - _ => None + _ => None, } } } @@ -568,7 +583,7 @@ impl<'a, T> Iterator for CscTripletIter<'a, T> { #[derive(Debug)] pub struct CscTripletIterMut<'a, T> { pattern_iter: SparsityPatternIter<'a>, - values_mut_iter: IterMut<'a, T> + values_mut_iter: IterMut<'a, T>, } impl<'a, T> Iterator for CscTripletIterMut<'a, T> { @@ -581,7 +596,7 @@ impl<'a, T> Iterator for CscTripletIterMut<'a, T> { match (next_entry, next_value) { (Some((i, j)), Some(v)) => Some((j, i, v)), - _ => None + _ => None, } } } @@ -589,7 +604,7 @@ impl<'a, T> Iterator for CscTripletIterMut<'a, T> { /// An immutable representation of a column in a CSC matrix. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CscCol<'a, T> { - lane: CsLane<'a, T> + lane: CsLane<'a, T>, } /// A mutable representation of a column in a CSC matrix. @@ -598,7 +613,7 @@ pub struct CscCol<'a, T> { /// to the column cannot be modified. #[derive(Debug, PartialEq, Eq)] pub struct CscColMut<'a, T> { - lane: CsLaneMut<'a, T> + lane: CsLaneMut<'a, T>, } /// Implement the methods common to both CscCol and CscColMut @@ -637,7 +652,7 @@ macro_rules! impl_csc_col_common_methods { self.lane.get_entry(global_row_index) } } - } + }; } impl_csc_col_common_methods!(CscCol<'a, T>); @@ -666,33 +681,29 @@ impl<'a, T> CscColMut<'a, T> { /// Column iterator for [CscMatrix](struct.CscMatrix.html). pub struct CscColIter<'a, T> { - lane_iter: CsLaneIter<'a, T> + lane_iter: CsLaneIter<'a, T>, } impl<'a, T> Iterator for CscColIter<'a, T> { type Item = CscCol<'a, T>; fn next(&mut self) -> Option { - self.lane_iter - .next() - .map(|lane| CscCol { lane }) + self.lane_iter.next().map(|lane| CscCol { lane }) } } /// Mutable column iterator for [CscMatrix](struct.CscMatrix.html). pub struct CscColIterMut<'a, T> { - lane_iter: CsLaneIterMut<'a, T> + lane_iter: CsLaneIterMut<'a, T>, } impl<'a, T> Iterator for CscColIterMut<'a, T> where - T: 'a + T: 'a, { type Item = CscColMut<'a, T>; fn next(&mut self) -> Option { - self.lane_iter - .next() - .map(|lane| CscColMut { lane }) + self.lane_iter.next().map(|lane| CscColMut { lane }) } -} \ No newline at end of file +} diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index d1e9ab1f..24c45e74 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -2,15 +2,15 @@ //! //! This is the module-level documentation. See [`CsrMatrix`] for the main documentation of the //! CSC implementation. -use crate::{SparseFormatError, SparseFormatErrorKind, SparseEntry, SparseEntryMut}; -use crate::pattern::{SparsityPattern, SparsityPatternFormatError, SparsityPatternIter}; +use crate::cs::{CsLane, CsLaneIter, CsLaneIterMut, CsLaneMut, CsMatrix}; use crate::csc::CscMatrix; -use crate::cs::{CsMatrix, CsLaneIterMut, CsLaneIter, CsLane, CsLaneMut}; +use crate::pattern::{SparsityPattern, SparsityPatternFormatError, SparsityPatternIter}; +use crate::{SparseEntry, SparseEntryMut, SparseFormatError, SparseFormatErrorKind}; use nalgebra::Scalar; -use num_traits::{One}; +use num_traits::One; -use std::slice::{IterMut, Iter}; +use std::slice::{Iter, IterMut}; /// A CSR representation of a sparse matrix. /// @@ -130,7 +130,7 @@ impl CsrMatrix { /// Create a zero CSR matrix with no explicitly stored entries. pub fn zeros(nrows: usize, ncols: usize) -> Self { Self { - cs: CsMatrix::new(nrows, ncols) + cs: CsMatrix::new(nrows, ncols), } } @@ -198,8 +198,12 @@ impl CsrMatrix { values: Vec, ) -> Result { let pattern = SparsityPattern::try_from_offsets_and_indices( - num_rows, num_cols, row_offsets, col_indices) - .map_err(pattern_format_error_to_csr_error)?; + num_rows, + num_cols, + row_offsets, + col_indices, + ) + .map_err(pattern_format_error_to_csr_error)?; Self::try_from_pattern_and_values(pattern, values) } @@ -207,16 +211,19 @@ impl CsrMatrix { /// /// Returns an error if the number of values does not match the number of minor indices /// in the pattern. - pub fn try_from_pattern_and_values(pattern: SparsityPattern, values: Vec) - -> Result { + pub fn try_from_pattern_and_values( + pattern: SparsityPattern, + values: Vec, + ) -> Result { if pattern.nnz() == values.len() { Ok(Self { - cs: CsMatrix::from_pattern_and_values(pattern, values) + cs: CsMatrix::from_pattern_and_values(pattern, values), }) } else { Err(SparseFormatError::from_kind_and_msg( SparseFormatErrorKind::InvalidStructure, - "Number of values and column indices must be the same")) + "Number of values and column indices must be the same", + )) } } @@ -241,7 +248,7 @@ impl CsrMatrix { pub fn triplet_iter(&self) -> CsrTripletIter { CsrTripletIter { pattern_iter: self.pattern().entries(), - values_iter: self.values().iter() + values_iter: self.values().iter(), } } @@ -272,7 +279,7 @@ impl CsrMatrix { let (pattern, values) = self.cs.pattern_and_values_mut(); CsrTripletIterMut { pattern_iter: pattern.entries(), - values_mut_iter: values.iter_mut() + values_mut_iter: values.iter_mut(), } } @@ -283,8 +290,7 @@ impl CsrMatrix { /// Panics if row index is out of bounds. #[inline] pub fn row(&self, index: usize) -> CsrRow { - self.get_row(index) - .expect("Row index must be in bounds") + self.get_row(index).expect("Row index must be in bounds") } /// Mutable row access for the given row index. @@ -301,23 +307,19 @@ impl CsrMatrix { /// Return the row at the given row index, or `None` if out of bounds. #[inline] pub fn get_row(&self, index: usize) -> Option> { - self.cs - .get_lane(index) - .map(|lane| CsrRow { lane }) + self.cs.get_lane(index).map(|lane| CsrRow { lane }) } /// Mutable row access for the given row index, or `None` if out of bounds. #[inline] pub fn get_row_mut(&mut self, index: usize) -> Option> { - self.cs - .get_lane_mut(index) - .map(|lane| CsrRowMut { lane }) + self.cs.get_lane_mut(index).map(|lane| CsrRowMut { lane }) } /// An iterator over rows in the matrix. pub fn row_iter(&self) -> CsrRowIter { CsrRowIter { - lane_iter: CsLaneIter::new(self.pattern(), self.values()) + lane_iter: CsLaneIter::new(self.pattern(), self.values()), } } @@ -399,8 +401,11 @@ impl CsrMatrix { /// /// Each call to this function incurs the cost of a binary search among the explicitly /// stored column entries for the given row. - pub fn get_entry_mut(&mut self, row_index: usize, col_index: usize) - -> Option> { + pub fn get_entry_mut( + &mut self, + row_index: usize, + col_index: usize, + ) -> Option> { self.cs.get_entry_mut(row_index, col_index) } @@ -444,19 +449,23 @@ impl CsrMatrix { /// Creates a sparse matrix that contains only the explicit entries decided by the /// given predicate. pub fn filter

(&self, predicate: P) -> Self - where - T: Clone, - P: Fn(usize, usize, &T) -> bool + where + T: Clone, + P: Fn(usize, usize, &T) -> bool, { - Self { cs: self.cs.filter(|row_idx, col_idx, v| predicate(row_idx, col_idx, v)) } + Self { + cs: self + .cs + .filter(|row_idx, col_idx, v| predicate(row_idx, col_idx, v)), + } } /// Returns a new matrix representing the upper triangular part of this matrix. /// /// The result includes the diagonal of the matrix. pub fn upper_triangle(&self) -> Self - where - T: Clone + where + T: Clone, { self.filter(|i, j, _| i <= j) } @@ -465,24 +474,26 @@ impl CsrMatrix { /// /// The result includes the diagonal of the matrix. pub fn lower_triangle(&self) -> Self - where - T: Clone + where + T: Clone, { self.filter(|i, j, _| i >= j) } /// Returns the diagonal of the matrix as a sparse matrix. pub fn diagonal_as_csr(&self) -> Self - where - T: Clone + where + T: Clone, { - Self { cs: self.cs.diagonal_as_matrix() } + Self { + cs: self.cs.diagonal_as_matrix(), + } } } impl CsrMatrix where - T: Scalar + T: Scalar, { /// Compute the transpose of the matrix. pub fn transpose(&self) -> CsrMatrix { @@ -495,7 +506,7 @@ impl CsrMatrix { #[inline] pub fn identity(n: usize) -> Self { Self { - cs: CsMatrix::identity(n) + cs: CsMatrix::identity(n), } } } @@ -505,30 +516,34 @@ impl CsrMatrix { /// This ensures that the terminology is consistent: we are talking about rows and columns, /// not lanes, major and minor dimensions. fn pattern_format_error_to_csr_error(err: SparsityPatternFormatError) -> SparseFormatError { - use SparsityPatternFormatError::*; - use SparsityPatternFormatError::DuplicateEntry as PatternDuplicateEntry; use SparseFormatError as E; use SparseFormatErrorKind as K; + use SparsityPatternFormatError::DuplicateEntry as PatternDuplicateEntry; + use SparsityPatternFormatError::*; match err { InvalidOffsetArrayLength => E::from_kind_and_msg( K::InvalidStructure, - "Length of row offset array is not equal to nrows + 1."), + "Length of row offset array is not equal to nrows + 1.", + ), InvalidOffsetFirstLast => E::from_kind_and_msg( K::InvalidStructure, - "First or last row offset is inconsistent with format specification."), + "First or last row offset is inconsistent with format specification.", + ), NonmonotonicOffsets => E::from_kind_and_msg( K::InvalidStructure, - "Row offsets are not monotonically increasing."), + "Row offsets are not monotonically increasing.", + ), NonmonotonicMinorIndices => E::from_kind_and_msg( K::InvalidStructure, - "Column indices are not monotonically increasing (sorted) within each row."), - MinorIndexOutOfBounds => E::from_kind_and_msg( - K::IndexOutOfBounds, - "Column indices are out of bounds."), - PatternDuplicateEntry => E::from_kind_and_msg( - K::DuplicateEntry, - "Matrix data contains duplicate entries."), + "Column indices are not monotonically increasing (sorted) within each row.", + ), + MinorIndexOutOfBounds => { + E::from_kind_and_msg(K::IndexOutOfBounds, "Column indices are out of bounds.") + } + PatternDuplicateEntry => { + E::from_kind_and_msg(K::DuplicateEntry, "Matrix data contains duplicate entries.") + } } } @@ -536,7 +551,7 @@ fn pattern_format_error_to_csr_error(err: SparsityPatternFormatError) -> SparseF #[derive(Debug)] pub struct CsrTripletIter<'a, T> { pattern_iter: SparsityPatternIter<'a>, - values_iter: Iter<'a, T> + values_iter: Iter<'a, T>, } impl<'a, T: Clone> CsrTripletIter<'a, T> { @@ -545,7 +560,7 @@ impl<'a, T: Clone> CsrTripletIter<'a, T> { /// The triplet iterator returns references to the values. This method adapts the iterator /// so that the values are cloned. #[inline] - pub fn cloned_values(self) -> impl 'a + Iterator { + pub fn cloned_values(self) -> impl 'a + Iterator { self.map(|(i, j, v)| (i, j, v.clone())) } } @@ -559,7 +574,7 @@ impl<'a, T> Iterator for CsrTripletIter<'a, T> { match (next_entry, next_value) { (Some((i, j)), Some(v)) => Some((i, j, v)), - _ => None + _ => None, } } } @@ -568,7 +583,7 @@ impl<'a, T> Iterator for CsrTripletIter<'a, T> { #[derive(Debug)] pub struct CsrTripletIterMut<'a, T> { pattern_iter: SparsityPatternIter<'a>, - values_mut_iter: IterMut<'a, T> + values_mut_iter: IterMut<'a, T>, } impl<'a, T> Iterator for CsrTripletIterMut<'a, T> { @@ -581,7 +596,7 @@ impl<'a, T> Iterator for CsrTripletIterMut<'a, T> { match (next_entry, next_value) { (Some((i, j)), Some(v)) => Some((i, j, v)), - _ => None + _ => None, } } } @@ -589,7 +604,7 @@ impl<'a, T> Iterator for CsrTripletIterMut<'a, T> { /// An immutable representation of a row in a CSR matrix. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CsrRow<'a, T> { - lane: CsLane<'a, T> + lane: CsLane<'a, T>, } /// A mutable representation of a row in a CSR matrix. @@ -598,7 +613,7 @@ pub struct CsrRow<'a, T> { /// to the row cannot be modified. #[derive(Debug, PartialEq, Eq)] pub struct CsrRowMut<'a, T> { - lane: CsLaneMut<'a, T> + lane: CsLaneMut<'a, T>, } /// Implement the methods common to both CsrRow and CsrRowMut @@ -638,7 +653,7 @@ macro_rules! impl_csr_row_common_methods { self.lane.get_entry(global_col_index) } } - } + }; } impl_csr_row_common_methods!(CsrRow<'a, T>); @@ -670,33 +685,29 @@ impl<'a, T> CsrRowMut<'a, T> { /// Row iterator for [CsrMatrix](struct.CsrMatrix.html). pub struct CsrRowIter<'a, T> { - lane_iter: CsLaneIter<'a, T> + lane_iter: CsLaneIter<'a, T>, } impl<'a, T> Iterator for CsrRowIter<'a, T> { type Item = CsrRow<'a, T>; fn next(&mut self) -> Option { - self.lane_iter - .next() - .map(|lane| CsrRow { lane }) + self.lane_iter.next().map(|lane| CsrRow { lane }) } } /// Mutable row iterator for [CsrMatrix](struct.CsrMatrix.html). pub struct CsrRowIterMut<'a, T> { - lane_iter: CsLaneIterMut<'a, T> + lane_iter: CsLaneIterMut<'a, T>, } impl<'a, T> Iterator for CsrRowIterMut<'a, T> where - T: 'a + T: 'a, { type Item = CsrRowMut<'a, T>; fn next(&mut self) -> Option { - self.lane_iter - .next() - .map(|lane| CsrRowMut { lane }) + self.lane_iter.next().map(|lane| CsrRowMut { lane }) } -} \ No newline at end of file +} diff --git a/nalgebra-sparse/src/factorization/cholesky.rs b/nalgebra-sparse/src/factorization/cholesky.rs index 65a3c02f..a18761c9 100644 --- a/nalgebra-sparse/src/factorization/cholesky.rs +++ b/nalgebra-sparse/src/factorization/cholesky.rs @@ -1,10 +1,10 @@ -use crate::pattern::SparsityPattern; use crate::csc::CscMatrix; -use core::{mem, iter}; -use nalgebra::{Scalar, RealField, DMatrixSlice, DMatrixSliceMut, DMatrix}; -use std::fmt::{Display, Formatter}; use crate::ops::serial::spsolve_csc_lower_triangular; use crate::ops::Op; +use crate::pattern::SparsityPattern; +use core::{iter, mem}; +use nalgebra::{DMatrix, DMatrixSlice, DMatrixSliceMut, RealField, Scalar}; +use std::fmt::{Display, Formatter}; /// A symbolic sparse Cholesky factorization of a CSC matrix. /// @@ -15,7 +15,7 @@ pub struct CscSymbolicCholesky { m_pattern: SparsityPattern, l_pattern: SparsityPattern, // u in this context is L^T, so that M = L L^T - u_pattern: SparsityPattern + u_pattern: SparsityPattern, } impl CscSymbolicCholesky { @@ -28,8 +28,11 @@ impl CscSymbolicCholesky { /// /// Panics if the sparsity pattern is not square. pub fn factor(pattern: SparsityPattern) -> Self { - assert_eq!(pattern.major_dim(), pattern.minor_dim(), - "Major and minor dimensions must be the same (square matrix)."); + assert_eq!( + pattern.major_dim(), + pattern.minor_dim(), + "Major and minor dimensions must be the same (square matrix)." + ); let (l_pattern, u_pattern) = nonzero_pattern(&pattern); Self { m_pattern: pattern, @@ -65,7 +68,7 @@ pub struct CscCholesky { l_factor: CscMatrix, u_pattern: SparsityPattern, work_x: Vec, - work_c: Vec + work_c: Vec, } #[derive(Debug, PartialEq, Eq, Clone)] @@ -100,16 +103,20 @@ impl CscCholesky { /// /// Panics if the number of values differ from the number of non-zeros of the sparsity pattern /// of the matrix that was symbolically factored. - pub fn factor_numerical(symbolic: CscSymbolicCholesky, values: &[T]) - -> Result - { - assert_eq!(symbolic.l_pattern.nnz(), symbolic.u_pattern.nnz(), - "u is just the transpose of l, so should have the same nnz"); + pub fn factor_numerical( + symbolic: CscSymbolicCholesky, + values: &[T], + ) -> Result { + assert_eq!( + symbolic.l_pattern.nnz(), + symbolic.u_pattern.nnz(), + "u is just the transpose of l, so should have the same nnz" + ); let l_nnz = symbolic.l_pattern.nnz(); let l_values = vec![T::zero(); l_nnz]; - let l_factor = CscMatrix::try_from_pattern_and_values(symbolic.l_pattern, l_values) - .unwrap(); + let l_factor = + CscMatrix::try_from_pattern_and_values(symbolic.l_pattern, l_values).unwrap(); let (nrows, ncols) = (l_factor.nrows(), l_factor.ncols()); @@ -169,7 +176,7 @@ impl CscCholesky { } /// Returns the Cholesky factor `L`. - pub fn take_l(self) -> CscMatrix { + pub fn take_l(self) -> CscMatrix { self.l_factor } @@ -229,11 +236,9 @@ impl CscCholesky { { let (offsets, _, values) = self.l_factor.csc_data_mut(); - *values - .get_unchecked_mut(*offsets.get_unchecked(k)) = denom; + *values.get_unchecked_mut(*offsets.get_unchecked(k)) = denom; } - let mut col_k = self.l_factor.col_mut(k); let (col_k_rows, col_k_values) = col_k.rows_and_values_mut(); let col_k_entries = col_k_rows.iter().zip(col_k_values); @@ -269,19 +274,16 @@ impl CscCholesky { /// # Panics /// /// Panics if `b` is not square. - pub fn solve_mut<'a>(&'a self, b: impl Into>) - { + pub fn solve_mut<'a>(&'a self, b: impl Into>) { let expect_msg = "If the Cholesky factorization succeeded,\ then the triangular solve should never fail"; // Solve LY = B let mut y = b.into(); - spsolve_csc_lower_triangular(Op::NoOp(self.l()), &mut y) - .expect(expect_msg); + spsolve_csc_lower_triangular(Op::NoOp(self.l()), &mut y).expect(expect_msg); // Solve L^T X = Y let mut x = y; - spsolve_csc_lower_triangular(Op::Transpose(self.l()), &mut x) - .expect(expect_msg); + spsolve_csc_lower_triangular(Op::Transpose(self.l()), &mut x).expect(expect_msg); } } @@ -333,8 +335,8 @@ fn nonzero_pattern(m: &SparsityPattern) -> (SparsityPattern, SparsityPattern) { col_offsets.push(rows.len()); } - let u_pattern = SparsityPattern::try_from_offsets_and_indices(nrows, ncols, col_offsets, rows) - .unwrap(); + let u_pattern = + SparsityPattern::try_from_offsets_and_indices(nrows, ncols, col_offsets, rows).unwrap(); // TODO: Avoid this transpose? let l_pattern = u_pattern.transpose(); @@ -368,4 +370,4 @@ fn elimination_tree(pattern: &SparsityPattern) -> Vec { } forest -} \ No newline at end of file +} diff --git a/nalgebra-sparse/src/factorization/mod.rs b/nalgebra-sparse/src/factorization/mod.rs index b4adcba2..b77a857c 100644 --- a/nalgebra-sparse/src/factorization/mod.rs +++ b/nalgebra-sparse/src/factorization/mod.rs @@ -3,4 +3,4 @@ //! Currently, the only factorization provided here is the [`CscCholesky`] factorization. mod cholesky; -pub use cholesky::*; \ No newline at end of file +pub use cholesky::*; diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index 3436c147..04b93091 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -135,13 +135,13 @@ #![deny(unused_results)] #![deny(missing_docs)] +pub mod convert; pub mod coo; pub mod csc; pub mod csr; -pub mod pattern; -pub mod ops; -pub mod convert; pub mod factorization; +pub mod ops; +pub mod pattern; pub(crate) mod cs; @@ -151,16 +151,16 @@ pub mod proptest; #[cfg(feature = "compare")] mod matrixcompare; +use num_traits::Zero; use std::error::Error; use std::fmt; -use num_traits::Zero; /// Errors produced by functions that expect well-formed sparse format data. #[derive(Debug)] pub struct SparseFormatError { kind: SparseFormatErrorKind, // Currently we only use an underlying error for generating the `Display` impl - error: Box + error: Box, } impl SparseFormatError { @@ -170,10 +170,7 @@ impl SparseFormatError { } pub(crate) fn from_kind_and_error(kind: SparseFormatErrorKind, error: Box) -> Self { - Self { - kind, - error - } + Self { kind, error } } /// Helper functionality for more conveniently creating errors. @@ -221,7 +218,7 @@ pub enum SparseEntry<'a, T> { /// is explicitly stored (a so-called "explicit zero"). NonZero(&'a T), /// The entry is implicitly zero, i.e. it is not explicitly stored. - Zero + Zero, } impl<'a, T: Clone + Zero> SparseEntry<'a, T> { @@ -232,7 +229,7 @@ impl<'a, T: Clone + Zero> SparseEntry<'a, T> { pub fn to_value(self) -> T { match self { SparseEntry::NonZero(value) => value.clone(), - SparseEntry::Zero => T::zero() + SparseEntry::Zero => T::zero(), } } } @@ -248,7 +245,7 @@ pub enum SparseEntryMut<'a, T> { /// is explicitly stored (a so-called "explicit zero"). NonZero(&'a mut T), /// The entry is implicitly zero i.e. it is not explicitly stored. - Zero + Zero, } impl<'a, T: Clone + Zero> SparseEntryMut<'a, T> { @@ -259,7 +256,7 @@ impl<'a, T: Clone + Zero> SparseEntryMut<'a, T> { pub fn to_value(self) -> T { match self { SparseEntryMut::NonZero(value) => value.clone(), - SparseEntryMut::Zero => T::zero() + SparseEntryMut::Zero => T::zero(), } } -} \ No newline at end of file +} diff --git a/nalgebra-sparse/src/matrixcompare.rs b/nalgebra-sparse/src/matrixcompare.rs index a4aafe3c..9c48ae40 100644 --- a/nalgebra-sparse/src/matrixcompare.rs +++ b/nalgebra-sparse/src/matrixcompare.rs @@ -1,36 +1,38 @@ //! Implements core traits for use with `matrixcompare`. -use crate::csr::CsrMatrix; +use crate::coo::CooMatrix; use crate::csc::CscMatrix; +use crate::csr::CsrMatrix; use matrixcompare_core; use matrixcompare_core::{Access, SparseAccess}; -use crate::coo::CooMatrix; macro_rules! impl_matrix_for_csr_csc { ($MatrixType:ident) => { - impl SparseAccess for $MatrixType { - fn nnz(&self) -> usize { - $MatrixType::nnz(self) + impl SparseAccess for $MatrixType { + fn nnz(&self) -> usize { + $MatrixType::nnz(self) + } + + fn fetch_triplets(&self) -> Vec<(usize, usize, T)> { + self.triplet_iter() + .map(|(i, j, v)| (i, j, v.clone())) + .collect() + } } - fn fetch_triplets(&self) -> Vec<(usize, usize, T)> { - self.triplet_iter().map(|(i, j, v)| (i, j, v.clone())).collect() - } - } + impl matrixcompare_core::Matrix for $MatrixType { + fn rows(&self) -> usize { + self.nrows() + } - impl matrixcompare_core::Matrix for $MatrixType { - fn rows(&self) -> usize { - self.nrows() - } + fn cols(&self) -> usize { + self.ncols() + } - fn cols(&self) -> usize { - self.ncols() + fn access(&self) -> Access { + Access::Sparse(self) + } } - - fn access(&self) -> Access { - Access::Sparse(self) - } - } - } + }; } impl_matrix_for_csr_csc!(CsrMatrix); @@ -42,7 +44,9 @@ impl SparseAccess for CooMatrix { } fn fetch_triplets(&self) -> Vec<(usize, usize, T)> { - self.triplet_iter().map(|(i, j, v)| (i, j, v.clone())).collect() + self.triplet_iter() + .map(|(i, j, v)| (i, j, v.clone())) + .collect() } } @@ -58,4 +62,4 @@ impl matrixcompare_core::Matrix for CooMatrix { fn access(&self) -> Access { Access::Sparse(self) } -} \ No newline at end of file +} diff --git a/nalgebra-sparse/src/ops/impl_std_ops.rs b/nalgebra-sparse/src/ops/impl_std_ops.rs index d62519e9..75645bed 100644 --- a/nalgebra-sparse/src/ops/impl_std_ops.rs +++ b/nalgebra-sparse/src/ops/impl_std_ops.rs @@ -1,15 +1,20 @@ -use crate::csr::CsrMatrix; use crate::csc::CscMatrix; +use crate::csr::CsrMatrix; -use std::ops::{Add, Div, DivAssign, Mul, MulAssign, Sub, Neg}; -use crate::ops::serial::{spadd_csr_prealloc, spadd_csc_prealloc, spadd_pattern, spmm_csr_pattern, spmm_csr_prealloc, spmm_csc_prealloc, spmm_csc_dense, spmm_csr_dense, spmm_csc_pattern}; -use nalgebra::{ClosedAdd, ClosedMul, ClosedSub, ClosedDiv, Scalar, Matrix, MatrixMN, Dim, - Dynamic, DefaultAllocator, U1}; -use nalgebra::allocator::{Allocator}; -use nalgebra::constraint::{DimEq, ShapeConstraint}; -use num_traits::{Zero, One}; -use crate::ops::{Op}; +use crate::ops::serial::{ + spadd_csc_prealloc, spadd_csr_prealloc, spadd_pattern, spmm_csc_dense, spmm_csc_pattern, + spmm_csc_prealloc, spmm_csr_dense, spmm_csr_pattern, spmm_csr_prealloc, +}; +use crate::ops::Op; +use nalgebra::allocator::Allocator; use nalgebra::base::storage::Storage; +use nalgebra::constraint::{DimEq, ShapeConstraint}; +use nalgebra::{ + ClosedAdd, ClosedDiv, ClosedMul, ClosedSub, DefaultAllocator, Dim, Dynamic, Matrix, MatrixMN, + Scalar, U1, +}; +use num_traits::{One, Zero}; +use std::ops::{Add, Div, DivAssign, Mul, MulAssign, Neg, Sub}; /// Helper macro for implementing binary operators for different matrix types /// See below for usage. @@ -188,7 +193,7 @@ macro_rules! impl_neg { ($matrix_type:ident) => { impl Neg for $matrix_type where - T: Scalar + Neg + T: Scalar + Neg, { type Output = $matrix_type; @@ -202,7 +207,7 @@ macro_rules! impl_neg { impl<'a, T> Neg for &'a $matrix_type where - T: Scalar + Neg + T: Scalar + Neg, { type Output = $matrix_type; @@ -211,10 +216,10 @@ macro_rules! impl_neg { // obtain both the sparsity pattern and values from the matrix, // and then modify the values before creating a new matrix from the pattern // and negated values. - - self.clone() + -self.clone() } } - } + }; } impl_neg!(CsrMatrix); @@ -323,4 +328,4 @@ macro_rules! impl_spmm_cs_dense { } impl_spmm_cs_dense!(CsrMatrix, spmm_csr_dense); -impl_spmm_cs_dense!(CscMatrix, spmm_csc_dense); \ No newline at end of file +impl_spmm_cs_dense!(CscMatrix, spmm_csc_dense); diff --git a/nalgebra-sparse/src/ops/mod.rs b/nalgebra-sparse/src/ops/mod.rs index 4b51d2ee..a6a21fbc 100644 --- a/nalgebra-sparse/src/ops/mod.rs +++ b/nalgebra-sparse/src/ops/mod.rs @@ -148,13 +148,14 @@ impl Op { pub fn as_ref(&self) -> Op<&T> { match self { Op::NoOp(obj) => Op::NoOp(&obj), - Op::Transpose(obj) => Op::Transpose(&obj) + Op::Transpose(obj) => Op::Transpose(&obj), } } /// Converts the underlying data type. pub fn convert(self) -> Op - where T: Into + where + T: Into, { self.map_same_op(T::into) } @@ -163,7 +164,7 @@ impl Op { pub fn map_same_op U>(self, f: F) -> Op { match self { Op::NoOp(obj) => Op::NoOp(f(obj)), - Op::Transpose(obj) => Op::Transpose(f(obj)) + Op::Transpose(obj) => Op::Transpose(f(obj)), } } @@ -181,7 +182,7 @@ impl Op { pub fn transposed(self) -> Self { match self { Op::NoOp(obj) => Op::Transpose(obj), - Op::Transpose(obj) => Op::NoOp(obj) + Op::Transpose(obj) => Op::NoOp(obj), } } } @@ -191,4 +192,3 @@ impl From for Op { Self::NoOp(obj) } } - diff --git a/nalgebra-sparse/src/ops/serial/cs.rs b/nalgebra-sparse/src/ops/serial/cs.rs index dd73c304..66b0ad76 100644 --- a/nalgebra-sparse/src/ops/serial/cs.rs +++ b/nalgebra-sparse/src/ops/serial/cs.rs @@ -1,14 +1,15 @@ use crate::cs::CsMatrix; +use crate::ops::serial::{OperationError, OperationErrorKind}; use crate::ops::Op; -use crate::ops::serial::{OperationErrorKind, OperationError}; -use nalgebra::{Scalar, ClosedAdd, ClosedMul, DMatrixSliceMut, DMatrixSlice}; -use num_traits::{Zero, One}; use crate::SparseEntryMut; +use nalgebra::{ClosedAdd, ClosedMul, DMatrixSlice, DMatrixSliceMut, Scalar}; +use num_traits::{One, Zero}; fn spmm_cs_unexpected_entry() -> OperationError { OperationError::from_kind_and_message( OperationErrorKind::InvalidPattern, - String::from("Found unexpected entry that is not present in `c`.")) + String::from("Found unexpected entry that is not present in `c`."), + ) } /// Helper functionality for implementing CSR/CSC SPMM. @@ -24,12 +25,12 @@ pub fn spmm_cs_prealloc( c: &mut CsMatrix, alpha: T, a: &CsMatrix, - b: &CsMatrix) - -> Result<(), OperationError> - where - T: Scalar + ClosedAdd + ClosedMul + Zero + One + b: &CsMatrix, +) -> Result<(), OperationError> +where + T: Scalar + ClosedAdd + ClosedMul + Zero + One, { - for i in 0 .. c.pattern().major_dim() { + for i in 0..c.pattern().major_dim() { let a_lane_i = a.get_lane(i).unwrap(); let mut c_lane_i = c.get_lane_mut(i).unwrap(); for c_ij in c_lane_i.values_mut() { @@ -42,14 +43,15 @@ pub fn spmm_cs_prealloc( let alpha_aik = alpha.inlined_clone() * a_ik.inlined_clone(); for (j, b_kj) in b_lane_k.minor_indices().iter().zip(b_lane_k.values()) { // Determine the location in C to append the value - let (c_local_idx, _) = c_lane_i_cols.iter() + let (c_local_idx, _) = c_lane_i_cols + .iter() .enumerate() .find(|(_, c_col)| *c_col == j) .ok_or_else(spmm_cs_unexpected_entry)?; c_lane_i_values[c_local_idx] += alpha_aik.inlined_clone() * b_kj.inlined_clone(); - c_lane_i_cols = &c_lane_i_cols[c_local_idx ..]; - c_lane_i_values = &mut c_lane_i_values[c_local_idx ..]; + c_lane_i_cols = &c_lane_i_cols[c_local_idx..]; + c_lane_i_values = &mut c_lane_i_values[c_local_idx..]; } } } @@ -60,17 +62,19 @@ pub fn spmm_cs_prealloc( fn spadd_cs_unexpected_entry() -> OperationError { OperationError::from_kind_and_message( OperationErrorKind::InvalidPattern, - String::from("Found entry in `op(a)` that is not present in `c`.")) + String::from("Found entry in `op(a)` that is not present in `c`."), + ) } /// Helper functionality for implementing CSR/CSC SPADD. -pub fn spadd_cs_prealloc(beta: T, - c: &mut CsMatrix, - alpha: T, - a: Op<&CsMatrix>) - -> Result<(), OperationError> - where - T: Scalar + ClosedAdd + ClosedMul + Zero + One +pub fn spadd_cs_prealloc( + beta: T, + c: &mut CsMatrix, + alpha: T, + a: Op<&CsMatrix>, +) -> Result<(), OperationError> +where + T: Scalar + ClosedAdd + ClosedMul + Zero + One, { match a { Op::NoOp(a) => { @@ -88,13 +92,14 @@ pub fn spadd_cs_prealloc(beta: T, // TODO: Use exponential search instead of linear search. // If C has substantially more entries in the row than A, then a line search // will needlessly visit many entries in C. - let (c_idx, _) = c_minors.iter() + let (c_idx, _) = c_minors + .iter() .enumerate() .find(|(_, c_col)| *c_col == a_col) .ok_or_else(spadd_cs_unexpected_entry)?; c_vals[c_idx] += alpha.inlined_clone() * a_val.inlined_clone(); - c_minors = &c_minors[c_idx ..]; - c_vals = &mut c_vals[c_idx ..]; + c_minors = &c_minors[c_idx..]; + c_vals = &mut c_vals[c_idx..]; } } } @@ -110,7 +115,7 @@ pub fn spadd_cs_prealloc(beta: T, let a_val = a_val.inlined_clone(); let alpha = alpha.inlined_clone(); match c.get_entry_mut(j, i).unwrap() { - SparseEntryMut::NonZero(c_ji) => { *c_ji += alpha * a_val } + SparseEntryMut::NonZero(c_ji) => *c_ji += alpha * a_val, SparseEntryMut::Zero => return Err(spadd_cs_unexpected_entry()), } } @@ -124,13 +129,14 @@ pub fn spadd_cs_prealloc(beta: T, /// /// The implementation essentially assumes that `a` is a CSR matrix. To use it with CSC matrices, /// the transposed operation must be specified for the CSC matrix. -pub fn spmm_cs_dense(beta: T, - mut c: DMatrixSliceMut, - alpha: T, - a: Op<&CsMatrix>, - b: Op>) - where - T: Scalar + ClosedAdd + ClosedMul + Zero + One +pub fn spmm_cs_dense( + beta: T, + mut c: DMatrixSliceMut, + alpha: T, + a: Op<&CsMatrix>, + b: Op>, +) where + T: Scalar + ClosedAdd + ClosedMul + Zero + One, { match a { Op::NoOp(a) => { @@ -139,17 +145,17 @@ pub fn spmm_cs_dense(beta: T, for (c_ij, a_row_i) in c_col_j.iter_mut().zip(a.lane_iter()) { let mut dot_ij = T::zero(); for (&k, a_ik) in a_row_i.minor_indices().iter().zip(a_row_i.values()) { - let b_contrib = - match b { - Op::NoOp(ref b) => b.index((k, j)), - Op::Transpose(ref b) => b.index((j, k)) - }; + let b_contrib = match b { + Op::NoOp(ref b) => b.index((k, j)), + Op::Transpose(ref b) => b.index((j, k)), + }; dot_ij += a_ik.inlined_clone() * b_contrib.inlined_clone(); } - *c_ij = beta.inlined_clone() * c_ij.inlined_clone() + alpha.inlined_clone() * dot_ij; + *c_ij = beta.inlined_clone() * c_ij.inlined_clone() + + alpha.inlined_clone() * dot_ij; } } - }, + } Op::Transpose(a) => { // In this case, we have to pre-multiply C by beta c *= beta; @@ -165,17 +171,16 @@ pub fn spmm_cs_dense(beta: T, for (c_ij, b_kj) in c_row_i.iter_mut().zip(b_row_k.iter()) { *c_ij += gamma_ki.inlined_clone() * b_kj.inlined_clone(); } - }, + } Op::Transpose(ref b) => { let b_col_k = b.column(k); for (c_ij, b_jk) in c_row_i.iter_mut().zip(b_col_k.iter()) { *c_ij += gamma_ki.inlined_clone() * b_jk.inlined_clone(); } - }, + } } } } - }, + } } } - diff --git a/nalgebra-sparse/src/ops/serial/csc.rs b/nalgebra-sparse/src/ops/serial/csc.rs index bcfb8108..95350d91 100644 --- a/nalgebra-sparse/src/ops/serial/csc.rs +++ b/nalgebra-sparse/src/ops/serial/csc.rs @@ -1,9 +1,9 @@ use crate::csc::CscMatrix; -use crate::ops::Op; -use crate::ops::serial::cs::{spmm_cs_prealloc, spmm_cs_dense, spadd_cs_prealloc}; +use crate::ops::serial::cs::{spadd_cs_prealloc, spmm_cs_dense, spmm_cs_prealloc}; use crate::ops::serial::{OperationError, OperationErrorKind}; -use nalgebra::{Scalar, ClosedAdd, ClosedMul, DMatrixSliceMut, DMatrixSlice, RealField}; -use num_traits::{Zero, One}; +use crate::ops::Op; +use nalgebra::{ClosedAdd, ClosedMul, DMatrixSlice, DMatrixSliceMut, RealField, Scalar}; +use num_traits::{One, Zero}; use std::borrow::Cow; @@ -12,25 +12,27 @@ use std::borrow::Cow; /// # Panics /// /// Panics if the dimensions of the matrices involved are not compatible with the expression. -pub fn spmm_csc_dense<'a, T>(beta: T, - c: impl Into>, - alpha: T, - a: Op<&CscMatrix>, - b: Op>>) - where - T: Scalar + ClosedAdd + ClosedMul + Zero + One +pub fn spmm_csc_dense<'a, T>( + beta: T, + c: impl Into>, + alpha: T, + a: Op<&CscMatrix>, + b: Op>>, +) where + T: Scalar + ClosedAdd + ClosedMul + Zero + One, { let b = b.convert(); spmm_csc_dense_(beta, c.into(), alpha, a, b) } -fn spmm_csc_dense_(beta: T, - c: DMatrixSliceMut, - alpha: T, - a: Op<&CscMatrix>, - b: Op>) - where - T: Scalar + ClosedAdd + ClosedMul + Zero + One +fn spmm_csc_dense_( + beta: T, + c: DMatrixSliceMut, + alpha: T, + a: Op<&CscMatrix>, + b: Op>, +) where + T: Scalar + ClosedAdd + ClosedMul + Zero + One, { assert_compatible_spmm_dims!(c, a, b); // Need to interpret matrix as transposed since the spmm_cs_dense function assumes CSR layout @@ -46,19 +48,19 @@ fn spmm_csc_dense_(beta: T, /// # Panics /// /// Panics if the dimensions of the matrices involved are not compatible with the expression. -pub fn spadd_csc_prealloc(beta: T, - c: &mut CscMatrix, - alpha: T, - a: Op<&CscMatrix>) - -> Result<(), OperationError> - where - T: Scalar + ClosedAdd + ClosedMul + Zero + One +pub fn spadd_csc_prealloc( + beta: T, + c: &mut CscMatrix, + alpha: T, + a: Op<&CscMatrix>, +) -> Result<(), OperationError> +where + T: Scalar + ClosedAdd + ClosedMul + Zero + One, { assert_compatible_spadd_dims!(c, a); spadd_cs_prealloc(beta, &mut c.cs, alpha, a.map_same_op(|a| &a.cs)) } - /// Sparse-sparse matrix multiplication, `C <- beta * C + alpha * op(A) * op(B)`. /// /// # Errors @@ -74,10 +76,10 @@ pub fn spmm_csc_prealloc( c: &mut CscMatrix, alpha: T, a: Op<&CscMatrix>, - b: Op<&CscMatrix>) - -> Result<(), OperationError> - where - T: Scalar + ClosedAdd + ClosedMul + Zero + One + b: Op<&CscMatrix>, +) -> Result<(), OperationError> +where + T: Scalar + ClosedAdd + ClosedMul + Zero + One, { assert_compatible_spmm_dims!(c, a, b); @@ -87,7 +89,7 @@ pub fn spmm_csc_prealloc( (NoOp(ref a), NoOp(ref b)) => { // Note: We have to reverse the order for CSC matrices spmm_cs_prealloc(beta, &mut c.cs, alpha, &b.cs, &a.cs) - }, + } _ => { // Currently we handle transposition by explicitly precomputing transposed matrices // and calling the operation again without transposition @@ -99,7 +101,9 @@ pub fn spmm_csc_prealloc( (NoOp(_), NoOp(_)) => unreachable!(), (Transpose(ref a), NoOp(_)) => (Owned(a.transpose()), Borrowed(b_ref)), (NoOp(_), Transpose(ref b)) => (Borrowed(a_ref), Owned(b.transpose())), - (Transpose(ref a), Transpose(ref b)) => (Owned(a.transpose()), Owned(b.transpose())) + (Transpose(ref a), Transpose(ref b)) => { + (Owned(a.transpose()), Owned(b.transpose())) + } } }; @@ -121,13 +125,20 @@ pub fn spmm_csc_prealloc( /// Panics if `L` is not square, or if `L` and `B` are not dimensionally compatible. pub fn spsolve_csc_lower_triangular<'a, T: RealField>( l: Op<&CscMatrix>, - b: impl Into>) - -> Result<(), OperationError> -{ + b: impl Into>, +) -> Result<(), OperationError> { let b = b.into(); let l_matrix = l.into_inner(); - assert_eq!(l_matrix.nrows(), l_matrix.ncols(), "Matrix must be square for triangular solve."); - assert_eq!(l_matrix.nrows(), b.nrows(), "Dimension mismatch in sparse lower triangular solver."); + assert_eq!( + l_matrix.nrows(), + l_matrix.ncols(), + "Matrix must be square for triangular solve." + ); + assert_eq!( + l_matrix.nrows(), + b.nrows(), + "Dimension mismatch in sparse lower triangular solver." + ); match l { Op::NoOp(a) => spsolve_csc_lower_triangular_no_transpose(a, b), Op::Transpose(a) => spsolve_csc_lower_triangular_transpose(a, b), @@ -136,16 +147,15 @@ pub fn spsolve_csc_lower_triangular<'a, T: RealField>( fn spsolve_csc_lower_triangular_no_transpose( l: &CscMatrix, - b: DMatrixSliceMut) - -> Result<(), OperationError> -{ + b: DMatrixSliceMut, +) -> Result<(), OperationError> { let mut x = b; // Solve column-by-column - for j in 0 .. x.ncols() { + for j in 0..x.ncols() { let mut x_col_j = x.column_mut(j); - for k in 0 .. l.ncols() { + for k in 0..l.ncols() { let l_col_k = l.col(k); // Skip entries above the diagonal @@ -163,8 +173,8 @@ fn spsolve_csc_lower_triangular_no_transpose( // Copy value after updating (so we don't run into the borrow checker) let x_kj = x_col_j[k]; - let row_indices = &l_col_k.row_indices()[(diag_csc_index + 1) ..]; - let l_values = &l_col_k.values()[(diag_csc_index + 1) ..]; + let row_indices = &l_col_k.row_indices()[(diag_csc_index + 1)..]; + let l_values = &l_col_k.values()[(diag_csc_index + 1)..]; // Note: The remaining entries are below the diagonal for (&i, l_ik) in row_indices.iter().zip(l_values) { @@ -187,24 +197,26 @@ fn spsolve_csc_lower_triangular_no_transpose( fn spsolve_encountered_zero_diagonal() -> Result<(), OperationError> { let message = "Matrix contains at least one diagonal entry that is zero."; - Err(OperationError::from_kind_and_message(OperationErrorKind::Singular, String::from(message))) + Err(OperationError::from_kind_and_message( + OperationErrorKind::Singular, + String::from(message), + )) } fn spsolve_csc_lower_triangular_transpose( l: &CscMatrix, - b: DMatrixSliceMut) - -> Result<(), OperationError> -{ + b: DMatrixSliceMut, +) -> Result<(), OperationError> { let mut x = b; // Solve column-by-column - for j in 0 .. x.ncols() { + for j in 0..x.ncols() { let mut x_col_j = x.column_mut(j); // Due to the transposition, we're essentially solving an upper triangular system, // and the columns in our matrix become rows - for i in (0 .. l.ncols()).rev() { + for i in (0..l.ncols()).rev() { let l_col_i = l.col(i); // Skip entries above the diagonal @@ -220,8 +232,8 @@ fn spsolve_csc_lower_triangular_transpose( // Copy value after updating (so we don't run into the borrow checker) let mut x_ii = x_col_j[i]; - let row_indices = &l_col_i.row_indices()[(diag_csc_index + 1) ..]; - let a_values = &l_col_i.values()[(diag_csc_index + 1) ..]; + let row_indices = &l_col_i.row_indices()[(diag_csc_index + 1)..]; + let a_values = &l_col_i.values()[(diag_csc_index + 1)..]; // Note: The remaining entries are below the diagonal for (&k, &l_ki) in row_indices.iter().zip(a_values) { @@ -240,4 +252,4 @@ fn spsolve_csc_lower_triangular_transpose( } Ok(()) -} \ No newline at end of file +} diff --git a/nalgebra-sparse/src/ops/serial/csr.rs b/nalgebra-sparse/src/ops/serial/csr.rs index fc632a35..f6fcc62a 100644 --- a/nalgebra-sparse/src/ops/serial/csr.rs +++ b/nalgebra-sparse/src/ops/serial/csr.rs @@ -1,31 +1,33 @@ use crate::csr::CsrMatrix; -use crate::ops::{Op}; -use crate::ops::serial::{OperationError}; -use nalgebra::{Scalar, DMatrixSlice, ClosedAdd, ClosedMul, DMatrixSliceMut}; -use num_traits::{Zero, One}; +use crate::ops::serial::cs::{spadd_cs_prealloc, spmm_cs_dense, spmm_cs_prealloc}; +use crate::ops::serial::OperationError; +use crate::ops::Op; +use nalgebra::{ClosedAdd, ClosedMul, DMatrixSlice, DMatrixSliceMut, Scalar}; +use num_traits::{One, Zero}; use std::borrow::Cow; -use crate::ops::serial::cs::{spmm_cs_prealloc, spmm_cs_dense, spadd_cs_prealloc}; /// Sparse-dense matrix-matrix multiplication `C <- beta * C + alpha * op(A) * op(B)`. -pub fn spmm_csr_dense<'a, T>(beta: T, - c: impl Into>, - alpha: T, - a: Op<&CsrMatrix>, - b: Op>>) - where - T: Scalar + ClosedAdd + ClosedMul + Zero + One +pub fn spmm_csr_dense<'a, T>( + beta: T, + c: impl Into>, + alpha: T, + a: Op<&CsrMatrix>, + b: Op>>, +) where + T: Scalar + ClosedAdd + ClosedMul + Zero + One, { let b = b.convert(); spmm_csr_dense_(beta, c.into(), alpha, a, b) } -fn spmm_csr_dense_(beta: T, - c: DMatrixSliceMut, - alpha: T, - a: Op<&CsrMatrix>, - b: Op>) -where - T: Scalar + ClosedAdd + ClosedMul + Zero + One +fn spmm_csr_dense_( + beta: T, + c: DMatrixSliceMut, + alpha: T, + a: Op<&CsrMatrix>, + b: Op>, +) where + T: Scalar + ClosedAdd + ClosedMul + Zero + One, { assert_compatible_spmm_dims!(c, a, b); spmm_cs_dense(beta, c, alpha, a.map_same_op(|a| &a.cs), b) @@ -41,13 +43,14 @@ where /// # Panics /// /// Panics if the dimensions of the matrices involved are not compatible with the expression. -pub fn spadd_csr_prealloc(beta: T, - c: &mut CsrMatrix, - alpha: T, - a: Op<&CsrMatrix>) - -> Result<(), OperationError> +pub fn spadd_csr_prealloc( + beta: T, + c: &mut CsrMatrix, + alpha: T, + a: Op<&CsrMatrix>, +) -> Result<(), OperationError> where - T: Scalar + ClosedAdd + ClosedMul + Zero + One + T: Scalar + ClosedAdd + ClosedMul + Zero + One, { assert_compatible_spadd_dims!(c, a); spadd_cs_prealloc(beta, &mut c.cs, alpha, a.map_same_op(|a| &a.cs)) @@ -67,19 +70,17 @@ pub fn spmm_csr_prealloc( c: &mut CsrMatrix, alpha: T, a: Op<&CsrMatrix>, - b: Op<&CsrMatrix>) --> Result<(), OperationError> + b: Op<&CsrMatrix>, +) -> Result<(), OperationError> where - T: Scalar + ClosedAdd + ClosedMul + Zero + One + T: Scalar + ClosedAdd + ClosedMul + Zero + One, { assert_compatible_spmm_dims!(c, a, b); use Op::{NoOp, Transpose}; match (&a, &b) { - (NoOp(ref a), NoOp(ref b)) => { - spmm_cs_prealloc(beta, &mut c.cs, alpha, &a.cs, &b.cs) - }, + (NoOp(ref a), NoOp(ref b)) => spmm_cs_prealloc(beta, &mut c.cs, alpha, &a.cs, &b.cs), _ => { // Currently we handle transposition by explicitly precomputing transposed matrices // and calling the operation again without transposition @@ -93,7 +94,9 @@ where (NoOp(_), NoOp(_)) => unreachable!(), (Transpose(ref a), NoOp(_)) => (Owned(a.transpose()), Borrowed(b_ref)), (NoOp(_), Transpose(ref b)) => (Borrowed(a_ref), Owned(b.transpose())), - (Transpose(ref a), Transpose(ref b)) => (Owned(a.transpose()), Owned(b.transpose())) + (Transpose(ref a), Transpose(ref b)) => { + (Owned(a.transpose()), Owned(b.transpose())) + } } }; @@ -101,4 +104,3 @@ where } } } - diff --git a/nalgebra-sparse/src/ops/serial/mod.rs b/nalgebra-sparse/src/ops/serial/mod.rs index 1497bcb0..82285e86 100644 --- a/nalgebra-sparse/src/ops/serial/mod.rs +++ b/nalgebra-sparse/src/ops/serial/mod.rs @@ -10,33 +10,31 @@ #[macro_use] macro_rules! assert_compatible_spmm_dims { - ($c:expr, $a:expr, $b:expr) => { - { - use crate::ops::Op::{NoOp, Transpose}; - match (&$a, &$b) { - (NoOp(ref a), NoOp(ref b)) => { - assert_eq!($c.nrows(), a.nrows(), "C.nrows() != A.nrows()"); - assert_eq!($c.ncols(), b.ncols(), "C.ncols() != B.ncols()"); - assert_eq!(a.ncols(), b.nrows(), "A.ncols() != B.nrows()"); - }, - (Transpose(ref a), NoOp(ref b)) => { - assert_eq!($c.nrows(), a.ncols(), "C.nrows() != A.ncols()"); - assert_eq!($c.ncols(), b.ncols(), "C.ncols() != B.ncols()"); - assert_eq!(a.nrows(), b.nrows(), "A.nrows() != B.nrows()"); - }, - (NoOp(ref a), Transpose(ref b)) => { - assert_eq!($c.nrows(), a.nrows(), "C.nrows() != A.nrows()"); - assert_eq!($c.ncols(), b.nrows(), "C.ncols() != B.nrows()"); - assert_eq!(a.ncols(), b.ncols(), "A.ncols() != B.ncols()"); - }, - (Transpose(ref a), Transpose(ref b)) => { - assert_eq!($c.nrows(), a.ncols(), "C.nrows() != A.ncols()"); - assert_eq!($c.ncols(), b.nrows(), "C.ncols() != B.nrows()"); - assert_eq!(a.nrows(), b.ncols(), "A.nrows() != B.ncols()"); - } + ($c:expr, $a:expr, $b:expr) => {{ + use crate::ops::Op::{NoOp, Transpose}; + match (&$a, &$b) { + (NoOp(ref a), NoOp(ref b)) => { + assert_eq!($c.nrows(), a.nrows(), "C.nrows() != A.nrows()"); + assert_eq!($c.ncols(), b.ncols(), "C.ncols() != B.ncols()"); + assert_eq!(a.ncols(), b.nrows(), "A.ncols() != B.nrows()"); + } + (Transpose(ref a), NoOp(ref b)) => { + assert_eq!($c.nrows(), a.ncols(), "C.nrows() != A.ncols()"); + assert_eq!($c.ncols(), b.ncols(), "C.ncols() != B.ncols()"); + assert_eq!(a.nrows(), b.nrows(), "A.nrows() != B.nrows()"); + } + (NoOp(ref a), Transpose(ref b)) => { + assert_eq!($c.nrows(), a.nrows(), "C.nrows() != A.nrows()"); + assert_eq!($c.ncols(), b.nrows(), "C.ncols() != B.nrows()"); + assert_eq!(a.ncols(), b.ncols(), "A.ncols() != B.ncols()"); + } + (Transpose(ref a), Transpose(ref b)) => { + assert_eq!($c.nrows(), a.ncols(), "C.nrows() != A.ncols()"); + assert_eq!($c.ncols(), b.nrows(), "C.ncols() != B.nrows()"); + assert_eq!(a.nrows(), b.ncols(), "A.nrows() != B.ncols()"); } } - } + }}; } #[macro_use] @@ -47,32 +45,31 @@ macro_rules! assert_compatible_spadd_dims { Op::NoOp(a) => { assert_eq!($c.nrows(), a.nrows(), "C.nrows() != A.nrows()"); assert_eq!($c.ncols(), a.ncols(), "C.ncols() != A.ncols()"); - }, + } Op::Transpose(a) => { assert_eq!($c.nrows(), a.ncols(), "C.nrows() != A.ncols()"); assert_eq!($c.ncols(), a.nrows(), "C.ncols() != A.nrows()"); } } - - } + }; } +mod cs; mod csc; mod csr; mod pattern; -mod cs; pub use csc::*; pub use csr::*; pub use pattern::*; -use std::fmt::Formatter; use std::fmt; +use std::fmt::Formatter; /// A description of the error that occurred during an arithmetic operation. #[derive(Clone, Debug)] pub struct OperationError { error_kind: OperationErrorKind, - message: String + message: String, } /// The different kinds of operation errors that may occur. @@ -92,7 +89,10 @@ pub enum OperationErrorKind { impl OperationError { fn from_kind_and_message(error_type: OperationErrorKind, message: String) -> Self { - Self { error_kind: error_type, message } + Self { + error_kind: error_type, + message, + } } /// The operation error kind. @@ -110,11 +110,15 @@ impl fmt::Display for OperationError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "Sparse matrix operation error: ")?; match self.kind() { - OperationErrorKind::InvalidPattern => { write!(f, "InvalidPattern")?; } - OperationErrorKind::Singular => { write!(f, "Singular")?; } + OperationErrorKind::InvalidPattern => { + write!(f, "InvalidPattern")?; + } + OperationErrorKind::Singular => { + write!(f, "Singular")?; + } } write!(f, " Message: {}", self.message) } } -impl std::error::Error for OperationError {} \ No newline at end of file +impl std::error::Error for OperationError {} diff --git a/nalgebra-sparse/src/ops/serial/pattern.rs b/nalgebra-sparse/src/ops/serial/pattern.rs index 2569bad3..b73f3375 100644 --- a/nalgebra-sparse/src/ops/serial/pattern.rs +++ b/nalgebra-sparse/src/ops/serial/pattern.rs @@ -12,11 +12,17 @@ use std::iter; /// # Panics /// /// Panics if the patterns do not have the same major and minor dimensions. -pub fn spadd_pattern(a: &SparsityPattern, - b: &SparsityPattern) -> SparsityPattern -{ - assert_eq!(a.major_dim(), b.major_dim(), "Patterns must have identical major dimensions."); - assert_eq!(a.minor_dim(), b.minor_dim(), "Patterns must have identical minor dimensions."); +pub fn spadd_pattern(a: &SparsityPattern, b: &SparsityPattern) -> SparsityPattern { + assert_eq!( + a.major_dim(), + b.major_dim(), + "Patterns must have identical major dimensions." + ); + assert_eq!( + a.minor_dim(), + b.minor_dim(), + "Patterns must have identical minor dimensions." + ); let mut offsets = Vec::new(); let mut indices = Vec::new(); @@ -25,7 +31,7 @@ pub fn spadd_pattern(a: &SparsityPattern, offsets.push(0); - for lane_idx in 0 .. a.major_dim() { + for lane_idx in 0..a.major_dim() { let lane_a = a.lane(lane_idx); let lane_b = b.lane(lane_idx); indices.extend(iterate_union(lane_a, lane_b)); @@ -33,8 +39,7 @@ pub fn spadd_pattern(a: &SparsityPattern, } // TODO: Consider circumventing format checks? (requires unsafe, should benchmark first) - SparsityPattern::try_from_offsets_and_indices( - a.major_dim(), a.minor_dim(), offsets, indices) + SparsityPattern::try_from_offsets_and_indices(a.major_dim(), a.minor_dim(), offsets, indices) .expect("Internal error: Pattern must be valid by definition") } @@ -66,7 +71,11 @@ pub fn spmm_csc_pattern(a: &SparsityPattern, b: &SparsityPattern) -> SparsityPat /// Panics if the patterns, when interpreted as CSR patterns, are not compatible for /// matrix multiplication. pub fn spmm_csr_pattern(a: &SparsityPattern, b: &SparsityPattern) -> SparsityPattern { - assert_eq!(a.minor_dim(), b.major_dim(), "a and b must have compatible dimensions"); + assert_eq!( + a.minor_dim(), + b.major_dim(), + "a and b must have compatible dimensions" + ); let mut offsets = Vec::new(); let mut indices = Vec::new(); @@ -78,7 +87,7 @@ pub fn spmm_csr_pattern(a: &SparsityPattern, b: &SparsityPattern) -> SparsityPat // (would cut memory use to 1/8, which might help reduce cache misses) let mut visited = vec![false; b.minor_dim()]; - for i in 0 .. a.major_dim() { + for i in 0..a.major_dim() { let a_lane_i = a.lane(i); let c_lane_i_offset = *offsets.last().unwrap(); for &k in a_lane_i { @@ -93,7 +102,7 @@ pub fn spmm_csr_pattern(a: &SparsityPattern, b: &SparsityPattern) -> SparsityPat } } - let c_lane_i = &mut indices[c_lane_i_offset ..]; + let c_lane_i = &mut indices[c_lane_i_offset..]; c_lane_i.sort_unstable(); // Reset visits so that visited[j] == false for all j for the next major lane @@ -110,21 +119,23 @@ pub fn spmm_csr_pattern(a: &SparsityPattern, b: &SparsityPattern) -> SparsityPat /// Iterate over the union of the two sets represented by sorted slices /// (with unique elements) -fn iterate_union<'a>(mut sorted_a: &'a [usize], - mut sorted_b: &'a [usize]) -> impl Iterator + 'a { +fn iterate_union<'a>( + mut sorted_a: &'a [usize], + mut sorted_b: &'a [usize], +) -> impl Iterator + 'a { iter::from_fn(move || { if let (Some(a_item), Some(b_item)) = (sorted_a.first(), sorted_b.first()) { let item = if a_item < b_item { - sorted_a = &sorted_a[1 ..]; + sorted_a = &sorted_a[1..]; a_item } else if b_item < a_item { - sorted_b = &sorted_b[1 ..]; + sorted_b = &sorted_b[1..]; b_item } else { // Both lists contain the same element, advance both slices to avoid // duplicate entries in the result - sorted_a = &sorted_a[1 ..]; - sorted_b = &sorted_b[1 ..]; + sorted_a = &sorted_a[1..]; + sorted_b = &sorted_b[1..]; a_item }; Some(*item) @@ -138,4 +149,4 @@ fn iterate_union<'a>(mut sorted_a: &'a [usize], None } }) -} \ No newline at end of file +} diff --git a/nalgebra-sparse/src/pattern.rs b/nalgebra-sparse/src/pattern.rs index ee7edd42..08552d6a 100644 --- a/nalgebra-sparse/src/pattern.rs +++ b/nalgebra-sparse/src/pattern.rs @@ -1,8 +1,8 @@ //! Sparsity patterns for CSR and CSC matrices. -use crate::SparseFormatError; -use std::fmt; -use std::error::Error; use crate::cs::transpose_cs; +use crate::SparseFormatError; +use std::error::Error; +use std::fmt; /// A representation of the sparsity pattern of a CSR or CSC matrix. /// @@ -137,7 +137,7 @@ impl SparsityPattern { // minor indices within a lane are sorted, unique. In addition, each minor index // must be in bounds with respect to the minor dimension. { - for lane_idx in 0 .. major_dim { + for lane_idx in 0..major_dim { let range_start = major_offsets[lane_idx]; let range_end = major_offsets[lane_idx + 1]; @@ -146,7 +146,7 @@ impl SparsityPattern { return Err(NonmonotonicOffsets); } - let minor_indices = &minor_indices[range_start .. range_end]; + let minor_indices = &minor_indices[range_start..range_end]; // We test for in-bounds, uniqueness and monotonicity at the same time // to ensure that we only visit each minor index once @@ -232,17 +232,20 @@ impl SparsityPattern { // By using unit () values, we can use the same routines as for CSR/CSC matrices let values = vec![(); self.nnz()]; let (new_offsets, new_indices, _) = transpose_cs( - self.major_dim(), - self.minor_dim(), - self.major_offsets(), - self.minor_indices(), - &values); + self.major_dim(), + self.minor_dim(), + self.major_offsets(), + self.minor_indices(), + &values, + ); // TODO: Skip checks - Self::try_from_offsets_and_indices(self.minor_dim(), - self.major_dim(), - new_offsets, - new_indices) - .expect("Internal error: Transpose should never fail.") + Self::try_from_offsets_and_indices( + self.minor_dim(), + self.major_dim(), + new_offsets, + new_indices, + ) + .expect("Internal error: Transpose should never fail.") } } @@ -275,22 +278,25 @@ pub enum SparsityPatternFormatError { impl From for SparseFormatError { fn from(err: SparsityPatternFormatError) -> Self { - use SparsityPatternFormatError::*; - use SparsityPatternFormatError::DuplicateEntry as PatternDuplicateEntry; use crate::SparseFormatErrorKind; use crate::SparseFormatErrorKind::*; + use SparsityPatternFormatError::DuplicateEntry as PatternDuplicateEntry; + use SparsityPatternFormatError::*; match err { InvalidOffsetArrayLength | InvalidOffsetFirstLast | NonmonotonicOffsets - | NonmonotonicMinorIndices - => SparseFormatError::from_kind_and_error(InvalidStructure, Box::from(err)), - MinorIndexOutOfBounds - => SparseFormatError::from_kind_and_error(IndexOutOfBounds, - Box::from(err)), - PatternDuplicateEntry - => SparseFormatError::from_kind_and_error(SparseFormatErrorKind::DuplicateEntry, - Box::from(err)), + | NonmonotonicMinorIndices => { + SparseFormatError::from_kind_and_error(InvalidStructure, Box::from(err)) + } + MinorIndexOutOfBounds => { + SparseFormatError::from_kind_and_error(IndexOutOfBounds, Box::from(err)) + } + PatternDuplicateEntry => SparseFormatError::from_kind_and_error( + #[allow(unused_qualifications)] + SparseFormatErrorKind::DuplicateEntry, + Box::from(err), + ), } } } @@ -300,22 +306,25 @@ impl fmt::Display for SparsityPatternFormatError { match self { SparsityPatternFormatError::InvalidOffsetArrayLength => { write!(f, "Length of offset array is not equal to (major_dim + 1).") - }, + } SparsityPatternFormatError::InvalidOffsetFirstLast => { write!(f, "First or last offset is incompatible with format.") - }, + } SparsityPatternFormatError::NonmonotonicOffsets => { write!(f, "Offsets are not monotonically increasing.") - }, + } SparsityPatternFormatError::MinorIndexOutOfBounds => { write!(f, "A minor index is out of bounds.") - }, + } SparsityPatternFormatError::DuplicateEntry => { write!(f, "Input data contains duplicate entries.") - }, + } SparsityPatternFormatError::NonmonotonicMinorIndices => { - write!(f, "Minor indices are not monotonically increasing within each lane.") - }, + write!( + f, + "Minor indices are not monotonically increasing within each lane." + ) + } } } } @@ -335,12 +344,12 @@ pub struct SparsityPatternIter<'a> { impl<'a> SparsityPatternIter<'a> { fn from_pattern(pattern: &'a SparsityPattern) -> Self { let first_lane_end = pattern.major_offsets().get(1).unwrap_or(&0); - let minors_in_first_lane = &pattern.minor_indices()[0 .. *first_lane_end]; + let minors_in_first_lane = &pattern.minor_indices()[0..*first_lane_end]; Self { major_offsets: pattern.major_offsets(), minor_indices: pattern.minor_indices(), current_lane_idx: 0, - remaining_minors_in_lane: minors_in_first_lane + remaining_minors_in_lane: minors_in_first_lane, } } } @@ -374,12 +383,11 @@ impl<'a> Iterator for SparsityPatternIter<'a> { let lower = self.major_offsets[self.current_lane_idx]; let upper = self.major_offsets[self.current_lane_idx + 1]; if upper > lower { - self.remaining_minors_in_lane = &self.minor_indices[(lower + 1) .. upper]; - return Some((self.current_lane_idx, self.minor_indices[lower])) + self.remaining_minors_in_lane = &self.minor_indices[(lower + 1)..upper]; + return Some((self.current_lane_idx, self.minor_indices[lower])); } } } } } } - diff --git a/nalgebra-sparse/src/proptest.rs b/nalgebra-sparse/src/proptest.rs index 43b80896..472c466f 100644 --- a/nalgebra-sparse/src/proptest.rs +++ b/nalgebra-sparse/src/proptest.rs @@ -11,20 +11,22 @@ mod proptest_patched; use crate::coo::CooMatrix; -use proptest::prelude::*; -use proptest::collection::{vec, hash_map, btree_set}; -use nalgebra::{Scalar, Dim}; -use std::cmp::min; -use std::iter::{repeat}; -use proptest::sample::{Index}; +use crate::csc::CscMatrix; use crate::csr::CsrMatrix; use crate::pattern::SparsityPattern; -use crate::csc::CscMatrix; use nalgebra::proptest::DimRange; +use nalgebra::{Dim, Scalar}; +use proptest::collection::{btree_set, hash_map, vec}; +use proptest::prelude::*; +use proptest::sample::Index; +use std::cmp::min; +use std::iter::repeat; -fn dense_row_major_coord_strategy(nrows: usize, ncols: usize, nnz: usize) - -> impl Strategy> -{ +fn dense_row_major_coord_strategy( + nrows: usize, + ncols: usize, + nnz: usize, +) -> impl Strategy> { assert!(nnz <= nrows * ncols); let mut booleans = vec![true; nnz]; booleans.append(&mut vec![false; (nrows * ncols) - nnz]); @@ -38,33 +40,33 @@ fn dense_row_major_coord_strategy(nrows: usize, ncols: usize, nnz: usize) // // Need to shuffle to make sure they are randomly distributed // .prop_shuffle() - proptest_patched::Shuffle(Just(booleans)) - .prop_map(move |booleans| { - booleans - .into_iter() - .enumerate() - .filter_map(|(index, is_entry)| { - if is_entry { - // Convert linear index to row/col pair - let i = index / ncols; - let j = index % ncols; - Some((i, j)) - } else { - None - } - }) - .collect::>() - }) + proptest_patched::Shuffle(Just(booleans)).prop_map(move |booleans| { + booleans + .into_iter() + .enumerate() + .filter_map(|(index, is_entry)| { + if is_entry { + // Convert linear index to row/col pair + let i = index / ncols; + let j = index % ncols; + Some((i, j)) + } else { + None + } + }) + .collect::>() + }) } /// A strategy for generating `nnz` triplets. /// /// This strategy should generally only be used when `nnz` is close to `nrows * ncols`. -fn dense_triplet_strategy(value_strategy: T, - nrows: usize, - ncols: usize, - nnz: usize) - -> impl Strategy> +fn dense_triplet_strategy( + value_strategy: T, + nrows: usize, + ncols: usize, + nnz: usize, +) -> impl Strategy> where T: Strategy + Clone + 'static, T::Value: Scalar, @@ -100,15 +102,14 @@ where }) // Assign values to each coordinate pair in order to generate a list of triplets .prop_flat_map(move |coords| { - vec![value_strategy.clone(); coords.len()] - .prop_map(move |values| { - coords.clone().into_iter() - .zip(values) - .map(|((i, j), v)| { - (i, j, v) - }) - .collect::>() - }) + vec![value_strategy.clone(); coords.len()].prop_map(move |values| { + coords + .clone() + .into_iter() + .zip(values) + .map(|((i, j), v)| (i, j, v)) + .collect::>() + }) }) } @@ -116,25 +117,23 @@ where /// /// This strategy should generally only be used when `nnz << nrows * ncols`. If `nnz` is too /// close to `nrows * ncols` it may fail due to excessive rejected samples. -fn sparse_triplet_strategy(value_strategy: T, - nrows: usize, - ncols: usize, - nnz: usize) - -> impl Strategy> - where - T: Strategy + Clone + 'static, - T::Value: Scalar, +fn sparse_triplet_strategy( + value_strategy: T, + nrows: usize, + ncols: usize, + nnz: usize, +) -> impl Strategy> +where + T: Strategy + Clone + 'static, + T::Value: Scalar, { // Have to handle the zero case: proptest doesn't like empty ranges (i.e. 0 .. 0) - let row_index_strategy = if nrows > 0 { 0 .. nrows } else { 0 .. 1 }; - let col_index_strategy = if ncols > 0 { 0 .. ncols } else { 0 .. 1 }; + let row_index_strategy = if nrows > 0 { 0..nrows } else { 0..1 }; + let col_index_strategy = if ncols > 0 { 0..ncols } else { 0..1 }; let coord_strategy = (row_index_strategy, col_index_strategy); hash_map(coord_strategy, value_strategy.clone(), nnz) .prop_map(|hash_map| { - let triplets: Vec<_> = hash_map - .into_iter() - .map(|((i, j), v)| (i, j, v)) - .collect(); + let triplets: Vec<_> = hash_map.into_iter().map(|((i, j), v)| (i, j, v)).collect(); triplets }) // Although order in the hash map is unspecified, it's not necessarily *random* @@ -153,36 +152,41 @@ pub fn coo_no_duplicates( value_strategy: T, rows: impl Into, cols: impl Into, - max_nonzeros: usize) -> impl Strategy> + max_nonzeros: usize, +) -> impl Strategy> where T: Strategy + Clone + 'static, T::Value: Scalar, { - (rows.into().to_range_inclusive(), cols.into().to_range_inclusive()) + ( + rows.into().to_range_inclusive(), + cols.into().to_range_inclusive(), + ) .prop_flat_map(move |(nrows, ncols)| { let max_nonzeros = min(max_nonzeros, nrows * ncols); - let size_range = 0 ..= max_nonzeros; + let size_range = 0..=max_nonzeros; let value_strategy = value_strategy.clone(); - size_range.prop_flat_map(move |nnz| { - let value_strategy = value_strategy.clone(); - if nnz as f64 > 0.10 * (nrows as f64) * (ncols as f64) { - // If the number of nnz is sufficiently dense, then use the dense - // sample strategy - dense_triplet_strategy(value_strategy, nrows, ncols, nnz).boxed() - } else { - // Otherwise, use a hash map strategy so that we can get a sparse sampling - // (so that complexity is rather on the order of max_nnz than nrows * ncols) - sparse_triplet_strategy(value_strategy, nrows, ncols, nnz).boxed() - } - }) - .prop_map(move |triplets| { - let mut coo = CooMatrix::new(nrows, ncols); - for (i, j, v) in triplets { - coo.push(i, j, v); - } - coo - }) + size_range + .prop_flat_map(move |nnz| { + let value_strategy = value_strategy.clone(); + if nnz as f64 > 0.10 * (nrows as f64) * (ncols as f64) { + // If the number of nnz is sufficiently dense, then use the dense + // sample strategy + dense_triplet_strategy(value_strategy, nrows, ncols, nnz).boxed() + } else { + // Otherwise, use a hash map strategy so that we can get a sparse sampling + // (so that complexity is rather on the order of max_nnz than nrows * ncols) + sparse_triplet_strategy(value_strategy, nrows, ncols, nnz).boxed() + } + }) + .prop_map(move |triplets| { + let mut coo = CooMatrix::new(nrows, ncols); + for (i, j, v) in triplets { + coo.push(i, j, v); + } + coo + }) }) } @@ -198,21 +202,22 @@ where /// number of duplicate entries is determined by `max_duplicates`. Note that the matrix might still /// contain explicitly stored zeroes if the value strategy is capable of generating zero values. pub fn coo_with_duplicates( - value_strategy: T, - rows: impl Into, - cols: impl Into, - max_nonzeros: usize, - max_duplicates: usize) - -> impl Strategy> + value_strategy: T, + rows: impl Into, + cols: impl Into, + max_nonzeros: usize, + max_duplicates: usize, +) -> impl Strategy> where T: Strategy + Clone + 'static, T::Value: Scalar, { let coo_strategy = coo_no_duplicates(value_strategy.clone(), rows, cols, max_nonzeros); - let duplicate_strategy = vec((any::(), value_strategy.clone()), 0 ..= max_duplicates); + let duplicate_strategy = vec((any::(), value_strategy.clone()), 0..=max_duplicates); (coo_strategy, duplicate_strategy) .prop_flat_map(|(coo, duplicates)| { - let mut triplets: Vec<(usize, usize, T::Value)> = coo.triplet_iter() + let mut triplets: Vec<(usize, usize, T::Value)> = coo + .triplet_iter() .map(|(i, j, v)| (i, j, v.clone())) .collect(); if !triplets.is_empty() { @@ -238,9 +243,13 @@ where }) } -fn sparsity_pattern_from_row_major_coords(nmajor: usize, nminor: usize, coords: I) -> SparsityPattern +fn sparsity_pattern_from_row_major_coords( + nmajor: usize, + nminor: usize, + coords: I, +) -> SparsityPattern where - I: Iterator + ExactSizeIterator, + I: Iterator + ExactSizeIterator, { let mut minors = Vec::with_capacity(coords.len()); let mut offsets = Vec::with_capacity(nmajor + 1); @@ -248,8 +257,11 @@ where offsets.push(0); for (idx, (i, j)) in coords.enumerate() { assert!(i >= current_major); - assert!(i < nmajor && j < nminor, "Generated coords are out of bounds"); - while current_major < i{ + assert!( + i < nmajor && j < nminor, + "Generated coords are out of bounds" + ); + while current_major < i { offsets.push(idx); current_major += 1; } @@ -264,10 +276,7 @@ where assert_eq!(offsets.first().unwrap(), &0); assert_eq!(offsets.len(), nmajor + 1); - SparsityPattern::try_from_offsets_and_indices(nmajor, - nminor, - offsets, - minors) + SparsityPattern::try_from_offsets_and_indices(nmajor, nminor, offsets, minors) .expect("Internal error: Generated sparsity pattern is invalid") } @@ -275,14 +284,17 @@ where pub fn sparsity_pattern( major_lanes: impl Into, minor_lanes: impl Into, - max_nonzeros: usize) --> impl Strategy -{ - (major_lanes.into().to_range_inclusive(), minor_lanes.into().to_range_inclusive()) + max_nonzeros: usize, +) -> impl Strategy { + ( + major_lanes.into().to_range_inclusive(), + minor_lanes.into().to_range_inclusive(), + ) .prop_flat_map(move |(nmajor, nminor)| { let max_nonzeros = min(nmajor * nminor, max_nonzeros); - (Just(nmajor), Just(nminor), 0 ..= max_nonzeros) - }).prop_flat_map(move |(nmajor, nminor, nnz)| { + (Just(nmajor), Just(nminor), 0..=max_nonzeros) + }) + .prop_flat_map(move |(nmajor, nminor, nnz)| { if 10 * nnz < nmajor * nminor { // If nnz is small compared to a dense matrix, then use a sparse sampling strategy btree_set((0..nmajor, 0..nminor), nnz) @@ -297,55 +309,66 @@ pub fn sparsity_pattern( .prop_map(move |coords| { let coords = coords.into_iter(); sparsity_pattern_from_row_major_coords(nmajor, nminor, coords) - }).boxed() + }) + .boxed() } }) } /// A strategy for generating CSR matrices. -pub fn csr(value_strategy: T, - rows: impl Into, - cols: impl Into, - max_nonzeros: usize) - -> impl Strategy> +pub fn csr( + value_strategy: T, + rows: impl Into, + cols: impl Into, + max_nonzeros: usize, +) -> impl Strategy> where T: Strategy + Clone + 'static, T::Value: Scalar, { let rows = rows.into(); let cols = cols.into(); - sparsity_pattern(rows.lower_bound().value() ..= rows.upper_bound().value(), cols.lower_bound().value() ..= cols.upper_bound().value(), max_nonzeros) - .prop_flat_map(move |pattern| { - let nnz = pattern.nnz(); - let values = vec![value_strategy.clone(); nnz]; - (Just(pattern), values) - }) - .prop_map(|(pattern, values)| { - CsrMatrix::try_from_pattern_and_values(pattern, values) - .expect("Internal error: Generated CsrMatrix is invalid") - }) + sparsity_pattern( + rows.lower_bound().value()..=rows.upper_bound().value(), + cols.lower_bound().value()..=cols.upper_bound().value(), + max_nonzeros, + ) + .prop_flat_map(move |pattern| { + let nnz = pattern.nnz(); + let values = vec![value_strategy.clone(); nnz]; + (Just(pattern), values) + }) + .prop_map(|(pattern, values)| { + CsrMatrix::try_from_pattern_and_values(pattern, values) + .expect("Internal error: Generated CsrMatrix is invalid") + }) } /// A strategy for generating CSC matrices. -pub fn csc(value_strategy: T, - rows: impl Into, - cols: impl Into, - max_nonzeros: usize) - -> impl Strategy> - where - T: Strategy + Clone + 'static, - T::Value: Scalar, +pub fn csc( + value_strategy: T, + rows: impl Into, + cols: impl Into, + max_nonzeros: usize, +) -> impl Strategy> +where + T: Strategy + Clone + 'static, + T::Value: Scalar, { let rows = rows.into(); let cols = cols.into(); - sparsity_pattern(cols.lower_bound().value() ..= cols.upper_bound().value(), rows.lower_bound().value() ..= rows.upper_bound().value(), max_nonzeros) - .prop_flat_map(move |pattern| { - let nnz = pattern.nnz(); - let values = vec![value_strategy.clone(); nnz]; - (Just(pattern), values) - }) - .prop_map(|(pattern, values)| { - CscMatrix::try_from_pattern_and_values(pattern, values) - .expect("Internal error: Generated CscMatrix is invalid") - }) -} \ No newline at end of file + sparsity_pattern( + cols.lower_bound().value()..=cols.upper_bound().value(), + rows.lower_bound().value()..=rows.upper_bound().value(), + max_nonzeros, + ) + .prop_flat_map(move |pattern| { + let nnz = pattern.nnz(); + let values = vec![value_strategy.clone(); nnz]; + (Just(pattern), values) + }) + .prop_map(|(pattern, values)| { + CscMatrix::try_from_pattern_and_values(pattern, values) + .expect("Internal error: Generated CscMatrix is invalid") + }) +} diff --git a/nalgebra-sparse/src/proptest/proptest_patched.rs b/nalgebra-sparse/src/proptest/proptest_patched.rs index fa3d9d25..695bc30b 100644 --- a/nalgebra-sparse/src/proptest/proptest_patched.rs +++ b/nalgebra-sparse/src/proptest/proptest_patched.rs @@ -22,19 +22,19 @@ */ -use proptest::strategy::{Strategy, Shuffleable, NewTree, ValueTree}; -use proptest::test_runner::{TestRunner, TestRng}; -use std::cell::Cell; use proptest::num; use proptest::prelude::Rng; +use proptest::strategy::{NewTree, Shuffleable, Strategy, ValueTree}; +use proptest::test_runner::{TestRng, TestRunner}; +use std::cell::Cell; #[derive(Clone, Debug)] #[must_use = "strategies do nothing unless used"] pub struct Shuffle(pub(super) S); impl Strategy for Shuffle - where - S::Value: Shuffleable, +where + S::Value: Shuffleable, { type Tree = ShuffleValueTree; type Value = S::Value; @@ -60,8 +60,8 @@ pub struct ShuffleValueTree { } impl ShuffleValueTree - where - V::Value: Shuffleable, +where + V::Value: Shuffleable, { fn init_dist(&self, dflt: usize) -> usize { if self.dist.get().is_none() { @@ -79,8 +79,8 @@ impl ShuffleValueTree } impl ValueTree for ShuffleValueTree - where - V::Value: Shuffleable, +where + V::Value: Shuffleable, { type Value = V::Value; @@ -143,4 +143,4 @@ impl ValueTree for ShuffleValueTree self.dist.get_mut().as_mut().unwrap().complicate() } } -} \ No newline at end of file +} diff --git a/nalgebra-sparse/tests/common/mod.rs b/nalgebra-sparse/tests/common/mod.rs index 2ff441fe..053fe756 100644 --- a/nalgebra-sparse/tests/common/mod.rs +++ b/nalgebra-sparse/tests/common/mod.rs @@ -1,15 +1,15 @@ -use proptest::strategy::Strategy; -use nalgebra_sparse::csr::CsrMatrix; -use nalgebra_sparse::proptest::{csr, csc}; use nalgebra_sparse::csc::CscMatrix; -use std::ops::RangeInclusive; -use std::convert::{TryFrom}; +use nalgebra_sparse::csr::CsrMatrix; +use nalgebra_sparse::proptest::{csc, csr}; +use proptest::strategy::Strategy; +use std::convert::TryFrom; use std::fmt::Debug; +use std::ops::RangeInclusive; #[macro_export] macro_rules! assert_panics { ($e:expr) => {{ - use std::panic::{catch_unwind}; + use std::panic::catch_unwind; use std::stringify; let expr_string = stringify!($e); @@ -22,37 +22,56 @@ macro_rules! assert_panics { let result = catch_unwind(|| $e); if result.is_ok() { - panic!("assert_panics!({}) failed: the expression did not panic.", expr_string); + panic!( + "assert_panics!({}) failed: the expression did not panic.", + expr_string + ); } }}; } pub const PROPTEST_MATRIX_DIM: RangeInclusive = 0..=6; pub const PROPTEST_MAX_NNZ: usize = 40; -pub const PROPTEST_I32_VALUE_STRATEGY: RangeInclusive = -5 ..= 5; +pub const PROPTEST_I32_VALUE_STRATEGY: RangeInclusive = -5..=5; pub fn value_strategy() -> RangeInclusive where T: TryFrom, - T::Error: Debug + T::Error: Debug, { - let (start, end) = (PROPTEST_I32_VALUE_STRATEGY.start(), PROPTEST_I32_VALUE_STRATEGY.end()); - T::try_from(*start).unwrap() ..= T::try_from(*end).unwrap() + let (start, end) = ( + PROPTEST_I32_VALUE_STRATEGY.start(), + PROPTEST_I32_VALUE_STRATEGY.end(), + ); + T::try_from(*start).unwrap()..=T::try_from(*end).unwrap() } -pub fn non_zero_i32_value_strategy() -> impl Strategy { - let (start, end) = (PROPTEST_I32_VALUE_STRATEGY.start(), PROPTEST_I32_VALUE_STRATEGY.end()); +pub fn non_zero_i32_value_strategy() -> impl Strategy { + let (start, end) = ( + PROPTEST_I32_VALUE_STRATEGY.start(), + PROPTEST_I32_VALUE_STRATEGY.end(), + ); assert!(start < &0); assert!(end > &0); // Note: we don't use RangeInclusive for the second range, because then we'd have different // types, which would require boxing - (*start .. 0).prop_union(1 .. *end + 1) + (*start..0).prop_union(1..*end + 1) } -pub fn csr_strategy() -> impl Strategy> { - csr(PROPTEST_I32_VALUE_STRATEGY, PROPTEST_MATRIX_DIM, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ) +pub fn csr_strategy() -> impl Strategy> { + csr( + PROPTEST_I32_VALUE_STRATEGY, + PROPTEST_MATRIX_DIM, + PROPTEST_MATRIX_DIM, + PROPTEST_MAX_NNZ, + ) } -pub fn csc_strategy() -> impl Strategy> { - csc(PROPTEST_I32_VALUE_STRATEGY, PROPTEST_MATRIX_DIM, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ) +pub fn csc_strategy() -> impl Strategy> { + csc( + PROPTEST_I32_VALUE_STRATEGY, + PROPTEST_MATRIX_DIM, + PROPTEST_MATRIX_DIM, + PROPTEST_MAX_NNZ, + ) } diff --git a/nalgebra-sparse/tests/unit.rs b/nalgebra-sparse/tests/unit.rs index 014a67f5..73a95cd7 100644 --- a/nalgebra-sparse/tests/unit.rs +++ b/nalgebra-sparse/tests/unit.rs @@ -5,4 +5,4 @@ compile_error!("Tests must be run with features `proptest-support` and `compare` mod unit_tests; #[macro_use] -pub mod common; \ No newline at end of file +pub mod common; diff --git a/nalgebra-sparse/tests/unit_tests/convert_serial.rs b/nalgebra-sparse/tests/unit_tests/convert_serial.rs index 9dc13c71..b895c945 100644 --- a/nalgebra-sparse/tests/unit_tests/convert_serial.rs +++ b/nalgebra-sparse/tests/unit_tests/convert_serial.rs @@ -1,17 +1,16 @@ -use nalgebra_sparse::coo::CooMatrix; -use nalgebra_sparse::convert::serial::{convert_coo_dense, convert_coo_csr, - convert_dense_coo, convert_csr_dense, - convert_csr_coo, convert_dense_csr, - convert_csc_coo, convert_coo_csc, - convert_csc_dense, convert_dense_csc, - convert_csr_csc, convert_csc_csr}; -use nalgebra_sparse::proptest::{coo_with_duplicates, coo_no_duplicates, csr, csc}; -use nalgebra::proptest::matrix; -use proptest::prelude::*; -use nalgebra::DMatrix; -use nalgebra_sparse::csr::CsrMatrix; -use nalgebra_sparse::csc::CscMatrix; use crate::common::csc_strategy; +use nalgebra::proptest::matrix; +use nalgebra::DMatrix; +use nalgebra_sparse::convert::serial::{ + convert_coo_csc, convert_coo_csr, convert_coo_dense, convert_csc_coo, convert_csc_csr, + convert_csc_dense, convert_csr_coo, convert_csr_csc, convert_csr_dense, convert_dense_coo, + convert_dense_csc, convert_dense_csr, +}; +use nalgebra_sparse::coo::CooMatrix; +use nalgebra_sparse::csc::CscMatrix; +use nalgebra_sparse::csr::CsrMatrix; +use nalgebra_sparse::proptest::{coo_no_duplicates, coo_with_duplicates, csc, csr}; +use proptest::prelude::*; #[test] fn test_convert_dense_coo() { @@ -41,16 +40,17 @@ fn test_convert_dense_coo() { // Here we implicitly test that the coo matrix is indeed constructed from column-major // iteration of the dense matrix. let dense = DMatrix::from_row_slice(2, 3, entries); - let coo_no_dup = CooMatrix::try_from_triplets(2, 3, - vec![0, 1, 0], - vec![0, 1, 2], - vec![1, 5, 3]) - .unwrap(); - let coo_dup = CooMatrix::try_from_triplets(2, 3, - vec![0, 1, 0, 1], - vec![0, 1, 2, 1], - vec![1, -2, 3, 7]) - .unwrap(); + let coo_no_dup = + CooMatrix::try_from_triplets(2, 3, vec![0, 1, 0], vec![0, 1, 2], vec![1, 5, 3]) + .unwrap(); + let coo_dup = CooMatrix::try_from_triplets( + 2, + 3, + vec![0, 1, 0, 1], + vec![0, 1, 2, 1], + vec![1, -2, 3, 7], + ) + .unwrap(); assert_eq!(CooMatrix::from(&dense), coo_no_dup); assert_eq!(DMatrix::from(&coo_dup), dense); @@ -76,8 +76,9 @@ fn test_convert_coo_csr() { 4, vec![0, 1, 2, 5], vec![1, 3, 0, 2, 3], - vec![2, 4, 1, 1, 2] - ).unwrap(); + vec![2, 4, 1, 1, 2], + ) + .unwrap(); assert_eq!(convert_coo_csr(&coo), expected_csr); } @@ -101,8 +102,9 @@ fn test_convert_coo_csr() { 4, vec![0, 1, 2, 5], vec![1, 3, 0, 2, 3], - vec![5, 4, 1, 1, 4] - ).unwrap(); + vec![5, 4, 1, 1, 4], + ) + .unwrap(); assert_eq!(convert_coo_csr(&coo), expected_csr); } @@ -115,16 +117,18 @@ fn test_convert_csr_coo() { 4, vec![0, 1, 2, 5], vec![1, 3, 0, 2, 3], - vec![5, 4, 1, 1, 4] - ).unwrap(); + vec![5, 4, 1, 1, 4], + ) + .unwrap(); let expected_coo = CooMatrix::try_from_triplets( 3, 4, vec![0, 1, 2, 2, 2], vec![1, 3, 0, 2, 3], - vec![5, 4, 1, 1, 4] - ).unwrap(); + vec![5, 4, 1, 1, 4], + ) + .unwrap(); assert_eq!(convert_csr_coo(&csr), expected_coo); } @@ -148,8 +152,9 @@ fn test_convert_coo_csc() { 4, vec![0, 1, 2, 3, 5], vec![2, 0, 2, 1, 2], - vec![1, 2, 1, 4, 2] - ).unwrap(); + vec![1, 2, 1, 4, 2], + ) + .unwrap(); assert_eq!(convert_coo_csc(&coo), expected_csc); } @@ -173,8 +178,9 @@ fn test_convert_coo_csc() { 4, vec![0, 1, 2, 3, 5], vec![2, 0, 2, 1, 2], - vec![1, 5, 1, 4, 4] - ).unwrap(); + vec![1, 5, 1, 4, 4], + ) + .unwrap(); assert_eq!(convert_coo_csc(&coo), expected_csc); } @@ -187,16 +193,18 @@ fn test_convert_csc_coo() { 4, vec![0, 1, 2, 3, 5], vec![2, 0, 2, 1, 2], - vec![1, 2, 1, 4, 2] - ).unwrap(); + vec![1, 2, 1, 4, 2], + ) + .unwrap(); let expected_coo = CooMatrix::try_from_triplets( 3, 4, vec![2, 0, 2, 1, 2], vec![0, 1, 2, 3, 3], - vec![1, 2, 1, 4, 2] - ).unwrap(); + vec![1, 2, 1, 4, 2], + ) + .unwrap(); assert_eq!(convert_csc_coo(&csc), expected_coo); } @@ -209,7 +217,8 @@ fn test_convert_csr_csc_bidirectional() { vec![0, 3, 4, 6], vec![1, 2, 3, 0, 1, 3], vec![5, 3, 2, 2, 1, 4], - ).unwrap(); + ) + .unwrap(); let csc = CscMatrix::try_from_csc_data( 3, @@ -217,7 +226,8 @@ fn test_convert_csr_csc_bidirectional() { vec![0, 1, 3, 4, 6], vec![1, 0, 2, 0, 0, 2], vec![2, 5, 1, 3, 2, 4], - ).unwrap(); + ) + .unwrap(); assert_eq!(convert_csr_csc(&csr), csc); assert_eq!(convert_csc_csr(&csc), csr); @@ -231,7 +241,8 @@ fn test_convert_csr_dense_bidirectional() { vec![0, 3, 4, 6], vec![1, 2, 3, 0, 1, 3], vec![5, 3, 2, 2, 1, 4], - ).unwrap(); + ) + .unwrap(); #[rustfmt::skip] let dense = DMatrix::from_row_slice(3, 4, &[ @@ -252,7 +263,8 @@ fn test_convert_csc_dense_bidirectional() { vec![0, 1, 3, 4, 6], vec![1, 0, 2, 0, 0, 2], vec![2, 5, 1, 3, 2, 4], - ).unwrap(); + ) + .unwrap(); #[rustfmt::skip] let dense = DMatrix::from_row_slice(3, 4, &[ @@ -265,29 +277,29 @@ fn test_convert_csc_dense_bidirectional() { assert_eq!(convert_dense_csc(&dense), csc); } -fn coo_strategy() -> impl Strategy> { - coo_with_duplicates(-5 ..= 5, 0..=6usize, 0..=6usize, 40, 2) +fn coo_strategy() -> impl Strategy> { + coo_with_duplicates(-5..=5, 0..=6usize, 0..=6usize, 40, 2) } -fn coo_no_duplicates_strategy() -> impl Strategy> { - coo_no_duplicates(-5 ..= 5, 0..=6usize, 0..=6usize, 40) +fn coo_no_duplicates_strategy() -> impl Strategy> { + coo_no_duplicates(-5..=5, 0..=6usize, 0..=6usize, 40) } -fn csr_strategy() -> impl Strategy> { - csr(-5 ..= 5, 0..=6usize, 0..=6usize, 40) +fn csr_strategy() -> impl Strategy> { + csr(-5..=5, 0..=6usize, 0..=6usize, 40) } /// Avoid generating explicit zero values so that it is possible to reason about sparsity patterns -fn non_zero_csr_strategy() -> impl Strategy> { - csr(1 ..= 5, 0..=6usize, 0..=6usize, 40) +fn non_zero_csr_strategy() -> impl Strategy> { + csr(1..=5, 0..=6usize, 0..=6usize, 40) } /// Avoid generating explicit zero values so that it is possible to reason about sparsity patterns -fn non_zero_csc_strategy() -> impl Strategy> { - csc(1 ..= 5, 0..=6usize, 0..=6usize, 40) +fn non_zero_csc_strategy() -> impl Strategy> { + csc(1..=5, 0..=6usize, 0..=6usize, 40) } -fn dense_strategy() -> impl Strategy> { +fn dense_strategy() -> impl Strategy> { matrix(-5..=5, 0..=6, 0..=6) } @@ -437,4 +449,4 @@ proptest! { fn csr_from_csc_roundtrip(csc in csc_strategy()) { prop_assert_eq!(&csc, &CscMatrix::from(&CsrMatrix::from(&csc))); } -} \ No newline at end of file +} diff --git a/nalgebra-sparse/tests/unit_tests/coo.rs b/nalgebra-sparse/tests/unit_tests/coo.rs index 3373d89a..c9fa1778 100644 --- a/nalgebra-sparse/tests/unit_tests/coo.rs +++ b/nalgebra-sparse/tests/unit_tests/coo.rs @@ -1,7 +1,7 @@ -use nalgebra_sparse::{SparseFormatErrorKind}; -use nalgebra_sparse::coo::CooMatrix; -use nalgebra::DMatrix; use crate::assert_panics; +use nalgebra::DMatrix; +use nalgebra_sparse::coo::CooMatrix; +use nalgebra_sparse::SparseFormatErrorKind; #[test] fn coo_construction_for_valid_data() { @@ -10,8 +10,8 @@ fn coo_construction_for_valid_data() { { // Zero matrix - let coo = CooMatrix::::try_from_triplets(3, 2, Vec::new(), Vec::new(), Vec::new()) - .unwrap(); + let coo = + CooMatrix::::try_from_triplets(3, 2, Vec::new(), Vec::new(), Vec::new()).unwrap(); assert_eq!(coo.nrows(), 3); assert_eq!(coo.ncols(), 2); assert!(coo.triplet_iter().next().is_none()); @@ -27,8 +27,8 @@ fn coo_construction_for_valid_data() { 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 coo = CooMatrix::::try_from_triplets(3, 5, i.clone(), j.clone(), v.clone()) - .unwrap(); + let coo = + CooMatrix::::try_from_triplets(3, 5, i.clone(), j.clone(), v.clone()).unwrap(); assert_eq!(coo.nrows(), 3); assert_eq!(coo.ncols(), 5); @@ -59,8 +59,8 @@ fn coo_construction_for_valid_data() { 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::::try_from_triplets(3, 5, i.clone(), j.clone(), v.clone()) - .unwrap(); + let coo = + CooMatrix::::try_from_triplets(3, 5, i.clone(), j.clone(), v.clone()).unwrap(); assert_eq!(coo.nrows(), 3); assert_eq!(coo.ncols(), 5); @@ -92,25 +92,37 @@ fn coo_try_from_triplets_reports_out_of_bounds_indices() { { // 0x0 matrix let result = CooMatrix::::try_from_triplets(0, 0, vec![0], vec![0], vec![2]); - assert!(matches!(result.unwrap_err().kind(), SparseFormatErrorKind::IndexOutOfBounds)); + assert!(matches!( + result.unwrap_err().kind(), + SparseFormatErrorKind::IndexOutOfBounds + )); } { // 1x1 matrix, row out of bounds let result = CooMatrix::::try_from_triplets(1, 1, vec![1], vec![0], vec![2]); - assert!(matches!(result.unwrap_err().kind(), SparseFormatErrorKind::IndexOutOfBounds)); + assert!(matches!( + result.unwrap_err().kind(), + SparseFormatErrorKind::IndexOutOfBounds + )); } { // 1x1 matrix, col out of bounds let result = CooMatrix::::try_from_triplets(1, 1, vec![0], vec![1], vec![2]); - assert!(matches!(result.unwrap_err().kind(), SparseFormatErrorKind::IndexOutOfBounds)); + assert!(matches!( + result.unwrap_err().kind(), + SparseFormatErrorKind::IndexOutOfBounds + )); } { // 1x1 matrix, row and col out of bounds let result = CooMatrix::::try_from_triplets(1, 1, vec![1], vec![1], vec![2]); - assert!(matches!(result.unwrap_err().kind(), SparseFormatErrorKind::IndexOutOfBounds)); + assert!(matches!( + result.unwrap_err().kind(), + SparseFormatErrorKind::IndexOutOfBounds + )); } { @@ -119,7 +131,10 @@ fn coo_try_from_triplets_reports_out_of_bounds_indices() { let j = vec![0, 2, 1, 3, 3]; let v = vec![2, 3, 7, 3, 1]; let result = CooMatrix::::try_from_triplets(3, 5, i, j, v); - assert!(matches!(result.unwrap_err().kind(), SparseFormatErrorKind::IndexOutOfBounds)); + assert!(matches!( + result.unwrap_err().kind(), + SparseFormatErrorKind::IndexOutOfBounds + )); } { @@ -128,7 +143,10 @@ fn coo_try_from_triplets_reports_out_of_bounds_indices() { let j = vec![0, 2, 1, 5, 3]; let v = vec![2, 3, 7, 3, 1]; let result = CooMatrix::::try_from_triplets(3, 5, i, j, v); - assert!(matches!(result.unwrap_err().kind(), SparseFormatErrorKind::IndexOutOfBounds)); + assert!(matches!( + result.unwrap_err().kind(), + SparseFormatErrorKind::IndexOutOfBounds + )); } } @@ -137,16 +155,55 @@ fn coo_try_from_triplets_panics_on_mismatched_vectors() { // Check that try_from_triplets panics when the triplet vectors have different lengths macro_rules! assert_errs { ($result:expr) => { - assert!(matches!($result.unwrap_err().kind(), SparseFormatErrorKind::InvalidStructure)) - } + assert!(matches!( + $result.unwrap_err().kind(), + SparseFormatErrorKind::InvalidStructure + )) + }; } - assert_errs!(CooMatrix::::try_from_triplets(3, 5, vec![1, 2], vec![0], vec![0])); - assert_errs!(CooMatrix::::try_from_triplets(3, 5, vec![1], vec![0, 0], vec![0])); - assert_errs!(CooMatrix::::try_from_triplets(3, 5, vec![1], vec![0], vec![0, 1])); - assert_errs!(CooMatrix::::try_from_triplets(3, 5, vec![1, 2], vec![0, 1], vec![0])); - assert_errs!(CooMatrix::::try_from_triplets(3, 5, vec![1], vec![0, 1], vec![0, 1])); - assert_errs!(CooMatrix::::try_from_triplets(3, 5, vec![1, 1], vec![0], vec![0, 1])); + assert_errs!(CooMatrix::::try_from_triplets( + 3, + 5, + vec![1, 2], + vec![0], + vec![0] + )); + assert_errs!(CooMatrix::::try_from_triplets( + 3, + 5, + vec![1], + vec![0, 0], + vec![0] + )); + assert_errs!(CooMatrix::::try_from_triplets( + 3, + 5, + vec![1], + vec![0], + vec![0, 1] + )); + assert_errs!(CooMatrix::::try_from_triplets( + 3, + 5, + vec![1, 2], + vec![0, 1], + vec![0] + )); + assert_errs!(CooMatrix::::try_from_triplets( + 3, + 5, + vec![1], + vec![0, 1], + vec![0, 1] + )); + assert_errs!(CooMatrix::::try_from_triplets( + 3, + 5, + vec![1, 1], + vec![0], + vec![0, 1] + )); } #[test] @@ -157,10 +214,16 @@ fn coo_push_valid_entries() { assert_eq!(coo.triplet_iter().collect::>(), vec![(0, 0, &1)]); coo.push(0, 0, 2); - assert_eq!(coo.triplet_iter().collect::>(), vec![(0, 0, &1), (0, 0, &2)]); + assert_eq!( + coo.triplet_iter().collect::>(), + vec![(0, 0, &1), (0, 0, &2)] + ); coo.push(2, 2, 3); - assert_eq!(coo.triplet_iter().collect::>(), vec![(0, 0, &1), (0, 0, &2), (2, 2, &3)]); + assert_eq!( + coo.triplet_iter().collect::>(), + vec![(0, 0, &1), (0, 0, &2), (2, 2, &3)] + ); } #[test] @@ -188,4 +251,4 @@ fn coo_push_out_of_bounds_entries() { assert_panics!(coo.clone().push(2, 2, 1)); assert_panics!(coo.clone().push(3, 2, 1)); } -} \ No newline at end of file +} diff --git a/nalgebra-sparse/tests/unit_tests/csc.rs b/nalgebra-sparse/tests/unit_tests/csc.rs index 47181987..b27f19be 100644 --- a/nalgebra-sparse/tests/unit_tests/csc.rs +++ b/nalgebra-sparse/tests/unit_tests/csc.rs @@ -1,6 +1,6 @@ +use nalgebra::DMatrix; use nalgebra_sparse::csc::CscMatrix; use nalgebra_sparse::SparseFormatErrorKind; -use nalgebra::DMatrix; use proptest::prelude::*; use proptest::sample::subsequence; @@ -42,7 +42,10 @@ fn csc_matrix_valid_data() { assert_eq!(matrix.col_mut(0).row_indices(), &[]); assert_eq!(matrix.col_mut(0).values(), &[]); assert_eq!(matrix.col_mut(0).values_mut(), &[]); - assert_eq!(matrix.col_mut(0).rows_and_values_mut(), ([].as_ref(), [].as_mut())); + assert_eq!( + matrix.col_mut(0).rows_and_values_mut(), + ([].as_ref(), [].as_mut()) + ); assert_eq!(matrix.col(1).nrows(), 2); assert_eq!(matrix.col(1).nnz(), 0); @@ -53,7 +56,10 @@ fn csc_matrix_valid_data() { assert_eq!(matrix.col_mut(1).row_indices(), &[]); assert_eq!(matrix.col_mut(1).values(), &[]); assert_eq!(matrix.col_mut(1).values_mut(), &[]); - assert_eq!(matrix.col_mut(1).rows_and_values_mut(), ([].as_ref(), [].as_mut())); + assert_eq!( + matrix.col_mut(1).rows_and_values_mut(), + ([].as_ref(), [].as_mut()) + ); assert_eq!(matrix.col(2).nrows(), 2); assert_eq!(matrix.col(2).nnz(), 0); @@ -64,7 +70,10 @@ fn csc_matrix_valid_data() { assert_eq!(matrix.col_mut(2).row_indices(), &[]); assert_eq!(matrix.col_mut(2).values(), &[]); assert_eq!(matrix.col_mut(2).values_mut(), &[]); - assert_eq!(matrix.col_mut(2).rows_and_values_mut(), ([].as_ref(), [].as_mut())); + assert_eq!( + matrix.col_mut(2).rows_and_values_mut(), + ([].as_ref(), [].as_mut()) + ); assert!(matrix.get_col(3).is_none()); assert!(matrix.get_col_mut(3).is_none()); @@ -81,11 +90,9 @@ fn csc_matrix_valid_data() { let offsets = vec![0, 2, 2, 5]; let indices = vec![0, 5, 1, 2, 3]; let values = vec![0, 1, 2, 3, 4]; - let mut matrix = CscMatrix::try_from_csc_data(6, - 3, - offsets.clone(), - indices.clone(), - values.clone()).unwrap(); + let mut matrix = + CscMatrix::try_from_csc_data(6, 3, offsets.clone(), indices.clone(), values.clone()) + .unwrap(); assert_eq!(matrix.nrows(), 6); assert_eq!(matrix.ncols(), 3); @@ -95,10 +102,20 @@ fn csc_matrix_valid_data() { assert_eq!(matrix.values(), &[0, 1, 2, 3, 4]); let expected_triplets = vec![(0, 0, 0), (5, 0, 1), (1, 2, 2), (2, 2, 3), (3, 2, 4)]; - assert_eq!(matrix.triplet_iter().map(|(i, j, v)| (i, j, *v)).collect::>(), - expected_triplets); - assert_eq!(matrix.triplet_iter_mut().map(|(i, j, v)| (i, j, *v)).collect::>(), - expected_triplets); + assert_eq!( + matrix + .triplet_iter() + .map(|(i, j, v)| (i, j, *v)) + .collect::>(), + expected_triplets + ); + assert_eq!( + matrix + .triplet_iter_mut() + .map(|(i, j, v)| (i, j, *v)) + .collect::>(), + expected_triplets + ); assert_eq!(matrix.col(0).nrows(), 6); assert_eq!(matrix.col(0).nnz(), 2); @@ -109,7 +126,10 @@ fn csc_matrix_valid_data() { assert_eq!(matrix.col_mut(0).row_indices(), &[0, 5]); assert_eq!(matrix.col_mut(0).values(), &[0, 1]); assert_eq!(matrix.col_mut(0).values_mut(), &[0, 1]); - assert_eq!(matrix.col_mut(0).rows_and_values_mut(), ([0, 5].as_ref(), [0, 1].as_mut())); + assert_eq!( + matrix.col_mut(0).rows_and_values_mut(), + ([0, 5].as_ref(), [0, 1].as_mut()) + ); assert_eq!(matrix.col(1).nrows(), 6); assert_eq!(matrix.col(1).nnz(), 0); @@ -120,7 +140,10 @@ fn csc_matrix_valid_data() { assert_eq!(matrix.col_mut(1).row_indices(), &[]); assert_eq!(matrix.col_mut(1).values(), &[]); assert_eq!(matrix.col_mut(1).values_mut(), &[]); - assert_eq!(matrix.col_mut(1).rows_and_values_mut(), ([].as_ref(), [].as_mut())); + assert_eq!( + matrix.col_mut(1).rows_and_values_mut(), + ([].as_ref(), [].as_mut()) + ); assert_eq!(matrix.col(2).nrows(), 6); assert_eq!(matrix.col(2).nnz(), 3); @@ -131,7 +154,10 @@ fn csc_matrix_valid_data() { assert_eq!(matrix.col_mut(2).row_indices(), &[1, 2, 3]); assert_eq!(matrix.col_mut(2).values(), &[2, 3, 4]); assert_eq!(matrix.col_mut(2).values_mut(), &[2, 3, 4]); - assert_eq!(matrix.col_mut(2).rows_and_values_mut(), ([1, 2, 3].as_ref(), [2, 3, 4].as_mut())); + assert_eq!( + matrix.col_mut(2).rows_and_values_mut(), + ([1, 2, 3].as_ref(), [2, 3, 4].as_mut()) + ); assert!(matrix.get_col(3).is_none()); assert!(matrix.get_col_mut(3).is_none()); @@ -146,11 +172,13 @@ fn csc_matrix_valid_data() { #[test] fn csc_matrix_try_from_invalid_csc_data() { - { // Empty offset array (invalid length) let matrix = CscMatrix::try_from_csc_data(0, 0, Vec::new(), Vec::new(), Vec::::new()); - assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + assert_eq!( + matrix.unwrap_err().kind(), + &SparseFormatErrorKind::InvalidStructure + ); } { @@ -160,7 +188,10 @@ fn csc_matrix_try_from_invalid_csc_data() { let values = vec![0, 1, 2, 3, 4]; let matrix = CscMatrix::try_from_csc_data(6, 3, offsets, indices, values); - assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + assert_eq!( + matrix.unwrap_err().kind(), + &SparseFormatErrorKind::InvalidStructure + ); } { @@ -169,7 +200,10 @@ fn csc_matrix_try_from_invalid_csc_data() { 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, indices, values); - assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + assert_eq!( + matrix.unwrap_err().kind(), + &SparseFormatErrorKind::InvalidStructure + ); } { @@ -178,7 +212,10 @@ fn csc_matrix_try_from_invalid_csc_data() { 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, indices, values); - assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + assert_eq!( + matrix.unwrap_err().kind(), + &SparseFormatErrorKind::InvalidStructure + ); } { @@ -187,7 +224,10 @@ fn csc_matrix_try_from_invalid_csc_data() { 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, indices, values); - assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + assert_eq!( + matrix.unwrap_err().kind(), + &SparseFormatErrorKind::InvalidStructure + ); } { @@ -196,7 +236,10 @@ fn csc_matrix_try_from_invalid_csc_data() { let indices = vec![0, 1, 2, 3, 4]; let values = vec![0, 1, 2, 3, 4]; let matrix = CscMatrix::try_from_csc_data(6, 3, offsets, indices, values); - assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + assert_eq!( + matrix.unwrap_err().kind(), + &SparseFormatErrorKind::InvalidStructure + ); } { @@ -205,7 +248,10 @@ fn csc_matrix_try_from_invalid_csc_data() { let indices = vec![0, 2, 3, 1, 4]; let values = vec![0, 1, 2, 3, 4]; let matrix = CscMatrix::try_from_csc_data(6, 3, offsets, indices, values); - assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + assert_eq!( + matrix.unwrap_err().kind(), + &SparseFormatErrorKind::InvalidStructure + ); } { @@ -214,7 +260,10 @@ fn csc_matrix_try_from_invalid_csc_data() { let indices = vec![0, 6, 1, 2, 3]; let values = vec![0, 1, 2, 3, 4]; let matrix = CscMatrix::try_from_csc_data(6, 3, offsets, indices, values); - assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::IndexOutOfBounds); + assert_eq!( + matrix.unwrap_err().kind(), + &SparseFormatErrorKind::IndexOutOfBounds + ); } { @@ -223,9 +272,11 @@ fn csc_matrix_try_from_invalid_csc_data() { let indices = vec![0, 5, 2, 2, 3]; let values = vec![0, 1, 2, 3, 4]; let matrix = CscMatrix::try_from_csc_data(6, 3, offsets, indices, values); - assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::DuplicateEntry); + assert_eq!( + matrix.unwrap_err().kind(), + &SparseFormatErrorKind::DuplicateEntry + ); } - } #[test] @@ -239,11 +290,7 @@ fn csc_disassemble_avoids_clone_when_owned() { let offsets_ptr = offsets.as_ptr(); let indices_ptr = indices.as_ptr(); let values_ptr = values.as_ptr(); - let matrix = CscMatrix::try_from_csc_data(6, - 3, - offsets, - indices, - values).unwrap(); + let matrix = CscMatrix::try_from_csc_data(6, 3, offsets, indices, values).unwrap(); let (offsets, indices, values) = matrix.disassemble(); assert_eq!(offsets.as_ptr(), offsets_ptr); @@ -328,4 +375,4 @@ proptest! { prop_assert_eq!(d_entries, csc_diagonal_entries); } -} \ No newline at end of file +} diff --git a/nalgebra-sparse/tests/unit_tests/csr.rs b/nalgebra-sparse/tests/unit_tests/csr.rs index 698fb5f1..2de93e51 100644 --- a/nalgebra-sparse/tests/unit_tests/csr.rs +++ b/nalgebra-sparse/tests/unit_tests/csr.rs @@ -1,6 +1,6 @@ +use nalgebra::DMatrix; use nalgebra_sparse::csr::CsrMatrix; use nalgebra_sparse::SparseFormatErrorKind; -use nalgebra::DMatrix; use proptest::prelude::*; use proptest::sample::subsequence; @@ -9,7 +9,6 @@ use crate::common::csr_strategy; use std::collections::HashSet; - #[test] fn csr_matrix_valid_data() { // Construct matrix from valid data and check that selected methods return results @@ -43,7 +42,10 @@ fn csr_matrix_valid_data() { assert_eq!(matrix.row_mut(0).col_indices(), &[]); assert_eq!(matrix.row_mut(0).values(), &[]); assert_eq!(matrix.row_mut(0).values_mut(), &[]); - assert_eq!(matrix.row_mut(0).cols_and_values_mut(), ([].as_ref(), [].as_mut())); + assert_eq!( + matrix.row_mut(0).cols_and_values_mut(), + ([].as_ref(), [].as_mut()) + ); assert_eq!(matrix.row(1).ncols(), 2); assert_eq!(matrix.row(1).nnz(), 0); @@ -54,7 +56,10 @@ fn csr_matrix_valid_data() { assert_eq!(matrix.row_mut(1).col_indices(), &[]); assert_eq!(matrix.row_mut(1).values(), &[]); assert_eq!(matrix.row_mut(1).values_mut(), &[]); - assert_eq!(matrix.row_mut(1).cols_and_values_mut(), ([].as_ref(), [].as_mut())); + assert_eq!( + matrix.row_mut(1).cols_and_values_mut(), + ([].as_ref(), [].as_mut()) + ); assert_eq!(matrix.row(2).ncols(), 2); assert_eq!(matrix.row(2).nnz(), 0); @@ -65,7 +70,10 @@ fn csr_matrix_valid_data() { assert_eq!(matrix.row_mut(2).col_indices(), &[]); assert_eq!(matrix.row_mut(2).values(), &[]); assert_eq!(matrix.row_mut(2).values_mut(), &[]); - assert_eq!(matrix.row_mut(2).cols_and_values_mut(), ([].as_ref(), [].as_mut())); + assert_eq!( + matrix.row_mut(2).cols_and_values_mut(), + ([].as_ref(), [].as_mut()) + ); assert!(matrix.get_row(3).is_none()); assert!(matrix.get_row_mut(3).is_none()); @@ -82,11 +90,9 @@ fn csr_matrix_valid_data() { let offsets = vec![0, 2, 2, 5]; let indices = vec![0, 5, 1, 2, 3]; let values = vec![0, 1, 2, 3, 4]; - let mut matrix = CsrMatrix::try_from_csr_data(3, - 6, - offsets.clone(), - indices.clone(), - values.clone()).unwrap(); + let mut matrix = + CsrMatrix::try_from_csr_data(3, 6, offsets.clone(), indices.clone(), values.clone()) + .unwrap(); assert_eq!(matrix.nrows(), 3); assert_eq!(matrix.ncols(), 6); @@ -96,10 +102,20 @@ fn csr_matrix_valid_data() { assert_eq!(matrix.values(), &[0, 1, 2, 3, 4]); let expected_triplets = vec![(0, 0, 0), (0, 5, 1), (2, 1, 2), (2, 2, 3), (2, 3, 4)]; - assert_eq!(matrix.triplet_iter().map(|(i, j, v)| (i, j, *v)).collect::>(), - expected_triplets); - assert_eq!(matrix.triplet_iter_mut().map(|(i, j, v)| (i, j, *v)).collect::>(), - expected_triplets); + assert_eq!( + matrix + .triplet_iter() + .map(|(i, j, v)| (i, j, *v)) + .collect::>(), + expected_triplets + ); + assert_eq!( + matrix + .triplet_iter_mut() + .map(|(i, j, v)| (i, j, *v)) + .collect::>(), + expected_triplets + ); assert_eq!(matrix.row(0).ncols(), 6); assert_eq!(matrix.row(0).nnz(), 2); @@ -110,7 +126,10 @@ fn csr_matrix_valid_data() { assert_eq!(matrix.row_mut(0).col_indices(), &[0, 5]); assert_eq!(matrix.row_mut(0).values(), &[0, 1]); assert_eq!(matrix.row_mut(0).values_mut(), &[0, 1]); - assert_eq!(matrix.row_mut(0).cols_and_values_mut(), ([0, 5].as_ref(), [0, 1].as_mut())); + assert_eq!( + matrix.row_mut(0).cols_and_values_mut(), + ([0, 5].as_ref(), [0, 1].as_mut()) + ); assert_eq!(matrix.row(1).ncols(), 6); assert_eq!(matrix.row(1).nnz(), 0); @@ -121,7 +140,10 @@ fn csr_matrix_valid_data() { assert_eq!(matrix.row_mut(1).col_indices(), &[]); assert_eq!(matrix.row_mut(1).values(), &[]); assert_eq!(matrix.row_mut(1).values_mut(), &[]); - assert_eq!(matrix.row_mut(1).cols_and_values_mut(), ([].as_ref(), [].as_mut())); + assert_eq!( + matrix.row_mut(1).cols_and_values_mut(), + ([].as_ref(), [].as_mut()) + ); assert_eq!(matrix.row(2).ncols(), 6); assert_eq!(matrix.row(2).nnz(), 3); @@ -132,7 +154,10 @@ fn csr_matrix_valid_data() { assert_eq!(matrix.row_mut(2).col_indices(), &[1, 2, 3]); assert_eq!(matrix.row_mut(2).values(), &[2, 3, 4]); assert_eq!(matrix.row_mut(2).values_mut(), &[2, 3, 4]); - assert_eq!(matrix.row_mut(2).cols_and_values_mut(), ([1, 2, 3].as_ref(), [2, 3, 4].as_mut())); + assert_eq!( + matrix.row_mut(2).cols_and_values_mut(), + ([1, 2, 3].as_ref(), [2, 3, 4].as_mut()) + ); assert!(matrix.get_row(3).is_none()); assert!(matrix.get_row_mut(3).is_none()); @@ -147,11 +172,13 @@ fn csr_matrix_valid_data() { #[test] fn csr_matrix_try_from_invalid_csr_data() { - { // Empty offset array (invalid length) let matrix = CsrMatrix::try_from_csr_data(0, 0, Vec::new(), Vec::new(), Vec::::new()); - assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + assert_eq!( + matrix.unwrap_err().kind(), + &SparseFormatErrorKind::InvalidStructure + ); } { @@ -161,7 +188,10 @@ fn csr_matrix_try_from_invalid_csr_data() { let values = vec![0, 1, 2, 3, 4]; let matrix = CsrMatrix::try_from_csr_data(3, 6, offsets, indices, values); - assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + assert_eq!( + matrix.unwrap_err().kind(), + &SparseFormatErrorKind::InvalidStructure + ); } { @@ -170,7 +200,10 @@ fn csr_matrix_try_from_invalid_csr_data() { 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, indices, values); - assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + assert_eq!( + matrix.unwrap_err().kind(), + &SparseFormatErrorKind::InvalidStructure + ); } { @@ -179,7 +212,10 @@ fn csr_matrix_try_from_invalid_csr_data() { 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, indices, values); - assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + assert_eq!( + matrix.unwrap_err().kind(), + &SparseFormatErrorKind::InvalidStructure + ); } { @@ -188,7 +224,10 @@ fn csr_matrix_try_from_invalid_csr_data() { 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, indices, values); - assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + assert_eq!( + matrix.unwrap_err().kind(), + &SparseFormatErrorKind::InvalidStructure + ); } { @@ -197,7 +236,10 @@ fn csr_matrix_try_from_invalid_csr_data() { let indices = vec![0, 1, 2, 3, 4]; let values = vec![0, 1, 2, 3, 4]; let matrix = CsrMatrix::try_from_csr_data(3, 6, offsets, indices, values); - assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + assert_eq!( + matrix.unwrap_err().kind(), + &SparseFormatErrorKind::InvalidStructure + ); } { @@ -206,7 +248,10 @@ fn csr_matrix_try_from_invalid_csr_data() { let indices = vec![0, 2, 3, 1, 4]; let values = vec![0, 1, 2, 3, 4]; let matrix = CsrMatrix::try_from_csr_data(3, 6, offsets, indices, values); - assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::InvalidStructure); + assert_eq!( + matrix.unwrap_err().kind(), + &SparseFormatErrorKind::InvalidStructure + ); } { @@ -215,7 +260,10 @@ fn csr_matrix_try_from_invalid_csr_data() { let indices = vec![0, 6, 1, 2, 3]; let values = vec![0, 1, 2, 3, 4]; let matrix = CsrMatrix::try_from_csr_data(3, 6, offsets, indices, values); - assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::IndexOutOfBounds); + assert_eq!( + matrix.unwrap_err().kind(), + &SparseFormatErrorKind::IndexOutOfBounds + ); } { @@ -224,9 +272,11 @@ fn csr_matrix_try_from_invalid_csr_data() { let indices = vec![0, 5, 2, 2, 3]; let values = vec![0, 1, 2, 3, 4]; let matrix = CsrMatrix::try_from_csr_data(3, 6, offsets, indices, values); - assert_eq!(matrix.unwrap_err().kind(), &SparseFormatErrorKind::DuplicateEntry); + assert_eq!( + matrix.unwrap_err().kind(), + &SparseFormatErrorKind::DuplicateEntry + ); } - } #[test] @@ -240,11 +290,7 @@ fn csr_disassemble_avoids_clone_when_owned() { let offsets_ptr = offsets.as_ptr(); let indices_ptr = indices.as_ptr(); let values_ptr = values.as_ptr(); - let matrix = CsrMatrix::try_from_csr_data(3, - 6, - offsets, - indices, - values).unwrap(); + let matrix = CsrMatrix::try_from_csr_data(3, 6, offsets, indices, values).unwrap(); let (offsets, indices, values) = matrix.disassemble(); assert_eq!(offsets.as_ptr(), offsets_ptr); @@ -329,4 +375,4 @@ proptest! { prop_assert_eq!(d_entries, csr_diagonal_entries); } -} \ No newline at end of file +} diff --git a/nalgebra-sparse/tests/unit_tests/mod.rs b/nalgebra-sparse/tests/unit_tests/mod.rs index a1f25715..ee2166dc 100644 --- a/nalgebra-sparse/tests/unit_tests/mod.rs +++ b/nalgebra-sparse/tests/unit_tests/mod.rs @@ -1,8 +1,8 @@ -mod coo; mod cholesky; +mod convert_serial; +mod coo; +mod csc; +mod csr; mod ops; mod pattern; -mod csr; -mod csc; -mod convert_serial; -mod proptest; \ No newline at end of file +mod proptest; diff --git a/nalgebra-sparse/tests/unit_tests/ops.rs b/nalgebra-sparse/tests/unit_tests/ops.rs index 4e789eb3..f2a02fd8 100644 --- a/nalgebra-sparse/tests/unit_tests/ops.rs +++ b/nalgebra-sparse/tests/unit_tests/ops.rs @@ -1,13 +1,19 @@ -use crate::common::{csc_strategy, csr_strategy, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ, PROPTEST_I32_VALUE_STRATEGY, non_zero_i32_value_strategy, value_strategy}; -use nalgebra_sparse::ops::serial::{spmm_csr_dense, spmm_csc_dense, spadd_pattern, spadd_csr_prealloc, spadd_csc_prealloc, spmm_csr_prealloc, spmm_csc_prealloc, spsolve_csc_lower_triangular, spmm_csr_pattern}; -use nalgebra_sparse::ops::{Op}; -use nalgebra_sparse::csr::CsrMatrix; +use crate::common::{ + csc_strategy, csr_strategy, non_zero_i32_value_strategy, value_strategy, + PROPTEST_I32_VALUE_STRATEGY, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ, +}; use nalgebra_sparse::csc::CscMatrix; -use nalgebra_sparse::proptest::{csc, csr, sparsity_pattern}; +use nalgebra_sparse::csr::CsrMatrix; +use nalgebra_sparse::ops::serial::{ + spadd_csc_prealloc, spadd_csr_prealloc, spadd_pattern, spmm_csc_dense, spmm_csc_prealloc, + spmm_csr_dense, spmm_csr_pattern, spmm_csr_prealloc, spsolve_csc_lower_triangular, +}; +use nalgebra_sparse::ops::Op; use nalgebra_sparse::pattern::SparsityPattern; +use nalgebra_sparse::proptest::{csc, csr, sparsity_pattern}; -use nalgebra::{DMatrix, Scalar, DMatrixSliceMut, DMatrixSlice}; use nalgebra::proptest::{matrix, vector}; +use nalgebra::{DMatrix, DMatrixSlice, DMatrixSliceMut, Scalar}; use proptest::prelude::*; @@ -17,19 +23,15 @@ use std::panic::catch_unwind; /// Represents the sparsity pattern of a CSR matrix as a dense matrix with 0/1 fn dense_csr_pattern(pattern: &SparsityPattern) -> DMatrix { - let boolean_csr = CsrMatrix::try_from_pattern_and_values( - pattern.clone(), - vec![1; pattern.nnz()]) - .unwrap(); + let boolean_csr = + CsrMatrix::try_from_pattern_and_values(pattern.clone(), vec![1; pattern.nnz()]).unwrap(); DMatrix::from(&boolean_csr) } /// Represents the sparsity pattern of a CSC matrix as a dense matrix with 0/1 fn dense_csc_pattern(pattern: &SparsityPattern) -> DMatrix { - let boolean_csc = CscMatrix::try_from_pattern_and_values( - pattern.clone(), - vec![1; pattern.nnz()]) - .unwrap(); + let boolean_csc = + CscMatrix::try_from_pattern_and_values(pattern.clone(), vec![1; pattern.nnz()]).unwrap(); DMatrix::from(&boolean_csc) } @@ -53,7 +55,7 @@ struct SpmmCscDenseArgs { /// Returns matrices C, A and B with compatible dimensions such that it can be used /// in an `spmm` operation `C = beta * C + alpha * trans(A) * trans(B)`. -fn spmm_csr_dense_args_strategy() -> impl Strategy> { +fn spmm_csr_dense_args_strategy() -> impl Strategy> { let max_nnz = PROPTEST_MAX_NNZ; let value_strategy = PROPTEST_I32_VALUE_STRATEGY; let c_rows = PROPTEST_MATRIX_DIM; @@ -62,14 +64,23 @@ fn spmm_csr_dense_args_strategy() -> impl Strategy> let trans_strategy = trans_strategy(); let c_matrix_strategy = matrix(value_strategy.clone(), c_rows, c_cols); - (c_matrix_strategy, common_dim, trans_strategy.clone(), trans_strategy.clone()) + ( + c_matrix_strategy, + common_dim, + trans_strategy.clone(), + trans_strategy.clone(), + ) .prop_flat_map(move |(c, common_dim, trans_a, trans_b)| { - let a_shape = - if trans_a { (common_dim, c.nrows()) } - else { (c.nrows(), common_dim) }; - let b_shape = - if trans_b { (c.ncols(), common_dim) } - else { (common_dim, c.ncols()) }; + let a_shape = if trans_a { + (common_dim, c.nrows()) + } else { + (c.nrows(), common_dim) + }; + let b_shape = if trans_b { + (c.ncols(), common_dim) + } else { + (common_dim, c.ncols()) + }; let a = csr(value_strategy.clone(), a_shape.0, a_shape.1, max_nnz); let b = matrix(value_strategy.clone(), b_shape.0, b_shape.1); @@ -78,30 +89,36 @@ fn spmm_csr_dense_args_strategy() -> impl Strategy> let beta = value_strategy.clone(); (Just(c), beta, alpha, Just(trans_a), a, Just(trans_b), b) - }).prop_map(|(c, beta, alpha, trans_a, a, trans_b, b)| { - SpmmCsrDenseArgs { + }) + .prop_map( + |(c, beta, alpha, trans_a, a, trans_b, b)| SpmmCsrDenseArgs { c, beta, alpha, - a: if trans_a { Op::Transpose(a) } else { Op::NoOp(a) }, - b: if trans_b { Op::Transpose(b) } else { Op::NoOp(b) }, - } - }) + a: if trans_a { + Op::Transpose(a) + } else { + Op::NoOp(a) + }, + b: if trans_b { + Op::Transpose(b) + } else { + Op::NoOp(b) + }, + }, + ) } /// Returns matrices C, A and B with compatible dimensions such that it can be used /// in an `spmm` operation `C = beta * C + alpha * trans(A) * trans(B)`. -fn spmm_csc_dense_args_strategy() -> impl Strategy> { - spmm_csr_dense_args_strategy() - .prop_map(|args| { - SpmmCscDenseArgs { - c: args.c, - beta: args.beta, - alpha: args.alpha, - a: args.a.map_same_op(|a| CscMatrix::from(&a)), - b: args.b - } - }) +fn spmm_csc_dense_args_strategy() -> impl Strategy> { + spmm_csr_dense_args_strategy().prop_map(|args| SpmmCscDenseArgs { + c: args.c, + beta: args.beta, + alpha: args.alpha, + a: args.a.map_same_op(|a| CscMatrix::from(&a)), + b: args.b, + }) } #[derive(Debug)] @@ -120,7 +137,7 @@ struct SpaddCscArgs { a: Op>, } -fn spadd_csr_prealloc_args_strategy() -> impl Strategy> { +fn spadd_csr_prealloc_args_strategy() -> impl Strategy> { let value_strategy = PROPTEST_I32_VALUE_STRATEGY; spadd_pattern_strategy() @@ -131,66 +148,83 @@ fn spadd_csr_prealloc_args_strategy() -> impl Strategy> let c_values = vec![value_strategy.clone(); c_pattern.nnz()]; let alpha = value_strategy.clone(); let beta = value_strategy.clone(); - (Just(c_pattern), Just(a_pattern), c_values, a_values, alpha, beta, trans_strategy()) - }).prop_map(|(c_pattern, a_pattern, c_values, a_values, alpha, beta, trans_a)| { - let c = CsrMatrix::try_from_pattern_and_values(c_pattern, c_values).unwrap(); - let a = CsrMatrix::try_from_pattern_and_values(a_pattern, a_values).unwrap(); - - let a = if trans_a { Op::Transpose(a.transpose()) } else { Op::NoOp(a) }; - SpaddCsrArgs { c, beta, alpha, a } + ( + Just(c_pattern), + Just(a_pattern), + c_values, + a_values, + alpha, + beta, + trans_strategy(), + ) }) + .prop_map( + |(c_pattern, a_pattern, c_values, a_values, alpha, beta, trans_a)| { + let c = CsrMatrix::try_from_pattern_and_values(c_pattern, c_values).unwrap(); + let a = CsrMatrix::try_from_pattern_and_values(a_pattern, a_values).unwrap(); + + let a = if trans_a { + Op::Transpose(a.transpose()) + } else { + Op::NoOp(a) + }; + SpaddCsrArgs { c, beta, alpha, a } + }, + ) } -fn spadd_csc_prealloc_args_strategy() -> impl Strategy> { - spadd_csr_prealloc_args_strategy() - .prop_map(|args| SpaddCscArgs { - c: CscMatrix::from(&args.c), - beta: args.beta, - alpha: args.alpha, - a: args.a.map_same_op(|a| CscMatrix::from(&a)) - }) +fn spadd_csc_prealloc_args_strategy() -> impl Strategy> { + spadd_csr_prealloc_args_strategy().prop_map(|args| SpaddCscArgs { + c: CscMatrix::from(&args.c), + beta: args.beta, + alpha: args.alpha, + a: args.a.map_same_op(|a| CscMatrix::from(&a)), + }) } -fn dense_strategy() -> impl Strategy> { - matrix(PROPTEST_I32_VALUE_STRATEGY, PROPTEST_MATRIX_DIM, PROPTEST_MATRIX_DIM) +fn dense_strategy() -> impl Strategy> { + matrix( + PROPTEST_I32_VALUE_STRATEGY, + PROPTEST_MATRIX_DIM, + PROPTEST_MATRIX_DIM, + ) } -fn trans_strategy() -> impl Strategy + Clone { +fn trans_strategy() -> impl Strategy + Clone { proptest::bool::ANY } /// Wraps the values of the given strategy in `Op`, producing both transposed and non-transposed /// values. -fn op_strategy(strategy: S) -> impl Strategy> { +fn op_strategy(strategy: S) -> impl Strategy> { let is_transposed = proptest::bool::ANY; - (strategy, is_transposed) - .prop_map(|(obj, is_trans)| if is_trans { + (strategy, is_transposed).prop_map(|(obj, is_trans)| { + if is_trans { Op::Transpose(obj) } else { Op::NoOp(obj) - }) + } + }) } -fn pattern_strategy() -> impl Strategy { +fn pattern_strategy() -> impl Strategy { sparsity_pattern(PROPTEST_MATRIX_DIM, PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ) } /// Constructs pairs (a, b) where a and b have the same dimensions -fn spadd_pattern_strategy() -> impl Strategy { - pattern_strategy() - .prop_flat_map(|a| { - let b = sparsity_pattern(a.major_dim(), a.minor_dim(), PROPTEST_MAX_NNZ); - (Just(a), b) - }) +fn spadd_pattern_strategy() -> impl Strategy { + pattern_strategy().prop_flat_map(|a| { + let b = sparsity_pattern(a.major_dim(), a.minor_dim(), PROPTEST_MAX_NNZ); + (Just(a), b) + }) } /// Constructs pairs (a, b) where a and b have compatible dimensions for a matrix product -fn spmm_csr_pattern_strategy() -> impl Strategy { - pattern_strategy() - .prop_flat_map(|a| { - let b = sparsity_pattern(a.minor_dim(), PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ); - (Just(a), b) - }) +fn spmm_csr_pattern_strategy() -> impl Strategy { + pattern_strategy().prop_flat_map(|a| { + let b = sparsity_pattern(a.minor_dim(), PROPTEST_MATRIX_DIM, PROPTEST_MAX_NNZ); + (Just(a), b) + }) } #[derive(Debug)] @@ -211,86 +245,98 @@ struct SpmmCscArgs { b: Op>, } -fn spmm_csr_prealloc_args_strategy() -> impl Strategy> { +fn spmm_csr_prealloc_args_strategy() -> impl Strategy> { spmm_csr_pattern_strategy() .prop_flat_map(|(a_pattern, b_pattern)| { let a_values = vec![PROPTEST_I32_VALUE_STRATEGY; a_pattern.nnz()]; let b_values = vec![PROPTEST_I32_VALUE_STRATEGY; b_pattern.nnz()]; let c_pattern = spmm_csr_pattern(&a_pattern, &b_pattern); let c_values = vec![PROPTEST_I32_VALUE_STRATEGY; c_pattern.nnz()]; - let a = a_values.prop_map(move |values| - CsrMatrix::try_from_pattern_and_values(a_pattern.clone(), values).unwrap()); - let b = b_values.prop_map(move |values| - CsrMatrix::try_from_pattern_and_values(b_pattern.clone(), values).unwrap()); - let c = c_values.prop_map(move |values| - CsrMatrix::try_from_pattern_and_values(c_pattern.clone(), values).unwrap()); + let a = a_values.prop_map(move |values| { + CsrMatrix::try_from_pattern_and_values(a_pattern.clone(), values).unwrap() + }); + let b = b_values.prop_map(move |values| { + CsrMatrix::try_from_pattern_and_values(b_pattern.clone(), values).unwrap() + }); + let c = c_values.prop_map(move |values| { + CsrMatrix::try_from_pattern_and_values(c_pattern.clone(), values).unwrap() + }); let alpha = PROPTEST_I32_VALUE_STRATEGY; let beta = PROPTEST_I32_VALUE_STRATEGY; (c, beta, alpha, trans_strategy(), a, trans_strategy(), b) }) - .prop_map(|(c, beta, alpha, trans_a, a, trans_b, b)| { - SpmmCsrArgs:: { + .prop_map( + |(c, beta, alpha, trans_a, a, trans_b, b)| SpmmCsrArgs:: { c, beta, alpha, - a: if trans_a { Op::Transpose(a.transpose()) } else { Op::NoOp(a) }, - b: if trans_b { Op::Transpose(b.transpose()) } else { Op::NoOp(b) } - } - }) + a: if trans_a { + Op::Transpose(a.transpose()) + } else { + Op::NoOp(a) + }, + b: if trans_b { + Op::Transpose(b.transpose()) + } else { + Op::NoOp(b) + }, + }, + ) } -fn spmm_csc_prealloc_args_strategy() -> impl Strategy> { +fn spmm_csc_prealloc_args_strategy() -> impl Strategy> { // Note: Converting from CSR is simple, but might be significantly slower than // writing a common implementation that can be shared between CSR and CSC args - spmm_csr_prealloc_args_strategy() - .prop_map(|args| { - SpmmCscArgs { - c: CscMatrix::from(&args.c), - beta: args.beta, - alpha: args.alpha, - a: args.a.map_same_op(|a| CscMatrix::from(&a)), - b: args.b.map_same_op(|b| CscMatrix::from(&b)) + spmm_csr_prealloc_args_strategy().prop_map(|args| SpmmCscArgs { + c: CscMatrix::from(&args.c), + beta: args.beta, + alpha: args.alpha, + a: args.a.map_same_op(|a| CscMatrix::from(&a)), + b: args.b.map_same_op(|b| CscMatrix::from(&b)), + }) +} + +fn csc_invertible_diagonal() -> impl Strategy> { + let non_zero_values = + value_strategy::().prop_filter("Only non-zeros values accepted", |x| x != &0.0); + + vector(non_zero_values, PROPTEST_MATRIX_DIM).prop_map(|d| { + let mut matrix = CscMatrix::identity(d.len()); + matrix.values_mut().clone_from_slice(&d.as_slice()); + matrix + }) +} + +fn csc_square_with_non_zero_diagonals() -> impl Strategy> { + csc_invertible_diagonal().prop_flat_map(|d| { + csc( + value_strategy::(), + d.nrows(), + d.nrows(), + PROPTEST_MAX_NNZ, + ) + .prop_map(move |mut c| { + for (i, j, v) in c.triplet_iter_mut() { + if i == j { + *v = 0.0; + } } + + // Return the sum of a matrix with zero diagonals and an invertible diagonal + // matrix + c + &d }) -} - -fn csc_invertible_diagonal() -> impl Strategy> { - let non_zero_values = value_strategy::() - .prop_filter("Only non-zeros values accepted", |x| x != &0.0); - - vector(non_zero_values, PROPTEST_MATRIX_DIM) - .prop_map(|d| { - let mut matrix = CscMatrix::identity(d.len()); - matrix.values_mut().clone_from_slice(&d.as_slice()); - matrix - }) -} - -fn csc_square_with_non_zero_diagonals() -> impl Strategy> { - csc_invertible_diagonal() - .prop_flat_map(|d| { - csc(value_strategy::(), d.nrows(), d.nrows(), PROPTEST_MAX_NNZ) - .prop_map(move |mut c| { - for (i, j, v) in c.triplet_iter_mut() { - if i == j { - *v = 0.0; - } - } - - // Return the sum of a matrix with zero diagonals and an invertible diagonal - // matrix - c + &d - }) - }) + }) } /// Helper function to help us call dense GEMM with our `Op` type -fn dense_gemm<'a>(beta: i32, - c: impl Into>, - alpha: i32, - a: Op>>, - b: Op>>) -{ +fn dense_gemm<'a>( + beta: i32, + c: impl Into>, + alpha: i32, + a: Op>>, + b: Op>>, +) { let mut c = c.into(); let a = a.convert(); let b = b.convert(); @@ -300,7 +346,7 @@ fn dense_gemm<'a>(beta: i32, (NoOp(a), NoOp(b)) => c.gemm(alpha, &a, &b, beta), (Transpose(a), NoOp(b)) => c.gemm(alpha, &a.transpose(), &b, beta), (NoOp(a), Transpose(b)) => c.gemm(alpha, &a, &b.transpose(), beta), - (Transpose(a), Transpose(b)) => c.gemm(alpha, &a.transpose(), &b.transpose(), beta) + (Transpose(a), Transpose(b)) => c.gemm(alpha, &a.transpose(), &b.transpose(), beta), } } @@ -1186,4 +1232,4 @@ proptest! { prop_assert_matrix_eq!(&a_lower.transpose() * &x, &b, comp = abs, tol = 1e-4); } -} \ No newline at end of file +} diff --git a/nalgebra-sparse/tests/unit_tests/pattern.rs b/nalgebra-sparse/tests/unit_tests/pattern.rs index ee6b2328..310cffae 100644 --- a/nalgebra-sparse/tests/unit_tests/pattern.rs +++ b/nalgebra-sparse/tests/unit_tests/pattern.rs @@ -7,11 +7,9 @@ fn sparsity_pattern_valid_data() { { // 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(); + let pattern = + SparsityPattern::try_from_offsets_and_indices(3, 2, vec![0, 0, 0, 0], Vec::new()) + .unwrap(); assert_eq!(pattern.major_dim(), 3); assert_eq!(pattern.minor_dim(), 2); @@ -36,7 +34,7 @@ fn sparsity_pattern_valid_data() { let indices = vec![0, 5, 1, 2, 3]; let pattern = SparsityPattern::try_from_offsets_and_indices(3, 6, offsets.clone(), indices.clone()) - .unwrap(); + .unwrap(); assert_eq!(pattern.major_dim(), 3); assert_eq!(pattern.minor_dim(), 6); @@ -46,8 +44,10 @@ fn sparsity_pattern_valid_data() { assert_eq!(pattern.lane(0), &[0, 5]); assert_eq!(pattern.lane(1), &[]); assert_eq!(pattern.lane(2), &[1, 2, 3]); - assert_eq!(pattern.entries().collect::>(), - vec![(0, 0), (0, 5), (2, 1), (2, 2), (2, 3)]); + assert_eq!( + pattern.entries().collect::>(), + vec![(0, 0), (0, 5), (2, 1), (2, 2), (2, 3)] + ); let (offsets2, indices2) = pattern.disassemble(); assert_eq!(offsets2, offsets); @@ -60,7 +60,10 @@ fn sparsity_pattern_try_from_invalid_data() { { // Empty offset array (invalid length) let pattern = SparsityPattern::try_from_offsets_and_indices(0, 0, Vec::new(), Vec::new()); - assert_eq!(pattern, Err(SparsityPatternFormatError::InvalidOffsetArrayLength)); + assert_eq!( + pattern, + Err(SparsityPatternFormatError::InvalidOffsetArrayLength) + ); } { @@ -69,7 +72,10 @@ fn sparsity_pattern_try_from_invalid_data() { let indices = vec![0, 1, 2, 3, 5]; let pattern = SparsityPattern::try_from_offsets_and_indices(3, 6, offsets, indices); - assert!(matches!(pattern, Err(SparsityPatternFormatError::InvalidOffsetArrayLength))); + assert!(matches!( + pattern, + Err(SparsityPatternFormatError::InvalidOffsetArrayLength) + )); } { @@ -77,7 +83,10 @@ fn sparsity_pattern_try_from_invalid_data() { let offsets = vec![1, 2, 2, 5]; let indices = vec![0, 5, 1, 2, 3]; let pattern = SparsityPattern::try_from_offsets_and_indices(3, 6, offsets, indices); - assert!(matches!(pattern, Err(SparsityPatternFormatError::InvalidOffsetFirstLast))); + assert!(matches!( + pattern, + Err(SparsityPatternFormatError::InvalidOffsetFirstLast) + )); } { @@ -85,7 +94,10 @@ fn sparsity_pattern_try_from_invalid_data() { let offsets = vec![0, 2, 2, 4]; let indices = vec![0, 5, 1, 2, 3]; let pattern = SparsityPattern::try_from_offsets_and_indices(3, 6, offsets, indices); - assert!(matches!(pattern, Err(SparsityPatternFormatError::InvalidOffsetFirstLast))); + assert!(matches!( + pattern, + Err(SparsityPatternFormatError::InvalidOffsetFirstLast) + )); } { @@ -93,7 +105,10 @@ fn sparsity_pattern_try_from_invalid_data() { let offsets = vec![0, 2, 2]; let indices = vec![0, 5, 1, 2, 3]; let pattern = SparsityPattern::try_from_offsets_and_indices(3, 6, offsets, indices); - assert!(matches!(pattern, Err(SparsityPatternFormatError::InvalidOffsetArrayLength))); + assert!(matches!( + pattern, + Err(SparsityPatternFormatError::InvalidOffsetArrayLength) + )); } { @@ -101,7 +116,10 @@ fn sparsity_pattern_try_from_invalid_data() { let offsets = vec![0, 3, 2, 5]; let indices = vec![0, 1, 2, 3, 4]; let pattern = SparsityPattern::try_from_offsets_and_indices(3, 6, offsets, indices); - assert_eq!(pattern, Err(SparsityPatternFormatError::NonmonotonicOffsets)); + assert_eq!( + pattern, + Err(SparsityPatternFormatError::NonmonotonicOffsets) + ); } { @@ -109,7 +127,10 @@ fn sparsity_pattern_try_from_invalid_data() { let offsets = vec![0, 2, 2, 5]; let indices = vec![0, 2, 3, 1, 4]; let pattern = SparsityPattern::try_from_offsets_and_indices(3, 6, offsets, indices); - assert_eq!(pattern, Err(SparsityPatternFormatError::NonmonotonicMinorIndices)); + assert_eq!( + pattern, + Err(SparsityPatternFormatError::NonmonotonicMinorIndices) + ); } { @@ -117,7 +138,10 @@ fn sparsity_pattern_try_from_invalid_data() { let offsets = vec![0, 2, 2, 5]; let indices = vec![0, 6, 1, 2, 3]; let pattern = SparsityPattern::try_from_offsets_and_indices(3, 6, offsets, indices); - assert_eq!(pattern, Err(SparsityPatternFormatError::MinorIndexOutOfBounds)); + assert_eq!( + pattern, + Err(SparsityPatternFormatError::MinorIndexOutOfBounds) + ); } { @@ -127,4 +151,4 @@ fn sparsity_pattern_try_from_invalid_data() { let pattern = SparsityPattern::try_from_offsets_and_indices(3, 6, offsets, indices); assert_eq!(pattern, Err(SparsityPatternFormatError::DuplicateEntry)); } -} \ No newline at end of file +} diff --git a/nalgebra-sparse/tests/unit_tests/proptest.rs b/nalgebra-sparse/tests/unit_tests/proptest.rs index 46bc5130..0116d25e 100644 --- a/nalgebra-sparse/tests/unit_tests/proptest.rs +++ b/nalgebra-sparse/tests/unit_tests/proptest.rs @@ -6,25 +6,27 @@ fn coo_no_duplicates_generates_admissible_matrices() { #[cfg(feature = "slow-tests")] mod slow { - use nalgebra_sparse::proptest::{coo_with_duplicates, coo_no_duplicates, csr, csc, sparsity_pattern}; use nalgebra::DMatrix; + use nalgebra_sparse::proptest::{ + coo_no_duplicates, coo_with_duplicates, csc, csr, sparsity_pattern, + }; - use proptest::test_runner::TestRunner; - use proptest::strategy::ValueTree; use itertools::Itertools; + use proptest::strategy::ValueTree; + use proptest::test_runner::TestRunner; use proptest::prelude::*; + use nalgebra_sparse::csr::CsrMatrix; use std::collections::HashSet; use std::iter::repeat; use std::ops::RangeInclusive; - use nalgebra_sparse::csr::CsrMatrix; - fn generate_all_possible_matrices(value_range: RangeInclusive, - rows_range: RangeInclusive, - cols_range: RangeInclusive) - -> HashSet> - { + fn generate_all_possible_matrices( + value_range: RangeInclusive, + rows_range: RangeInclusive, + cols_range: RangeInclusive, + ) -> HashSet> { // Enumerate all possible combinations let mut all_combinations = HashSet::new(); for nrows in rows_range { @@ -48,7 +50,11 @@ mod slow { .take(n_values) .multi_cartesian_product(); for matrix_values in values_iter { - all_combinations.insert(DMatrix::from_row_slice(nrows, ncols, &matrix_values)); + all_combinations.insert(DMatrix::from_row_slice( + nrows, + ncols, + &matrix_values, + )); } } } @@ -80,12 +86,14 @@ mod slow { // Enumerate all possible combinations let all_combinations = generate_all_possible_matrices(values, rows, cols); - let visited_combinations = sample_matrix_output_space(strategy, - &mut runner, - num_generated_matrices); + let visited_combinations = + sample_matrix_output_space(strategy, &mut runner, num_generated_matrices); assert_eq!(visited_combinations.len(), all_combinations.len()); - assert_eq!(visited_combinations, all_combinations, "Did not sample all possible values."); + assert_eq!( + visited_combinations, all_combinations, + "Did not sample all possible values." + ); } #[cfg(feature = "slow-tests")] @@ -113,9 +121,8 @@ mod slow { // `coo_with_duplicates`) let all_combinations = generate_all_possible_matrices(values, rows, cols); - let visited_combinations = sample_matrix_output_space(strategy, - &mut runner, - num_generated_matrices); + let visited_combinations = + sample_matrix_output_space(strategy, &mut runner, num_generated_matrices); // Here we cannot verify that the set of visited combinations is *equal* to // all possible outcomes with the given constraints, however the @@ -143,12 +150,14 @@ mod slow { let all_combinations = generate_all_possible_matrices(values, rows, cols); - let visited_combinations = sample_matrix_output_space(strategy, - &mut runner, - num_generated_matrices); + let visited_combinations = + sample_matrix_output_space(strategy, &mut runner, num_generated_matrices); assert_eq!(visited_combinations.len(), all_combinations.len()); - assert_eq!(visited_combinations, all_combinations, "Did not sample all possible values."); + assert_eq!( + visited_combinations, all_combinations, + "Did not sample all possible values." + ); } #[cfg(feature = "slow-tests")] @@ -169,12 +178,14 @@ mod slow { let all_combinations = generate_all_possible_matrices(values, rows, cols); - let visited_combinations = sample_matrix_output_space(strategy, - &mut runner, - num_generated_matrices); + let visited_combinations = + sample_matrix_output_space(strategy, &mut runner, num_generated_matrices); assert_eq!(visited_combinations.len(), all_combinations.len()); - assert_eq!(visited_combinations, all_combinations, "Did not sample all possible values."); + assert_eq!( + visited_combinations, all_combinations, + "Did not sample all possible values." + ); } #[cfg(feature = "slow-tests")] @@ -206,13 +217,14 @@ mod slow { assert_eq!(visited_patterns, all_possible_patterns); } - fn sample_matrix_output_space(strategy: S, - runner: &mut TestRunner, - num_samples: usize) - -> HashSet> + fn sample_matrix_output_space( + strategy: S, + runner: &mut TestRunner, + num_samples: usize, + ) -> HashSet> where S: Strategy, - DMatrix: for<'b> From<&'b S::Value> + DMatrix: for<'b> From<&'b S::Value>, { sample_strategy(strategy, runner) .take(num_samples) @@ -220,8 +232,10 @@ mod slow { .collect() } - fn sample_strategy<'a, S: 'a + Strategy>(strategy: S, runner: &'a mut TestRunner) - -> impl 'a + Iterator { + fn sample_strategy<'a, S: 'a + Strategy>( + strategy: S, + runner: &'a mut TestRunner, + ) -> impl 'a + Iterator { repeat(()).map(move |_| { let tree = strategy .new_tree(runner) @@ -230,4 +244,4 @@ mod slow { value }) } -} \ No newline at end of file +} From e7975ce09addfce8b3c9d91e83fbb074e11bbfed Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 25 Jan 2021 17:40:50 +0100 Subject: [PATCH 093/183] Rebase and update nalgebra version for nalgebra-sparse --- nalgebra-sparse/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nalgebra-sparse/Cargo.toml b/nalgebra-sparse/Cargo.toml index 27d90a90..2ff0ec88 100644 --- a/nalgebra-sparse/Cargo.toml +++ b/nalgebra-sparse/Cargo.toml @@ -12,7 +12,7 @@ compare = [ "matrixcompare-core" ] slow-tests = [] [dependencies] -nalgebra = { version="0.23", path = "../" } +nalgebra = { version="0.24", path = "../" } num-traits = { version = "0.2", default-features = false } proptest = { version = "0.10", optional = true } matrixcompare-core = { version = "0.1.0", optional = true } @@ -20,7 +20,7 @@ matrixcompare-core = { version = "0.1.0", optional = true } [dev-dependencies] itertools = "0.9" matrixcompare = { version = "0.2.0", features = [ "proptest-support" ] } -nalgebra = { version="0.23", path = "../", features = ["compare"] } +nalgebra = { version="0.24", path = "../", features = ["compare"] } [package.metadata.docs.rs] # Enable certain features when building docs for docs.rs From 7bef417f9990f8f49405d3d44d9e79dd8bc319f4 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 26 Jan 2021 08:49:07 +0100 Subject: [PATCH 094/183] Use nalgebra/proptest-support instead of /proptest in nalgebra-sparse --- nalgebra-sparse/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nalgebra-sparse/Cargo.toml b/nalgebra-sparse/Cargo.toml index 2ff0ec88..bca07280 100644 --- a/nalgebra-sparse/Cargo.toml +++ b/nalgebra-sparse/Cargo.toml @@ -5,7 +5,7 @@ authors = [ "Andreas Longva", "Sébastien Crozet " ] edition = "2018" [features] -proptest-support = ["proptest", "nalgebra/proptest"] +proptest-support = ["proptest", "nalgebra/proptest-support"] compare = [ "matrixcompare-core" ] # Enable to enable running some tests that take a lot of time to run From bda8207ffd4900d67f7c1ca776390c1289609495 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 26 Jan 2021 09:28:15 +0100 Subject: [PATCH 095/183] Rename to_value to into_value (clippy suggestion) --- nalgebra-sparse/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index 04b93091..1de835c7 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -226,7 +226,7 @@ impl<'a, T: Clone + Zero> SparseEntry<'a, T> { /// /// Either clones the underlying reference or returns zero if the entry is not explicitly /// stored. - pub fn to_value(self) -> T { + pub fn into_value(self) -> T { match self { SparseEntry::NonZero(value) => value.clone(), SparseEntry::Zero => T::zero(), @@ -253,7 +253,7 @@ impl<'a, T: Clone + Zero> SparseEntryMut<'a, T> { /// /// Either clones the underlying reference or returns zero if the entry is not explicitly /// stored. - pub fn to_value(self) -> T { + pub fn into_value(self) -> T { match self { SparseEntryMut::NonZero(value) => value.clone(), SparseEntryMut::Zero => T::zero(), From 86aeed6a09cb943f25f7ae209d37be55925a73da Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Tue, 26 Jan 2021 10:03:39 +0100 Subject: [PATCH 096/183] Test nalgebra-sparse in CI --- .circleci/config.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6d293d37..e5be1145 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -61,6 +61,15 @@ jobs: - run: name: test nalgebra-glm command: cargo test -p nalgebra-glm --features arbitrary --features serde-serialize --features abomonation-serialize --features sparse --features debug --features io --features compare --features libm --features slow-tests + - run: + 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 + command: PROPTEST_CASES=10000 cargo test --manifest-path=nalgebra-sparse/Cargo.toml --features compare,proptest-support + - run: + name: test nalgebra-sparse (slow tests) + # Unfortunately, the "slow-tests" take so much time that we need to run them with --release + command: PROPTEST_CASES=10000 cargo test --release --manifest-path=nalgebra-sparse/Cargo.toml --features compare,proptest-support,slow-tests slow build-wasm: executor: rust-executor steps: From 12c259f0b4d7c2e885586891240642b0958e9c01 Mon Sep 17 00:00:00 2001 From: Terence Date: Thu, 28 Jan 2021 17:25:32 -0500 Subject: [PATCH 097/183] Implement additional `DualQuaternion` ops and `UnitDualQuaternion` This implements `UnitDualQuaternion` as an alternative to `Isometry3` for representing 3D isometries, which also provides the `sclerp` operation which can be used to perform screw-linear interpolation between two unit dual quaternions. --- src/geometry/dual_quaternion.rs | 815 ++++++++++++++++++- src/geometry/dual_quaternion_alga.rs | 316 +++++++ src/geometry/dual_quaternion_construction.rs | 159 +++- src/geometry/dual_quaternion_conversion.rs | 186 +++++ src/geometry/dual_quaternion_ops.rs | 786 +++++++++++++++++- src/geometry/isometry_conversion.rs | 28 +- src/geometry/mod.rs | 3 + src/geometry/quaternion_conversion.rs | 25 +- src/geometry/rotation_conversion.rs | 28 +- src/geometry/translation_conversion.rs | 26 + 10 files changed, 2329 insertions(+), 43 deletions(-) create mode 100644 src/geometry/dual_quaternion_alga.rs create mode 100644 src/geometry/dual_quaternion_conversion.rs diff --git a/src/geometry/dual_quaternion.rs b/src/geometry/dual_quaternion.rs index ce9f7284..63c5fa03 100644 --- a/src/geometry/dual_quaternion.rs +++ b/src/geometry/dual_quaternion.rs @@ -1,7 +1,14 @@ -use crate::{Quaternion, SimdRealField}; +use std::fmt; +use approx::{AbsDiffEq, RelativeEq, UlpsEq}; +use crate::{ + Quaternion, SimdRealField, VectorN, U8, Vector3, Point3, Isometry3, Unit, Matrix4, + Translation3, UnitQuaternion, Scalar, Normed +}; #[cfg(feature = "serde-serialize")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use simba::scalar::{ClosedNeg, RealField}; + /// A dual quaternion. /// /// # Indexing @@ -77,8 +84,147 @@ where /// relative_eq!(dq.real.norm(), 1.0); /// ``` #[inline] - pub fn normalize_mut(&mut self) { - *self = self.normalize(); + pub fn normalize_mut(&mut self) -> N { + let real_norm = self.real.norm(); + self.real /= real_norm; + self.dual /= real_norm; + real_norm + } + + /// The conjugate of this dual quaternion, containing the conjugate of + /// the real and imaginary parts.. + /// + /// # Example + /// ``` + /// # use nalgebra::{DualQuaternion, Quaternion}; + /// let real = Quaternion::new(1.0, 2.0, 3.0, 4.0); + /// let dual = Quaternion::new(5.0, 6.0, 7.0, 8.0); + /// let dq = DualQuaternion::from_real_and_dual(real, dual); + /// + /// let conj = dq.conjugate(); + /// assert!(conj.real.i == -2.0 && conj.real.j == -3.0 && conj.real.k == -4.0); + /// assert!(conj.real.w == 1.0); + /// assert!(conj.dual.i == -6.0 && conj.dual.j == -7.0 && conj.dual.k == -8.0); + /// assert!(conj.dual.w == 5.0); + /// ``` + #[inline] + #[must_use = "Did you mean to use conjugate_mut()?"] + pub fn conjugate(&self) -> Self { + Self::from_real_and_dual(self.real.conjugate(), self.dual.conjugate()) + } + + /// Replaces this quaternion by its conjugate. + /// + /// # Example + /// ``` + /// # use nalgebra::{DualQuaternion, Quaternion}; + /// let real = Quaternion::new(1.0, 2.0, 3.0, 4.0); + /// let dual = Quaternion::new(5.0, 6.0, 7.0, 8.0); + /// let mut dq = DualQuaternion::from_real_and_dual(real, dual); + /// + /// dq.conjugate_mut(); + /// assert!(dq.real.i == -2.0 && dq.real.j == -3.0 && dq.real.k == -4.0); + /// assert!(dq.real.w == 1.0); + /// assert!(dq.dual.i == -6.0 && dq.dual.j == -7.0 && dq.dual.k == -8.0); + /// assert!(dq.dual.w == 5.0); + /// ``` + #[inline] + pub fn conjugate_mut(&mut self) { + self.real.conjugate_mut(); + self.dual.conjugate_mut(); + } + + /// Inverts this dual quaternion if it is not zero. + /// + /// # Example + /// ``` + /// # #[macro_use] extern crate approx; + /// # use nalgebra::{DualQuaternion, Quaternion}; + /// let real = Quaternion::new(1.0, 2.0, 3.0, 4.0); + /// let dual = Quaternion::new(5.0, 6.0, 7.0, 8.0); + /// let dq = DualQuaternion::from_real_and_dual(real, dual); + /// let inverse = dq.try_inverse(); + /// + /// assert!(inverse.is_some()); + /// assert_relative_eq!(inverse.unwrap() * dq, DualQuaternion::identity()); + /// + /// //Non-invertible case + /// let zero = Quaternion::new(0.0, 0.0, 0.0, 0.0); + /// let dq = DualQuaternion::from_real_and_dual(zero, zero); + /// let inverse = dq.try_inverse(); + /// + /// assert!(inverse.is_none()); + /// ``` + #[inline] + #[must_use = "Did you mean to use try_inverse_mut()?"] + pub fn try_inverse(&self) -> Option + where + N: RealField, + { + let mut res = *self; + if res.try_inverse_mut() { + Some(res) + } else { + None + } + } + + /// Inverts this dual quaternion in-place if it is not zero. + /// + /// # Example + /// ``` + /// # #[macro_use] extern crate approx; + /// # use nalgebra::{DualQuaternion, Quaternion}; + /// let real = Quaternion::new(1.0, 2.0, 3.0, 4.0); + /// let dual = Quaternion::new(5.0, 6.0, 7.0, 8.0); + /// let dq = DualQuaternion::from_real_and_dual(real, dual); + /// let mut dq_inverse = dq; + /// dq_inverse.try_inverse_mut(); + /// + /// assert_relative_eq!(dq_inverse * dq, DualQuaternion::identity()); + /// + /// //Non-invertible case + /// let zero = Quaternion::new(0.0, 0.0, 0.0, 0.0); + /// let mut dq = DualQuaternion::from_real_and_dual(zero, zero); + /// assert!(!dq.try_inverse_mut()); + /// ``` + #[inline] + pub fn try_inverse_mut(&mut self) -> bool + where + N: RealField, + { + let inverted = self.real.try_inverse_mut(); + if inverted { + self.dual = -self.real * self.dual * self.real; + true + } else { + false + } + } + + /// Linear interpolation between two dual quaternions. + /// + /// Computes `self * (1 - t) + other * t`. + /// + /// # Example + /// ``` + /// # use nalgebra::{DualQuaternion, Quaternion}; + /// let dq1 = DualQuaternion::from_real_and_dual( + /// Quaternion::new(1.0, 0.0, 0.0, 4.0), + /// Quaternion::new(0.0, 2.0, 0.0, 0.0) + /// ); + /// let dq2 = DualQuaternion::from_real_and_dual( + /// Quaternion::new(2.0, 0.0, 1.0, 0.0), + /// Quaternion::new(0.0, 2.0, 0.0, 0.0) + /// ); + /// assert_eq!(dq1.lerp(&dq2, 0.25), DualQuaternion::from_real_and_dual( + /// Quaternion::new(1.25, 0.0, 0.25, 3.0), + /// Quaternion::new(0.0, 2.0, 0.0, 0.0) + /// )); + /// ``` + #[inline] + pub fn lerp(&self, other: &Self, t: N) -> Self { + self * (N::one() - t) + other * t } } @@ -114,3 +260,666 @@ where }) } } + +impl DualQuaternion { + fn to_vector(&self) -> VectorN { + self.as_ref().clone().into() + } +} + +impl> AbsDiffEq for DualQuaternion { + type Epsilon = N; + + #[inline] + fn default_epsilon() -> Self::Epsilon { + N::default_epsilon() + } + + #[inline] + fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool { + self.to_vector().abs_diff_eq(&other.to_vector(), epsilon) || + // Account for the double-covering of S², i.e. q = -q + self.to_vector().iter().zip(other.to_vector().iter()).all(|(a, b)| a.abs_diff_eq(&-*b, epsilon)) + } +} + +impl> RelativeEq for DualQuaternion { + #[inline] + fn default_max_relative() -> Self::Epsilon { + N::default_max_relative() + } + + #[inline] + fn relative_eq( + &self, + other: &Self, + epsilon: Self::Epsilon, + max_relative: Self::Epsilon, + ) -> bool { + self.to_vector().relative_eq(&other.to_vector(), epsilon, max_relative) || + // Account for the double-covering of S², i.e. q = -q + self.to_vector().iter().zip(other.to_vector().iter()).all(|(a, b)| a.relative_eq(&-*b, epsilon, max_relative)) + } +} + +impl> UlpsEq for DualQuaternion { + #[inline] + fn default_max_ulps() -> u32 { + N::default_max_ulps() + } + + #[inline] + fn ulps_eq(&self, other: &Self, epsilon: Self::Epsilon, max_ulps: u32) -> bool { + self.to_vector().ulps_eq(&other.to_vector(), epsilon, max_ulps) || + // Account for the double-covering of S², i.e. q = -q. + self.to_vector().iter().zip(other.to_vector().iter()).all(|(a, b)| a.ulps_eq(&-*b, epsilon, max_ulps)) + } +} + +/// A unit quaternions. May be used to represent a rotation. +pub type UnitDualQuaternion = Unit>; + +impl PartialEq for UnitDualQuaternion { + #[inline] + fn eq(&self, rhs: &Self) -> bool { + self.as_ref().eq(rhs.as_ref()) + } +} + +impl Eq for UnitDualQuaternion {} + +impl Normed for DualQuaternion { + type Norm = N::SimdRealField; + + #[inline] + fn norm(&self) -> N::SimdRealField { + self.real.norm() + } + + #[inline] + fn norm_squared(&self) -> N::SimdRealField { + self.real.norm_squared() + } + + #[inline] + fn scale_mut(&mut self, n: Self::Norm) { + self.real.scale_mut(n); + self.dual.scale_mut(n); + } + + #[inline] + fn unscale_mut(&mut self, n: Self::Norm) { + self.real.unscale_mut(n); + self.dual.unscale_mut(n); + } +} + +impl UnitDualQuaternion +where + N::Element: SimdRealField, +{ + /// The underlying dual quaternion. + /// + /// Same as `self.as_ref()`. + /// + /// # Example + /// ``` + /// # use nalgebra::{DualQuaternion, UnitDualQuaternion, Quaternion}; + /// let id = UnitDualQuaternion::identity(); + /// assert_eq!(*id.dual_quaternion(), DualQuaternion::from_real_and_dual( + /// Quaternion::new(1.0, 0.0, 0.0, 0.0), + /// Quaternion::new(0.0, 0.0, 0.0, 0.0) + /// )); + /// ``` + #[inline] + pub fn dual_quaternion(&self) -> &DualQuaternion { + self.as_ref() + } + + /// Compute the conjugate of this unit quaternion. + /// + /// # Example + /// ``` + /// # use nalgebra::{UnitDualQuaternion, DualQuaternion, Quaternion}; + /// let qr = Quaternion::new(1.0, 2.0, 3.0, 4.0); + /// let qd = Quaternion::new(5.0, 6.0, 7.0, 8.0); + /// let unit = UnitDualQuaternion::new_normalize( + /// DualQuaternion::from_real_and_dual(qr, qd) + /// ); + /// let conj = unit.conjugate(); + /// assert_eq!(conj.real, unit.real.conjugate()); + /// assert_eq!(conj.dual, unit.dual.conjugate()); + /// ``` + #[inline] + #[must_use = "Did you mean to use conjugate_mut()?"] + pub fn conjugate(&self) -> Self { + Self::new_unchecked(self.as_ref().conjugate()) + } + + /// Compute the conjugate of this unit quaternion in-place. + /// + /// # Example + /// ``` + /// # use nalgebra::{UnitDualQuaternion, DualQuaternion, Quaternion}; + /// let qr = Quaternion::new(1.0, 2.0, 3.0, 4.0); + /// let qd = Quaternion::new(5.0, 6.0, 7.0, 8.0); + /// let unit = UnitDualQuaternion::new_normalize( + /// DualQuaternion::from_real_and_dual(qr, qd) + /// ); + /// let mut conj = unit.clone(); + /// conj.conjugate_mut(); + /// assert_eq!(conj.as_ref().real, unit.as_ref().real.conjugate()); + /// assert_eq!(conj.as_ref().dual, unit.as_ref().dual.conjugate()); + /// ``` + #[inline] + pub fn conjugate_mut(&mut self) { + self.as_mut_unchecked().conjugate_mut() + } + + /// Inverts this dual quaternion if it is not zero. + /// + /// # Example + /// ``` + /// # #[macro_use] extern crate approx; + /// # use nalgebra::{UnitDualQuaternion, Quaternion, DualQuaternion}; + /// let qr = Quaternion::new(1.0, 2.0, 3.0, 4.0); + /// let qd = Quaternion::new(5.0, 6.0, 7.0, 8.0); + /// let unit = UnitDualQuaternion::new_normalize(DualQuaternion::from_real_and_dual(qr, qd)); + /// let inv = unit.inverse(); + /// assert_relative_eq!(unit * inv, UnitDualQuaternion::identity(), epsilon = 1.0e-6); + /// assert_relative_eq!(inv * unit, UnitDualQuaternion::identity(), epsilon = 1.0e-6); + /// ``` + #[inline] + #[must_use = "Did you mean to use inverse_mut()?"] + pub fn inverse(&self) -> Self { + let real = Unit::new_unchecked(self.as_ref().real).inverse().into_inner(); + let dual = -real * self.as_ref().dual * real; + UnitDualQuaternion::new_unchecked(DualQuaternion { real, dual }) + } + + /// Inverts this dual quaternion in place if it is not zero. + /// + /// # Example + /// ``` + /// # #[macro_use] extern crate approx; + /// # use nalgebra::{UnitDualQuaternion, Quaternion, DualQuaternion}; + /// let qr = Quaternion::new(1.0, 2.0, 3.0, 4.0); + /// let qd = Quaternion::new(5.0, 6.0, 7.0, 8.0); + /// let unit = UnitDualQuaternion::new_normalize(DualQuaternion::from_real_and_dual(qr, qd)); + /// let mut inv = unit.clone(); + /// inv.inverse_mut(); + /// assert_relative_eq!(unit * inv, UnitDualQuaternion::identity(), epsilon = 1.0e-6); + /// assert_relative_eq!(inv * unit, UnitDualQuaternion::identity(), epsilon = 1.0e-6); + /// ``` + #[inline] + #[must_use = "Did you mean to use inverse_mut()?"] + pub fn inverse_mut(&mut self) { + let quat = self.as_mut_unchecked(); + quat.real = Unit::new_unchecked(quat.real).inverse().into_inner(); + quat.dual = -quat.real * quat.dual * quat.real; + } + + /// The unit dual quaternion needed to make `self` and `other` coincide. + /// + /// The result is such that: `self.isometry_to(other) * self == other`. + /// + /// # Example + /// ``` + /// # #[macro_use] extern crate approx; + /// # use nalgebra::{UnitDualQuaternion, DualQuaternion, Quaternion}; + /// let qr = Quaternion::new(1.0, 2.0, 3.0, 4.0); + /// let qd = Quaternion::new(5.0, 6.0, 7.0, 8.0); + /// let iso1 = UnitDualQuaternion::new_normalize(DualQuaternion::from_real_and_dual(qr, qd)); + /// let iso2 = UnitDualQuaternion::new_normalize(DualQuaternion::from_real_and_dual(qd, qr)); + /// let iso_to = iso1.isometry_to(&iso2); + /// assert_relative_eq!(iso_to * iso1, iso2, epsilon = 1.0e-6); + /// ``` + #[inline] + pub fn isometry_to(&self, other: &Self) -> Self { + other / self + } + + /// Linear interpolation between two unit dual quaternions. + /// + /// The result is not normalized. + /// + /// # Example + /// ``` + /// # #[macro_use] extern crate approx; + /// # use nalgebra::{UnitDualQuaternion, DualQuaternion, Quaternion}; + /// let dq1 = UnitDualQuaternion::new_normalize(DualQuaternion::from_real_and_dual( + /// Quaternion::new(0.5, 0.0, 0.5, 0.0), + /// Quaternion::new(0.0, 0.5, 0.0, 0.5) + /// )); + /// let dq2 = UnitDualQuaternion::new_normalize(DualQuaternion::from_real_and_dual( + /// Quaternion::new(0.5, 0.0, 0.0, 0.5), + /// Quaternion::new(0.5, 0.0, 0.5, 0.0) + /// )); + /// assert_relative_eq!( + /// UnitDualQuaternion::new_normalize(dq1.lerp(&dq2, 0.5)), + /// UnitDualQuaternion::new_normalize( + /// DualQuaternion::from_real_and_dual( + /// Quaternion::new(0.5, 0.0, 0.25, 0.25), + /// Quaternion::new(0.25, 0.25, 0.25, 0.25) + /// ) + /// ), + /// epsilon = 1.0e-6 + /// ); + /// ``` + #[inline] + pub fn lerp(&self, other: &Self, t: N) -> DualQuaternion { + self.as_ref().lerp(other.as_ref(), t) + } + + /// Normalized linear interpolation between two unit quaternions. + /// + /// This is the same as `self.lerp` except that the result is normalized. + /// + /// # Example + /// ``` + /// # #[macro_use] extern crate approx; + /// # use nalgebra::{UnitDualQuaternion, DualQuaternion, Quaternion}; + /// let dq1 = UnitDualQuaternion::new_normalize(DualQuaternion::from_real_and_dual( + /// Quaternion::new(0.5, 0.0, 0.5, 0.0), + /// Quaternion::new(0.0, 0.5, 0.0, 0.5) + /// )); + /// let dq2 = UnitDualQuaternion::new_normalize(DualQuaternion::from_real_and_dual( + /// Quaternion::new(0.5, 0.0, 0.0, 0.5), + /// Quaternion::new(0.5, 0.0, 0.5, 0.0) + /// )); + /// assert_relative_eq!(dq1.nlerp(&dq2, 0.2), UnitDualQuaternion::new_normalize( + /// DualQuaternion::from_real_and_dual( + /// Quaternion::new(0.5, 0.0, 0.4, 0.1), + /// Quaternion::new(0.1, 0.4, 0.1, 0.4) + /// ) + /// ), epsilon = 1.0e-6); + /// ``` + #[inline] + pub fn nlerp(&self, other: &Self, t: N) -> Self { + let mut res = self.lerp(other, t); + let _ = res.normalize_mut(); + + Self::new_unchecked(res) + } + + /// Screw linear interpolation between two unit quaternions. This creates a + /// smooth arc from one isometry to another. + /// + /// Panics if the angle between both quaternion is 180 degrees (in which case the interpolation + /// is not well-defined). Use `.try_sclerp` instead to avoid the panic. + /// + /// # Example + /// ``` + /// # #[macro_use] extern crate approx; + /// # use nalgebra::{UnitDualQuaternion, DualQuaternion, UnitQuaternion, Vector3}; + /// + /// let dq1 = UnitDualQuaternion::from_parts( + /// Vector3::new(0.0, 3.0, 0.0).into(), + /// UnitQuaternion::from_euler_angles(std::f32::consts::FRAC_PI_4, 0.0, 0.0), + /// ); + /// + /// let dq2 = UnitDualQuaternion::from_parts( + /// Vector3::new(0.0, 0.0, 3.0).into(), + /// UnitQuaternion::from_euler_angles(-std::f32::consts::PI, 0.0, 0.0), + /// ); + /// + /// let dq = dq1.sclerp(&dq2, 1.0 / 3.0); + /// + /// assert_relative_eq!( + /// dq.rotation().euler_angles().0, std::f32::consts::FRAC_PI_2, epsilon = 1.0e-6 + /// ); + /// assert_relative_eq!(dq.translation().vector.y, 3.0, epsilon = 1.0e-6); + #[inline] + pub fn sclerp(&self, other: &Self, t: N) -> Self + where + N: RealField, + { + self.try_sclerp(other, t, N::default_epsilon()) + .expect("DualQuaternion sclerp: ambiguous configuration.") + } + + /// Computes the screw-linear interpolation between two unit quaternions or returns `None` + /// if both quaternions are approximately 180 degrees apart (in which case the interpolation is + /// not well-defined). + /// + /// # Arguments + /// * `self`: the first quaternion to interpolate from. + /// * `other`: the second quaternion to interpolate toward. + /// * `t`: the interpolation parameter. Should be between 0 and 1. + /// * `epsilon`: the value below which the sinus of the angle separating both quaternion + /// must be to return `None`. + #[inline] + pub fn try_sclerp(&self, other: &Self, t: N, epsilon: N) -> Option + where + N: RealField, + { + let two = N::one() + N::one(); + let half = N::one() / two; + + // Invert one of the quaternions if we've got a longest-path + // interpolation. + let other = { + let dot_product = self.as_ref().real.coords.dot(&other.as_ref().real.coords); + if dot_product < N::zero() { + -other.clone() + } else { + other.clone() + } + }; + + let difference = self.as_ref().conjugate() * other.as_ref(); + let norm_squared = difference.real.vector().norm_squared(); + if relative_eq!(norm_squared, N::zero(), epsilon = epsilon) { + return None; + } + + let inverse_norm_squared = N::one() / norm_squared; + let inverse_norm = inverse_norm_squared.sqrt(); + + let mut angle = two * difference.real.scalar().acos(); + let mut pitch = -two * difference.dual.scalar() * inverse_norm; + let direction = difference.real.vector() * inverse_norm; + let moment = (difference.dual.vector() + - direction * (pitch * difference.real.scalar() * half)) + * inverse_norm; + + angle *= t; + pitch *= t; + + let sin = (half * angle).sin(); + let cos = (half * angle).cos(); + let real = Quaternion::from_parts(cos, direction * sin); + let dual = Quaternion::from_parts( + -pitch * half * sin, + moment * sin + direction * (pitch * half * cos), + ); + + Some(self * UnitDualQuaternion::new_unchecked( + DualQuaternion::from_real_and_dual(real, dual) + )) + } + + /// Return the rotation part of this unit dual quaternion. + /// + /// ``` + /// # #[macro_use] extern crate approx; + /// # use nalgebra::{UnitDualQuaternion, UnitQuaternion, Vector3}; + /// let dq = UnitDualQuaternion::from_parts( + /// Vector3::new(0.0, 3.0, 0.0).into(), + /// UnitQuaternion::from_euler_angles(std::f32::consts::FRAC_PI_4, 0.0, 0.0) + /// ); + /// + /// assert_relative_eq!( + /// dq.rotation().angle(), std::f32::consts::FRAC_PI_4, epsilon = 1.0e-6 + /// ); + /// ``` + #[inline] + pub fn rotation(&self) -> UnitQuaternion { + Unit::new_unchecked(self.as_ref().real) + } + + /// Return the translation part of this unit dual quaternion. + /// + /// ``` + /// # #[macro_use] extern crate approx; + /// # use nalgebra::{UnitDualQuaternion, UnitQuaternion, Vector3}; + /// let dq = UnitDualQuaternion::from_parts( + /// Vector3::new(0.0, 3.0, 0.0).into(), + /// UnitQuaternion::from_euler_angles(std::f32::consts::FRAC_PI_4, 0.0, 0.0) + /// ); + /// + /// assert_relative_eq!( + /// dq.translation().vector, Vector3::new(0.0, 3.0, 0.0), epsilon = 1.0e-6 + /// ); + /// ``` + #[inline] + pub fn translation(&self) -> Translation3 { + let two = N::one() + N::one(); + Translation3::from( + ((self.as_ref().dual * self.as_ref().real.conjugate()) * two) + .vector() + .into_owned(), + ) + } + + /// Builds an isometry from this unit dual quaternion. + /// + /// ``` + /// # #[macro_use] extern crate approx; + /// # use nalgebra::{UnitDualQuaternion, UnitQuaternion, Vector3}; + /// let rotation = UnitQuaternion::from_euler_angles(std::f32::consts::PI, 0.0, 0.0); + /// let translation = Vector3::new(1.0, 3.0, 2.5); + /// let dq = UnitDualQuaternion::from_parts( + /// translation.into(), + /// rotation + /// ); + /// let iso = dq.to_isometry(); + /// + /// assert_relative_eq!(iso.rotation.angle(), std::f32::consts::PI, epsilon = 1.0e-6); + /// assert_relative_eq!(iso.translation.vector, translation, epsilon = 1.0e-6); + /// ``` + #[inline] + pub fn to_isometry(&self) -> Isometry3 { + Isometry3::from_parts(self.translation(), self.rotation()) + } + + /// Rotate and translate a point by this unit dual quaternion interpreted + /// as an isometry. + /// + /// This is the same as the multiplication `self * pt`. + /// + /// ``` + /// # #[macro_use] extern crate approx; + /// # use nalgebra::{UnitDualQuaternion, UnitQuaternion, Vector3, Point3}; + /// let dq = UnitDualQuaternion::from_parts( + /// Vector3::new(0.0, 3.0, 0.0).into(), + /// UnitQuaternion::from_euler_angles(std::f32::consts::FRAC_PI_2, 0.0, 0.0) + /// ); + /// let point = Point3::new(1.0, 2.0, 3.0); + /// + /// assert_relative_eq!( + /// dq.transform_point(&point), Point3::new(1.0, 0.0, 2.0), epsilon = 1.0e-6 + /// ); + /// ``` + #[inline] + pub fn transform_point(&self, pt: &Point3) -> Point3 { + self * pt + } + + /// Rotate a vector by this unit dual quaternion, ignoring the translational + /// component. + /// + /// This is the same as the multiplication `self * v`. + /// + /// ``` + /// # #[macro_use] extern crate approx; + /// # use nalgebra::{UnitDualQuaternion, UnitQuaternion, Vector3}; + /// let dq = UnitDualQuaternion::from_parts( + /// Vector3::new(0.0, 3.0, 0.0).into(), + /// UnitQuaternion::from_euler_angles(std::f32::consts::FRAC_PI_2, 0.0, 0.0) + /// ); + /// let vector = Vector3::new(1.0, 2.0, 3.0); + /// + /// assert_relative_eq!( + /// dq.transform_vector(&vector), Vector3::new(1.0, -3.0, 2.0), epsilon = 1.0e-6 + /// ); + /// ``` + #[inline] + pub fn transform_vector(&self, v: &Vector3) -> Vector3 { + self * v + } + + /// Rotate and translate a point by the inverse of this unit quaternion. + /// This may be + /// cheaper than inverting the unit dual quaternion and transforming the + /// point. + /// + /// ``` + /// # #[macro_use] extern crate approx; + /// # use nalgebra::{UnitDualQuaternion, UnitQuaternion, Vector3, Point3}; + /// let dq = UnitDualQuaternion::from_parts( + /// Vector3::new(0.0, 3.0, 0.0).into(), + /// UnitQuaternion::from_euler_angles(std::f32::consts::FRAC_PI_2, 0.0, 0.0) + /// ); + /// let point = Point3::new(1.0, 2.0, 3.0); + /// + /// assert_relative_eq!( + /// dq.inverse_transform_point(&point), Point3::new(1.0, 3.0, 1.0), epsilon = 1.0e-6 + /// ); + /// ``` + #[inline] + pub fn inverse_transform_point(&self, pt: &Point3) -> Point3 { + self.inverse() * pt + } + + /// Rotate a vector by the inverse of this unit quaternion, ignoring the + /// translational component + /// This may be + /// cheaper than inverting the unit dual quaternion and transforming the + /// vector. + /// + /// ``` + /// # #[macro_use] extern crate approx; + /// # use nalgebra::{UnitDualQuaternion, UnitQuaternion, Vector3}; + /// let dq = UnitDualQuaternion::from_parts( + /// Vector3::new(0.0, 3.0, 0.0).into(), + /// UnitQuaternion::from_euler_angles(std::f32::consts::FRAC_PI_2, 0.0, 0.0) + /// ); + /// let vector = Vector3::new(1.0, 2.0, 3.0); + /// + /// assert_relative_eq!( + /// dq.inverse_transform_vector(&vector), Vector3::new(1.0, 3.0, -2.0), epsilon = 1.0e-6 + /// ); + /// ``` + #[inline] + pub fn inverse_transform_vector(&self, v: &Vector3) -> Vector3 { + self.inverse() * v + } + + /// Rotate a unit vector by the inverse of this unit quaternion, ignoring + /// the translational component. This may be + /// cheaper than inverting the unit dual quaternion and transforming the + /// vector. + /// + /// ``` + /// # #[macro_use] extern crate approx; + /// # use nalgebra::{UnitDualQuaternion, UnitQuaternion, Unit, Vector3}; + /// let dq = UnitDualQuaternion::from_parts( + /// Vector3::new(0.0, 3.0, 0.0).into(), + /// UnitQuaternion::from_euler_angles(std::f32::consts::FRAC_PI_2, 0.0, 0.0) + /// ); + /// let vector = Unit::new_unchecked(Vector3::new(0.0, 1.0, 0.0)); + /// + /// assert_relative_eq!( + /// dq.inverse_transform_unit_vector(&vector), + /// Unit::new_unchecked(Vector3::new(0.0, 0.0, -1.0)), + /// epsilon = 1.0e-6 + /// ); + /// ``` + #[inline] + pub fn inverse_transform_unit_vector(&self, v: &Unit>) -> Unit> { + self.inverse() * v + } +} + +impl UnitDualQuaternion +where + N::Element: SimdRealField, +{ + /// Converts this unit dual quaternion interpreted as an isometry + /// into its equivalent homogeneous transformation matrix. + /// + /// ``` + /// # #[macro_use] extern crate approx; + /// # use nalgebra::{Matrix4, UnitDualQuaternion, UnitQuaternion, Vector3}; + /// let dq = UnitDualQuaternion::from_parts( + /// Vector3::new(1.0, 3.0, 2.0).into(), + /// UnitQuaternion::from_axis_angle(&Vector3::z_axis(), std::f32::consts::FRAC_PI_6) + /// ); + /// let expected = Matrix4::new(0.8660254, -0.5, 0.0, 1.0, + /// 0.5, 0.8660254, 0.0, 3.0, + /// 0.0, 0.0, 1.0, 2.0, + /// 0.0, 0.0, 0.0, 1.0); + /// + /// assert_relative_eq!(dq.to_homogeneous(), expected, epsilon = 1.0e-6); + /// ``` + #[inline] + pub fn to_homogeneous(&self) -> Matrix4 + { + self.to_isometry().to_homogeneous() + } +} + +impl Default for UnitDualQuaternion { + fn default() -> Self { + Self::identity() + } +} + +impl fmt::Display for UnitDualQuaternion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(axis) = self.rotation().axis() { + let axis = axis.into_inner(); + write!( + f, + "UnitDualQuaternion translation: {} − angle: {} − axis: ({}, {}, {})", + self.translation().vector, + self.rotation().angle(), + axis[0], + axis[1], + axis[2] + ) + } else { + write!( + f, + "UnitDualQuaternion translation: {} − angle: {} − axis: (undefined)", + self.translation().vector, + self.rotation().angle() + ) + } + } +} + +impl> AbsDiffEq for UnitDualQuaternion { + type Epsilon = N; + + #[inline] + fn default_epsilon() -> Self::Epsilon { + N::default_epsilon() + } + + #[inline] + fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool { + self.as_ref().abs_diff_eq(other.as_ref(), epsilon) + } +} + +impl> RelativeEq for UnitDualQuaternion { + #[inline] + fn default_max_relative() -> Self::Epsilon { + N::default_max_relative() + } + + #[inline] + fn relative_eq( + &self, + other: &Self, + epsilon: Self::Epsilon, + max_relative: Self::Epsilon, + ) -> bool { + self.as_ref() + .relative_eq(other.as_ref(), epsilon, max_relative) + } +} + +impl> UlpsEq for UnitDualQuaternion { + #[inline] + fn default_max_ulps() -> u32 { + N::default_max_ulps() + } + + #[inline] + fn ulps_eq(&self, other: &Self, epsilon: Self::Epsilon, max_ulps: u32) -> bool { + self.as_ref().ulps_eq(other.as_ref(), epsilon, max_ulps) + } +} diff --git a/src/geometry/dual_quaternion_alga.rs b/src/geometry/dual_quaternion_alga.rs new file mode 100644 index 00000000..d0dfdb24 --- /dev/null +++ b/src/geometry/dual_quaternion_alga.rs @@ -0,0 +1,316 @@ +use num::Zero; + +use alga::general::{ + AbstractGroup, AbstractGroupAbelian, AbstractLoop, AbstractMagma, AbstractModule, + AbstractMonoid, AbstractQuasigroup, AbstractSemigroup, Additive, Id, Identity, Module, + Multiplicative, RealField, TwoSidedInverse, +}; +use alga::linear::{ + AffineTransformation, DirectIsometry, FiniteDimVectorSpace, Isometry, NormedSpace, + ProjectiveTransformation, Similarity, Transformation, + VectorSpace, +}; + +use crate::base::Vector3; +use crate::geometry::{ + Point3, Quaternion, UnitQuaternion, DualQuaternion, UnitDualQuaternion, Translation3 +}; + +impl Identity for DualQuaternion { + #[inline] + fn identity() -> Self { + Self::identity() + } +} + +impl Identity for DualQuaternion { + #[inline] + fn identity() -> Self { + Self::zero() + } +} + +impl AbstractMagma for DualQuaternion { + #[inline] + fn operate(&self, rhs: &Self) -> Self { + self * rhs + } +} + +impl AbstractMagma for DualQuaternion { + #[inline] + fn operate(&self, rhs: &Self) -> Self { + self + rhs + } +} + +impl TwoSidedInverse for DualQuaternion { + #[inline] + fn two_sided_inverse(&self) -> Self { + -self + } +} + +macro_rules! impl_structures( + ($DualQuaternion: ident; $($marker: ident<$operator: ident>),* $(,)*) => {$( + impl $marker<$operator> for $DualQuaternion { } + )*} +); + +impl_structures!( + DualQuaternion; + AbstractSemigroup, + AbstractMonoid, + + AbstractSemigroup, + AbstractQuasigroup, + AbstractMonoid, + AbstractLoop, + AbstractGroup, + AbstractGroupAbelian +); + +/* + * + * Vector space. + * + */ +impl AbstractModule for DualQuaternion { + type AbstractRing = N; + + #[inline] + fn multiply_by(&self, n: N) -> Self { + self * n + } +} + +impl Module for DualQuaternion { + type Ring = N; +} + +impl VectorSpace for DualQuaternion { + type Field = N; +} + +impl FiniteDimVectorSpace for DualQuaternion { + #[inline] + fn dimension() -> usize { + 8 + } + + #[inline] + fn canonical_basis_element(i: usize) -> Self { + if i < 4 { + DualQuaternion::from_real_and_dual( + Quaternion::canonical_basis_element(i), + Quaternion::zero() + ) + } else { + DualQuaternion::from_real_and_dual( + Quaternion::zero(), + Quaternion::canonical_basis_element(i - 4) + ) + } + } + + #[inline] + fn dot(&self, other: &Self) -> N { + self.real.dot(&other.real) + self.dual.dot(&other.dual) + } + + #[inline] + unsafe fn component_unchecked(&self, i: usize) -> &N { + self.as_ref().get_unchecked(i) + } + + #[inline] + unsafe fn component_unchecked_mut(&mut self, i: usize) -> &mut N { + self.as_mut().get_unchecked_mut(i) + } +} + +impl NormedSpace for DualQuaternion { + type RealField = N; + type ComplexField = N; + + #[inline] + fn norm_squared(&self) -> N { + self.real.norm_squared() + } + + #[inline] + fn norm(&self) -> N { + self.real.norm() + } + + #[inline] + fn normalize(&self) -> Self { + self.normalize() + } + + #[inline] + fn normalize_mut(&mut self) -> N { + self.normalize_mut() + } + + #[inline] + fn try_normalize(&self, min_norm: N) -> Option { + let real_norm = self.real.norm(); + if real_norm > min_norm { + Some(Self::from_real_and_dual(self.real / real_norm, self.dual / real_norm)) + } else { + None + } + } + + #[inline] + fn try_normalize_mut(&mut self, min_norm: N) -> Option { + let real_norm = self.real.norm(); + if real_norm > min_norm { + self.real /= real_norm; + self.dual /= real_norm; + Some(real_norm) + } else { + None + } + } +} + +/* + * + * Implementations for UnitDualQuaternion. + * + */ +impl Identity for UnitDualQuaternion { + #[inline] + fn identity() -> Self { + Self::identity() + } +} + +impl AbstractMagma for UnitDualQuaternion { + #[inline] + fn operate(&self, rhs: &Self) -> Self { + self * rhs + } +} + +impl TwoSidedInverse + for UnitDualQuaternion +{ + #[inline] + fn two_sided_inverse(&self) -> Self { + self.inverse() + } + + #[inline] + fn two_sided_inverse_mut(&mut self) { + self.inverse_mut() + } +} + +impl_structures!( + UnitDualQuaternion; + AbstractSemigroup, + AbstractQuasigroup, + AbstractMonoid, + AbstractLoop, + AbstractGroup +); + +impl Transformation> for UnitDualQuaternion { + #[inline] + fn transform_point(&self, pt: &Point3) -> Point3 { + self.transform_point(pt) + } + + #[inline] + fn transform_vector(&self, v: &Vector3) -> Vector3 { + self.transform_vector(v) + } +} + +impl ProjectiveTransformation> + for UnitDualQuaternion +{ + #[inline] + fn inverse_transform_point(&self, pt: &Point3) -> Point3 { + self.inverse_transform_point(pt) + } + + #[inline] + fn inverse_transform_vector(&self, v: &Vector3) -> Vector3 { + self.inverse_transform_vector(v) + } +} + +impl AffineTransformation> + for UnitDualQuaternion +{ + type Rotation = UnitQuaternion; + type NonUniformScaling = Id; + type Translation = Translation3; + + #[inline] + fn decompose(&self) -> (Self::Translation, Self::Rotation, Id, Self::Rotation) { + (self.translation(), self.rotation(), Id::new(), UnitQuaternion::identity()) + } + + #[inline] + fn append_translation(&self, translation: &Self::Translation) -> Self { + self * Self::from_parts(translation.clone(), UnitQuaternion::identity()) + } + + #[inline] + fn prepend_translation(&self, translation: &Self::Translation) -> Self { + Self::from_parts(translation.clone(), UnitQuaternion::identity()) * self + } + + #[inline] + fn append_rotation(&self, r: &Self::Rotation) -> Self { + r * self + } + + #[inline] + fn prepend_rotation(&self, r: &Self::Rotation) -> Self { + self * r + } + + #[inline] + fn append_scaling(&self, _: &Self::NonUniformScaling) -> Self { + self.clone() + } + + #[inline] + fn prepend_scaling(&self, _: &Self::NonUniformScaling) -> Self { + self.clone() + } +} + +impl Similarity> for UnitDualQuaternion { + type Scaling = Id; + + #[inline] + fn translation(&self) -> Translation3 { + self.translation() + } + + #[inline] + fn rotation(&self) -> UnitQuaternion { + self.rotation() + } + + #[inline] + fn scaling(&self) -> Id { + Id::new() + } +} + +macro_rules! marker_impl( + ($($Trait: ident),*) => {$( + impl $Trait> for UnitDualQuaternion { } + )*} +); + +marker_impl!(Isometry, DirectIsometry); + diff --git a/src/geometry/dual_quaternion_construction.rs b/src/geometry/dual_quaternion_construction.rs index 25a979f7..2ec3d6b5 100644 --- a/src/geometry/dual_quaternion_construction.rs +++ b/src/geometry/dual_quaternion_construction.rs @@ -1,4 +1,8 @@ -use crate::{DualQuaternion, Quaternion, SimdRealField}; +use crate::{ + DualQuaternion, Quaternion, UnitDualQuaternion, SimdRealField, Isometry3, + Translation3, UnitQuaternion +}; +use num::{One, Zero}; impl DualQuaternion { /// Creates a dual quaternion from its rotation and translation components. @@ -16,7 +20,8 @@ impl DualQuaternion { pub fn from_real_and_dual(real: Quaternion, dual: Quaternion) -> Self { Self { real, dual } } - /// The dual quaternion multiplicative identity + + /// The dual quaternion multiplicative identity. /// /// # Example /// @@ -40,3 +45,153 @@ impl DualQuaternion { ) } } + +impl DualQuaternion +where + N::Element: SimdRealField +{ + + /// Creates a dual quaternion from only its real part, with no translation + /// component. + /// + /// # Example + /// ``` + /// # use nalgebra::{DualQuaternion, Quaternion}; + /// let rot = Quaternion::new(1.0, 2.0, 3.0, 4.0); + /// + /// let dq = DualQuaternion::from_real(rot); + /// assert_eq!(dq.real.w, 1.0); + /// assert_eq!(dq.dual.w, 0.0); + /// ``` + #[inline] + pub fn from_real(real: Quaternion) -> Self { + Self { real, dual: Quaternion::zero() } + } +} + +impl One for DualQuaternion +where + N::Element: SimdRealField, +{ + #[inline] + fn one() -> Self { + Self::identity() + } +} + +impl Zero for DualQuaternion +where + N::Element: SimdRealField, +{ + #[inline] + fn zero() -> Self { + DualQuaternion::from_real_and_dual( + Quaternion::zero(), + Quaternion::zero() + ) + } + + #[inline] + fn is_zero(&self) -> bool { + self.real.is_zero() && self.dual.is_zero() + } +} + +impl UnitDualQuaternion { + /// The unit dual quaternion multiplicative identity, which also represents + /// the identity transformation as an isometry. + /// + /// ``` + /// # use nalgebra::{UnitDualQuaternion, UnitQuaternion, Vector3, Point3}; + /// let ident = UnitDualQuaternion::identity(); + /// let point = Point3::new(1.0, -4.3, 3.33); + /// + /// assert_eq!(ident * point, point); + /// assert_eq!(ident, ident.inverse()); + /// ``` + #[inline] + pub fn identity() -> Self { + Self::new_unchecked(DualQuaternion::identity()) + } +} + +impl UnitDualQuaternion +where + N::Element: SimdRealField, +{ + /// Return a dual quaternion representing the translation and orientation + /// given by the provided rotation quaternion and translation vector. + /// + /// ``` + /// # #[macro_use] extern crate approx; + /// # use nalgebra::{UnitDualQuaternion, UnitQuaternion, Vector3, Point3}; + /// let dq = UnitDualQuaternion::from_parts( + /// Vector3::new(0.0, 3.0, 0.0).into(), + /// UnitQuaternion::from_euler_angles(std::f32::consts::FRAC_PI_2, 0.0, 0.0) + /// ); + /// let point = Point3::new(1.0, 2.0, 3.0); + /// + /// assert_relative_eq!(dq * point, Point3::new(1.0, 0.0, 2.0), epsilon = 1.0e-6); + /// ``` + #[inline] + pub fn from_parts( + translation: Translation3, + rotation: UnitQuaternion + ) -> Self { + let half: N = crate::convert(0.5f64); + UnitDualQuaternion::new_unchecked(DualQuaternion { + real: rotation.clone().into_inner(), + dual: Quaternion::from_parts(N::zero(), translation.vector) + * rotation.clone().into_inner() + * half, + }) + } + + /// Return a unit dual quaternion representing the translation and orientation + /// given by the provided isometry. + /// + /// ``` + /// # #[macro_use] extern crate approx; + /// # use nalgebra::{Isometry3, UnitDualQuaternion, UnitQuaternion, Vector3, Point3}; + /// let iso = Isometry3::from_parts( + /// Vector3::new(0.0, 3.0, 0.0).into(), + /// UnitQuaternion::from_euler_angles(std::f32::consts::FRAC_PI_2, 0.0, 0.0) + /// ); + /// let dq = UnitDualQuaternion::from_isometry(&iso); + /// let point = Point3::new(1.0, 2.0, 3.0); + /// + /// assert_relative_eq!(dq * point, iso * point, epsilon = 1.0e-6); + /// ``` + #[inline] + pub fn from_isometry(isometry: &Isometry3) -> Self { + UnitDualQuaternion::from_parts(isometry.translation, isometry.rotation) + } + + /// Creates a dual quaternion from a unit quaternion rotation. + /// + /// # Example + /// ``` + /// # #[macro_use] extern crate approx; + /// # use nalgebra::{UnitQuaternion, UnitDualQuaternion, Quaternion}; + /// let q = Quaternion::new(1.0, 2.0, 3.0, 4.0); + /// let rot = UnitQuaternion::new_normalize(q); + /// + /// let dq = UnitDualQuaternion::from_rotation(rot); + /// assert_relative_eq!(dq.as_ref().real.norm(), 1.0, epsilon = 1.0e-6); + /// assert_eq!(dq.as_ref().dual.norm(), 0.0); + /// ``` + #[inline] + pub fn from_rotation(rotation: UnitQuaternion) -> Self { + Self::new_unchecked(DualQuaternion::from_real(rotation.into_inner())) + } +} + +impl One for UnitDualQuaternion +where + N::Element: SimdRealField, +{ + #[inline] + fn one() -> Self { + Self::identity() + } +} diff --git a/src/geometry/dual_quaternion_conversion.rs b/src/geometry/dual_quaternion_conversion.rs new file mode 100644 index 00000000..41548391 --- /dev/null +++ b/src/geometry/dual_quaternion_conversion.rs @@ -0,0 +1,186 @@ +use simba::scalar::{RealField, SubsetOf, SupersetOf}; +use simba::simd::SimdRealField; + +use crate::base::dimension::U3; +use crate::base::{Matrix4, Vector4}; +use crate::geometry::{ + Isometry3, DualQuaternion, Similarity3, SuperTCategoryOf, + TAffine, Transform, Translation3, UnitQuaternion, UnitDualQuaternion +}; + +/* + * This file provides the following conversions: + * ============================================= + * + * DualQuaternion -> DualQuaternion + * UnitDualQuaternion -> UnitDualQuaternion + * UnitDualQuaternion -> Isometry + * UnitDualQuaternion -> Similarity + * UnitDualQuaternion -> Transform + * UnitDualQuaternion -> Matrix (homogeneous) + * + * NOTE: + * UnitDualQuaternion -> DualQuaternion is already provided by: Unit -> T + */ + +impl SubsetOf> for DualQuaternion +where + N1: SimdRealField, + N2: SimdRealField + SupersetOf, +{ + #[inline] + fn to_superset(&self) -> DualQuaternion { + DualQuaternion::from_real_and_dual(self.real.to_superset(), self.dual.to_superset()) + } + + #[inline] + fn is_in_subset(dq: &DualQuaternion) -> bool { + crate::is_convertible::<_, Vector4>(&dq.real.coords) && + crate::is_convertible::<_, Vector4>(&dq.dual.coords) + } + + #[inline] + fn from_superset_unchecked(dq: &DualQuaternion) -> Self { + DualQuaternion::from_real_and_dual( + dq.real.to_subset_unchecked(), dq.dual.to_subset_unchecked() + ) + } +} + +impl SubsetOf> for UnitDualQuaternion +where + N1: SimdRealField, + N2: SimdRealField + SupersetOf, +{ + #[inline] + fn to_superset(&self) -> UnitDualQuaternion { + UnitDualQuaternion::new_unchecked(self.as_ref().to_superset()) + } + + #[inline] + fn is_in_subset(dq: &UnitDualQuaternion) -> bool { + crate::is_convertible::<_, DualQuaternion>(dq.as_ref()) + } + + #[inline] + fn from_superset_unchecked(dq: &UnitDualQuaternion) -> Self { + Self::new_unchecked(crate::convert_ref_unchecked(dq.as_ref())) + } +} + +impl SubsetOf> for UnitDualQuaternion +where + N1: RealField, + N2: RealField + SupersetOf +{ + #[inline] + fn to_superset(&self) -> Isometry3 { + let dq: UnitDualQuaternion = self.to_superset(); + let iso = dq.to_isometry(); + crate::convert_unchecked(iso) + } + + #[inline] + fn is_in_subset(iso: &Isometry3) -> bool { + crate::is_convertible::<_, UnitQuaternion>(&iso.rotation) && + crate::is_convertible::<_, Translation3>(&iso.translation) + } + + #[inline] + fn from_superset_unchecked(iso: &Isometry3) -> Self { + let dq = UnitDualQuaternion::::from_isometry(iso); + crate::convert_unchecked(dq) + } +} + +impl SubsetOf> for UnitDualQuaternion +where + N1: RealField, + N2: RealField + SupersetOf +{ + #[inline] + fn to_superset(&self) -> Similarity3 { + Similarity3::from_isometry(crate::convert_ref(self), N2::one()) + } + + #[inline] + fn is_in_subset(sim: &Similarity3) -> bool { + sim.scaling() == N2::one() + } + + #[inline] + fn from_superset_unchecked(sim: &Similarity3) -> Self { + crate::convert_ref_unchecked(&sim.isometry) + } +} + +impl SubsetOf> for UnitDualQuaternion +where + N1: RealField, + N2: RealField + SupersetOf, + C: SuperTCategoryOf, +{ + #[inline] + fn to_superset(&self) -> Transform { + Transform::from_matrix_unchecked(self.to_homogeneous().to_superset()) + } + + #[inline] + fn is_in_subset(t: &Transform) -> bool { + >::is_in_subset(t.matrix()) + } + + #[inline] + fn from_superset_unchecked(t: &Transform) -> Self { + Self::from_superset_unchecked(t.matrix()) + } +} + +impl> SubsetOf> for UnitDualQuaternion { + #[inline] + fn to_superset(&self) -> Matrix4 { + self.to_homogeneous().to_superset() + } + + #[inline] + fn is_in_subset(m: &Matrix4) -> bool { + crate::is_convertible::<_, Isometry3>(m) + } + + #[inline] + fn from_superset_unchecked(m: &Matrix4) -> Self { + let iso: Isometry3 = crate::convert_ref_unchecked(m); + Self::from_isometry(&iso) + } +} + +impl From> for Matrix4 +where + N::Element: SimdRealField, +{ + #[inline] + fn from(dq: UnitDualQuaternion) -> Self { + dq.to_homogeneous() + } +} + +impl From> for Isometry3 +where + N::Element: SimdRealField, +{ + #[inline] + fn from(dq: UnitDualQuaternion) -> Self { + dq.to_isometry() + } +} + +impl From> for UnitDualQuaternion +where + N::Element: SimdRealField, +{ + #[inline] + fn from(iso: Isometry3) -> Self { + Self::from_isometry(&iso) + } +} + diff --git a/src/geometry/dual_quaternion_ops.rs b/src/geometry/dual_quaternion_ops.rs index 0c9f78f4..95991c63 100644 --- a/src/geometry/dual_quaternion_ops.rs +++ b/src/geometry/dual_quaternion_ops.rs @@ -22,9 +22,16 @@ * - https://cs.gmu.edu/~jmlien/teaching/cs451/uploads/Main/dual-quaternion.pdf */ -use crate::{DualQuaternion, SimdRealField}; +use crate::{ + DualQuaternion, SimdRealField, Point3, Point, Vector3, Isometry3, Quaternion, + UnitDualQuaternion, UnitQuaternion, U1, U3, U4, Unit, Allocator, + DefaultAllocator, Vector +}; +use crate::base::storage::Storage; use std::mem; -use std::ops::{Add, Index, IndexMut, Mul, Sub}; +use std::ops::{ + Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub, SubAssign +}; impl AsRef<[N; 8]> for DualQuaternion { #[inline] @@ -56,49 +63,758 @@ impl IndexMut for DualQuaternion { } } -impl Mul> for DualQuaternion +impl Neg for DualQuaternion where N::Element: SimdRealField, { type Output = DualQuaternion; - fn mul(self, rhs: Self) -> Self::Output { - Self::from_real_and_dual( - self.real * rhs.real, - self.real * rhs.dual + self.dual * rhs.real, + #[inline] + fn neg(self) -> Self::Output { + DualQuaternion::from_real_and_dual(-self.real, -self.dual) + } +} + +impl<'a, N: SimdRealField> Neg for &'a DualQuaternion +where + N::Element: SimdRealField, +{ + type Output = DualQuaternion; + + #[inline] + fn neg(self) -> Self::Output { + DualQuaternion::from_real_and_dual(-&self.real, -&self.dual) + } +} + +impl Neg for UnitDualQuaternion +where + N::Element: SimdRealField, +{ + type Output = UnitDualQuaternion; + + #[inline] + fn neg(self) -> Self::Output { + UnitDualQuaternion::new_unchecked(-self.into_inner()) + } +} + +impl<'a, N: SimdRealField> Neg for &'a UnitDualQuaternion +where + N::Element: SimdRealField, +{ + type Output = UnitDualQuaternion; + + #[inline] + fn neg(self) -> Self::Output { + UnitDualQuaternion::new_unchecked(-self.as_ref()) + } +} + +macro_rules! dual_quaternion_op_impl( + ($Op: ident, $op: ident; + ($LhsRDim: ident, $LhsCDim: ident), ($RhsRDim: ident, $RhsCDim: ident) + $(for $Storage: ident: $StoragesBound: ident $(<$($BoundParam: ty),*>)*),*; + $lhs: ident: $Lhs: ty, $rhs: ident: $Rhs: ty, Output = $Result: ty $(=> $VDimA: ty, $VDimB: ty)*; + $action: expr; $($lives: tt),*) => { + impl<$($lives ,)* N: SimdRealField $(, $Storage: $StoragesBound $(<$($BoundParam),*>)*)*> $Op<$Rhs> for $Lhs + where N::Element: SimdRealField, + DefaultAllocator: Allocator + + Allocator { + type Output = $Result; + + #[inline] + fn $op($lhs, $rhs: $Rhs) -> Self::Output { + $action + } + } + } +); + +// DualQuaternion + DualQuaternion +dual_quaternion_op_impl!( + Add, add; + (U4, U1), (U4, U1); + self: &'a DualQuaternion, rhs: &'b DualQuaternion, Output = DualQuaternion; + DualQuaternion::from_real_and_dual( + &self.real + &rhs.real, + &self.dual + &rhs.dual, + ); + 'a, 'b); + +dual_quaternion_op_impl!( + Add, add; + (U4, U1), (U4, U1); + self: &'a DualQuaternion, rhs: DualQuaternion, Output = DualQuaternion; + DualQuaternion::from_real_and_dual( + &self.real + rhs.real, + &self.dual + rhs.dual, + ); + 'a); + +dual_quaternion_op_impl!( + Add, add; + (U4, U1), (U4, U1); + self: DualQuaternion, rhs: &'b DualQuaternion, Output = DualQuaternion; + DualQuaternion::from_real_and_dual( + self.real + &rhs.real, + self.dual + &rhs.dual, + ); + 'b); + +dual_quaternion_op_impl!( + Add, add; + (U4, U1), (U4, U1); + self: DualQuaternion, rhs: DualQuaternion, Output = DualQuaternion; + DualQuaternion::from_real_and_dual( + self.real + rhs.real, + self.dual + rhs.dual, + ); ); + +// DualQuaternion - DualQuaternion +dual_quaternion_op_impl!( + Sub, sub; + (U4, U1), (U4, U1); + self: &'a DualQuaternion, rhs: &'b DualQuaternion, Output = DualQuaternion; + DualQuaternion::from_real_and_dual( + &self.real - &rhs.real, + &self.dual - &rhs.dual, + ); + 'a, 'b); + +dual_quaternion_op_impl!( + Sub, sub; + (U4, U1), (U4, U1); + self: &'a DualQuaternion, rhs: DualQuaternion, Output = DualQuaternion; + DualQuaternion::from_real_and_dual( + &self.real - rhs.real, + &self.dual - rhs.dual, + ); + 'a); + +dual_quaternion_op_impl!( + Sub, sub; + (U4, U1), (U4, U1); + self: DualQuaternion, rhs: &'b DualQuaternion, Output = DualQuaternion; + DualQuaternion::from_real_and_dual( + self.real - &rhs.real, + self.dual - &rhs.dual, + ); + 'b); + +dual_quaternion_op_impl!( + Sub, sub; + (U4, U1), (U4, U1); + self: DualQuaternion, rhs: DualQuaternion, Output = DualQuaternion; + DualQuaternion::from_real_and_dual( + self.real - rhs.real, + self.dual - rhs.dual, + ); ); + +// DualQuaternion × DualQuaternion +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U4, U1); + self: &'a DualQuaternion, rhs: &'b DualQuaternion, Output = DualQuaternion; + DualQuaternion::from_real_and_dual( + &self.real * &rhs.real, + &self.real * &rhs.dual + &self.dual * &rhs.real, + ); + 'a, 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U4, U1); + self: &'a DualQuaternion, rhs: DualQuaternion, Output = DualQuaternion; + self * &rhs; + 'a); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U4, U1); + self: DualQuaternion, rhs: &'b DualQuaternion, Output = DualQuaternion; + &self * rhs; + 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U4, U1); + self: DualQuaternion, rhs: DualQuaternion, Output = DualQuaternion; + &self * &rhs; ); + +// UnitDualQuaternion × UnitDualQuaternion +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U4, U1); + self: &'a UnitDualQuaternion, rhs: &'b UnitDualQuaternion, Output = UnitDualQuaternion; + UnitDualQuaternion::new_unchecked(self.as_ref() * rhs.as_ref()); + 'a, 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U4, U1); + self: &'a UnitDualQuaternion, rhs: UnitDualQuaternion, Output = UnitDualQuaternion; + self * &rhs; + 'a); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U4, U1); + self: UnitDualQuaternion, rhs: &'b UnitDualQuaternion, Output = UnitDualQuaternion; + &self * rhs; + 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U4, U1); + self: UnitDualQuaternion, rhs: UnitDualQuaternion, Output = UnitDualQuaternion; + &self * &rhs; ); + +// UnitDualQuaternion ÷ UnitDualQuaternion +dual_quaternion_op_impl!( + Div, div; + (U4, U1), (U4, U1); + self: &'a UnitDualQuaternion, rhs: &'b UnitDualQuaternion, Output = UnitDualQuaternion; + #[allow(clippy::suspicious_arithmetic_impl)] { self * rhs.inverse() }; + 'a, 'b); + +dual_quaternion_op_impl!( + Div, div; + (U4, U1), (U4, U1); + self: &'a UnitDualQuaternion, rhs: UnitDualQuaternion, Output = UnitDualQuaternion; + self / &rhs; + 'a); + +dual_quaternion_op_impl!( + Div, div; + (U4, U1), (U4, U1); + self: UnitDualQuaternion, rhs: &'b UnitDualQuaternion, Output = UnitDualQuaternion; + &self / rhs; + 'b); + +dual_quaternion_op_impl!( + Div, div; + (U4, U1), (U4, U1); + self: UnitDualQuaternion, rhs: UnitDualQuaternion, Output = UnitDualQuaternion; + &self / &rhs; ); + +// UnitDualQuaternion × UnitQuaternion +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U4, U1); + self: &'a UnitDualQuaternion, rhs: &'b UnitQuaternion, + Output = UnitDualQuaternion => U1, U4; + self * UnitDualQuaternion::::new_unchecked(DualQuaternion::from_real(rhs.into_inner())); + 'a, 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U4, U1); + self: &'a UnitDualQuaternion, rhs: UnitQuaternion, + Output = UnitDualQuaternion => U3, U3; + self * UnitDualQuaternion::::new_unchecked(DualQuaternion::from_real(rhs.into_inner())); + 'a); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U4, U1); + self: UnitDualQuaternion, rhs: &'b UnitQuaternion, + Output = UnitDualQuaternion => U3, U3; + self * UnitDualQuaternion::::new_unchecked(DualQuaternion::from_real(rhs.into_inner())); + 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U4, U1); + self: UnitDualQuaternion, rhs: UnitQuaternion, + Output = UnitDualQuaternion => U3, U3; + self * UnitDualQuaternion::::new_unchecked(DualQuaternion::from_real(rhs.into_inner()));); + +// UnitQuaternion × UnitDualQuaternion +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U4, U1); + self: &'a UnitQuaternion, rhs: &'b UnitDualQuaternion, + Output = UnitDualQuaternion => U1, U4; + UnitDualQuaternion::::new_unchecked(DualQuaternion::from_real(self.into_inner())) * rhs; + 'a, 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U4, U1); + self: &'a UnitQuaternion, rhs: UnitDualQuaternion, + Output = UnitDualQuaternion => U3, U3; + UnitDualQuaternion::::new_unchecked(DualQuaternion::from_real(self.into_inner())) * rhs; + 'a); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U4, U1); + self: UnitQuaternion, rhs: &'b UnitDualQuaternion, + Output = UnitDualQuaternion => U3, U3; + UnitDualQuaternion::::new_unchecked(DualQuaternion::from_real(self.into_inner())) * rhs; + 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U4, U1); + self: UnitQuaternion, rhs: UnitDualQuaternion, + Output = UnitDualQuaternion => U3, U3; + UnitDualQuaternion::::new_unchecked(DualQuaternion::from_real(self.into_inner())) * rhs;); + +// UnitDualQuaternion × Isometry3 +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U3, U1); + self: &'a UnitDualQuaternion, rhs: &'b Isometry3, + Output = UnitDualQuaternion => U3, U1; + self * UnitDualQuaternion::::from_isometry(rhs); + 'a, 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U3, U3); + self: &'a UnitDualQuaternion, rhs: Isometry3, + Output = UnitDualQuaternion => U3, U1; + self * UnitDualQuaternion::::from_isometry(&rhs); + 'a); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U3, U3); + self: UnitDualQuaternion, rhs: &'b Isometry3, + Output = UnitDualQuaternion => U3, U1; + self * UnitDualQuaternion::::from_isometry(rhs); + 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U3, U3); + self: UnitDualQuaternion, rhs: Isometry3, + Output = UnitDualQuaternion => U3, U1; + self * UnitDualQuaternion::::from_isometry(&rhs); ); + +// UnitDualQuaternion ÷ Isometry +dual_quaternion_op_impl!( + Div, div; + (U4, U1), (U3, U1); + self: &'a UnitDualQuaternion, rhs: &'b Isometry3, + Output = UnitDualQuaternion => U3, U1; + // TODO: can we avoid the conversion to a rotation matrix? + self / UnitDualQuaternion::::from_isometry(rhs); + 'a, 'b); + +dual_quaternion_op_impl!( + Div, div; + (U4, U1), (U3, U3); + self: &'a UnitDualQuaternion, rhs: Isometry3, + Output = UnitDualQuaternion => U3, U1; + self / UnitDualQuaternion::::from_isometry(&rhs); + 'a); + +dual_quaternion_op_impl!( + Div, div; + (U4, U1), (U3, U3); + self: UnitDualQuaternion, rhs: &'b Isometry3, + Output = UnitDualQuaternion => U3, U1; + self / UnitDualQuaternion::::from_isometry(rhs); + 'b); + +dual_quaternion_op_impl!( + Div, div; + (U4, U1), (U3, U3); + self: UnitDualQuaternion, rhs: Isometry3, + Output = UnitDualQuaternion => U3, U1; + self / UnitDualQuaternion::::from_isometry(&rhs); ); + +// Isometry × UnitDualQuaternion +dual_quaternion_op_impl!( + Mul, mul; + (U3, U1), (U4, U1); + self: &'a Isometry3, rhs: &'b UnitDualQuaternion, + Output = UnitDualQuaternion => U3, U1; + UnitDualQuaternion::::from_isometry(self) * rhs; + 'a, 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U3, U1), (U4, U1); + self: &'a Isometry3, rhs: UnitDualQuaternion, + Output = UnitDualQuaternion => U3, U1; + UnitDualQuaternion::::from_isometry(self) * rhs; + 'a); + +dual_quaternion_op_impl!( + Mul, mul; + (U3, U1), (U4, U1); + self: Isometry3, rhs: &'b UnitDualQuaternion, + Output = UnitDualQuaternion => U3, U1; + UnitDualQuaternion::::from_isometry(&self) * rhs; + 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U3, U1), (U4, U1); + self: Isometry3, rhs: UnitDualQuaternion, + Output = UnitDualQuaternion => U3, U1; + UnitDualQuaternion::::from_isometry(&self) * rhs; ); + +// Isometry ÷ UnitDualQuaternion +dual_quaternion_op_impl!( + Div, div; + (U3, U1), (U4, U1); + self: &'a Isometry3, rhs: &'b UnitDualQuaternion, + Output = UnitDualQuaternion => U3, U1; + // TODO: can we avoid the conversion from a rotation matrix? + UnitDualQuaternion::::from_isometry(self) / rhs; + 'a, 'b); + +dual_quaternion_op_impl!( + Div, div; + (U3, U1), (U4, U1); + self: &'a Isometry3, rhs: UnitDualQuaternion, + Output = UnitDualQuaternion => U3, U1; + UnitDualQuaternion::::from_isometry(self) / rhs; + 'a); + +dual_quaternion_op_impl!( + Div, div; + (U3, U1), (U4, U1); + self: Isometry3, rhs: &'b UnitDualQuaternion, + Output = UnitDualQuaternion => U3, U1; + UnitDualQuaternion::::from_isometry(&self) / rhs; + 'b); + +dual_quaternion_op_impl!( + Div, div; + (U3, U1), (U4, U1); + self: Isometry3, rhs: UnitDualQuaternion, + Output = UnitDualQuaternion => U3, U1; + UnitDualQuaternion::::from_isometry(&self) / rhs; ); + +// UnitDualQuaternion × Vector +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U3, U1) for SB: Storage ; + self: &'a UnitDualQuaternion, rhs: &'b Vector, + Output = Vector3 => U3, U1; + Unit::new_unchecked(self.as_ref().real) * rhs; + 'a, 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U3, U1) for SB: Storage ; + self: &'a UnitDualQuaternion, rhs: Vector, + Output = Vector3 => U3, U1; + self * &rhs; + 'a); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U3, U1) for SB: Storage ; + self: UnitDualQuaternion, rhs: &'b Vector, + Output = Vector3 => U3, U1; + &self * rhs; + 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U3, U1) for SB: Storage ; + self: UnitDualQuaternion, rhs: Vector, + Output = Vector3 => U3, U1; + &self * &rhs; ); + +// UnitDualQuaternion × Point +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U3, U1); + self: &'a UnitDualQuaternion, rhs: &'b Point3, + Output = Point3 => U3, U1; + { + let two: N = crate::convert(2.0f64); + let q_point = Quaternion::from_parts(N::zero(), rhs.coords.clone()); + Point::from( + ((self.as_ref().real * q_point + self.as_ref().dual * two) * self.as_ref().real.conjugate()) + .vector() + .into_owned(), ) + }; + 'a, 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U3, U1); + self: &'a UnitDualQuaternion, rhs: Point3, + Output = Point3 => U3, U1; + self * &rhs; + 'a); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U3, U1); + self: UnitDualQuaternion, rhs: &'b Point3, + Output = Point3 => U3, U1; + &self * rhs; + 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U3, U1); + self: UnitDualQuaternion, rhs: Point3, + Output = Point3 => U3, U1; + &self * &rhs; ); + +// UnitDualQuaternion × Unit +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U3, U1) for SB: Storage ; + self: &'a UnitDualQuaternion, rhs: &'b Unit>, + Output = Unit> => U3, U4; + Unit::new_unchecked(self * rhs.as_ref()); + 'a, 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U3, U1) for SB: Storage ; + self: &'a UnitDualQuaternion, rhs: Unit>, + Output = Unit> => U3, U4; + Unit::new_unchecked(self * rhs.into_inner()); + 'a); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U3, U1) for SB: Storage ; + self: UnitDualQuaternion, rhs: &'b Unit>, + Output = Unit> => U3, U4; + Unit::new_unchecked(self * rhs.as_ref()); + 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U3, U1) for SB: Storage ; + self: UnitDualQuaternion, rhs: Unit>, + Output = Unit> => U3, U4; + Unit::new_unchecked(self * rhs.into_inner()); ); + +macro_rules! left_scalar_mul_impl( + ($($T: ty),* $(,)*) => {$( + impl Mul> for $T { + type Output = DualQuaternion<$T>; + + #[inline] + fn mul(self, right: DualQuaternion<$T>) -> Self::Output { + DualQuaternion::from_real_and_dual( + self * right.real, + self * right.dual + ) + } + } + + impl<'b> Mul<&'b DualQuaternion<$T>> for $T { + type Output = DualQuaternion<$T>; + + #[inline] + fn mul(self, right: &'b DualQuaternion<$T>) -> Self::Output { + DualQuaternion::from_real_and_dual( + self * &right.real, + self * &right.dual + ) + } + } + )*} +); + +left_scalar_mul_impl!(f32, f64); + +macro_rules! dual_quaternion_op_impl( + ($OpAssign: ident, $op_assign: ident; + ($LhsRDim: ident, $LhsCDim: ident), ($RhsRDim: ident, $RhsCDim: ident); + $lhs: ident: $Lhs: ty, $rhs: ident: $Rhs: ty $(=> $VDimA: ty, $VDimB: ty)*; + $action: expr; $($lives: tt),*) => { + impl<$($lives ,)* N: SimdRealField> $OpAssign<$Rhs> for $Lhs + where N::Element: SimdRealField, + DefaultAllocator: Allocator + + Allocator { + + #[inline] + fn $op_assign(&mut $lhs, $rhs: $Rhs) { + $action + } + } } -} +); -impl Mul for DualQuaternion -where - N::Element: SimdRealField, -{ - type Output = DualQuaternion; +// DualQuaternion += DualQuaternion +dual_quaternion_op_impl!( + AddAssign, add_assign; + (U4, U1), (U4, U1); + self: DualQuaternion, rhs: &'b DualQuaternion; + { + self.real += &rhs.real; + self.dual += &rhs.dual; + }; + 'b); - fn mul(self, rhs: N) -> Self::Output { - Self::from_real_and_dual(self.real * rhs, self.dual * rhs) - } -} +dual_quaternion_op_impl!( + AddAssign, add_assign; + (U4, U1), (U4, U1); + self: DualQuaternion, rhs: DualQuaternion; + { + self.real += rhs.real; + self.dual += rhs.dual; + };); -impl Add> for DualQuaternion -where - N::Element: SimdRealField, -{ - type Output = DualQuaternion; +// DualQuaternion -= DualQuaternion +dual_quaternion_op_impl!( + SubAssign, sub_assign; + (U4, U1), (U4, U1); + self: DualQuaternion, rhs: &'b DualQuaternion; + { + self.real -= &rhs.real; + self.dual -= &rhs.dual; + }; + 'b); - fn add(self, rhs: DualQuaternion) -> Self::Output { - Self::from_real_and_dual(self.real + rhs.real, self.dual + rhs.dual) - } -} +dual_quaternion_op_impl!( + SubAssign, sub_assign; + (U4, U1), (U4, U1); + self: DualQuaternion, rhs: DualQuaternion; + { + self.real -= rhs.real; + self.dual -= rhs.dual; + };); -impl Sub> for DualQuaternion -where - N::Element: SimdRealField, -{ - type Output = DualQuaternion; +// DualQuaternion ×= DualQuaternion +dual_quaternion_op_impl!( + MulAssign, mul_assign; + (U4, U1), (U4, U1); + self: DualQuaternion, rhs: &'b DualQuaternion; + { + let res = &*self * rhs; + self.real.coords.copy_from(&res.real.coords); + self.dual.coords.copy_from(&res.dual.coords); + }; + 'b); - fn sub(self, rhs: DualQuaternion) -> Self::Output { - Self::from_real_and_dual(self.real - rhs.real, self.dual - rhs.dual) - } -} +dual_quaternion_op_impl!( + MulAssign, mul_assign; + (U4, U1), (U4, U1); + self: DualQuaternion, rhs: DualQuaternion; + *self *= &rhs; ); + +// UnitDualQuaternion ×= UnitDualQuaternion +dual_quaternion_op_impl!( + MulAssign, mul_assign; + (U4, U1), (U4, U1); + self: UnitDualQuaternion, rhs: &'b UnitDualQuaternion; + { + let res = &*self * rhs; + self.as_mut_unchecked().real.coords.copy_from(&res.as_ref().real.coords); + self.as_mut_unchecked().dual.coords.copy_from(&res.as_ref().dual.coords); + }; + 'b); + +dual_quaternion_op_impl!( + MulAssign, mul_assign; + (U4, U1), (U4, U1); + self: UnitDualQuaternion, rhs: UnitDualQuaternion; + *self *= &rhs; ); + +// UnitDualQuaternion ÷= UnitDualQuaternion +dual_quaternion_op_impl!( + DivAssign, div_assign; + (U4, U1), (U4, U1); + self: UnitDualQuaternion, rhs: &'b UnitDualQuaternion; + { + let res = &*self / rhs; + self.as_mut_unchecked().real.coords.copy_from(&res.as_ref().real.coords); + self.as_mut_unchecked().dual.coords.copy_from(&res.as_ref().dual.coords); + }; + 'b); + +dual_quaternion_op_impl!( + DivAssign, div_assign; + (U4, U1), (U4, U1); + self: UnitDualQuaternion, rhs: UnitDualQuaternion; + *self /= &rhs; ); + +// UnitDualQuaternion ×= Isometry3 +dual_quaternion_op_impl!( + MulAssign, mul_assign; + (U4, U1), (U3, U1); + self: UnitDualQuaternion, rhs: &'b Isometry3 => U3, U1; + { + let res = &*self * rhs; + self.as_mut_unchecked().real.coords.copy_from(&res.as_ref().real.coords); + self.as_mut_unchecked().dual.coords.copy_from(&res.as_ref().dual.coords); + }; + 'b); + +dual_quaternion_op_impl!( + MulAssign, mul_assign; + (U4, U1), (U3, U1); + self: UnitDualQuaternion, rhs: Isometry3 => U3, U1; + *self *= &rhs; ); + +// UnitDualQuaternion ÷= Isometry3 +dual_quaternion_op_impl!( + DivAssign, div_assign; + (U4, U1), (U3, U1); + self: UnitDualQuaternion, rhs: &'b Isometry3 => U3, U1; + { + let res = &*self / rhs; + self.as_mut_unchecked().real.coords.copy_from(&res.as_ref().real.coords); + self.as_mut_unchecked().dual.coords.copy_from(&res.as_ref().dual.coords); + }; + 'b); + +dual_quaternion_op_impl!( + DivAssign, div_assign; + (U4, U1), (U3, U1); + self: UnitDualQuaternion, rhs: Isometry3 => U3, U1; + *self /= &rhs; ); + +macro_rules! scalar_op_impl( + ($($Op: ident, $op: ident, $OpAssign: ident, $op_assign: ident);* $(;)*) => {$( + impl $Op for DualQuaternion + where N::Element: SimdRealField { + type Output = DualQuaternion; + + #[inline] + fn $op(self, n: N) -> Self::Output { + DualQuaternion::from_real_and_dual( + self.real.$op(n), + self.dual.$op(n) + ) + } + } + + impl<'a, N: SimdRealField> $Op for &'a DualQuaternion + where N::Element: SimdRealField { + type Output = DualQuaternion; + + #[inline] + fn $op(self, n: N) -> Self::Output { + DualQuaternion::from_real_and_dual( + self.real.$op(n), + self.dual.$op(n) + ) + } + } + + impl $OpAssign for DualQuaternion + where N::Element: SimdRealField { + + #[inline] + fn $op_assign(&mut self, n: N) { + self.real.$op_assign(n); + self.dual.$op_assign(n); + } + } + )*} +); + +scalar_op_impl!( + Mul, mul, MulAssign, mul_assign; + Div, div, DivAssign, div_assign; +); diff --git a/src/geometry/isometry_conversion.rs b/src/geometry/isometry_conversion.rs index e3416cac..5939c2c1 100644 --- a/src/geometry/isometry_conversion.rs +++ b/src/geometry/isometry_conversion.rs @@ -6,7 +6,8 @@ use crate::base::dimension::{DimMin, DimName, DimNameAdd, DimNameSum, U1}; use crate::base::{DefaultAllocator, MatrixN, Scalar}; use crate::geometry::{ - AbstractRotation, Isometry, Similarity, SuperTCategoryOf, TAffine, Transform, Translation, + AbstractRotation, Isometry, Isometry3, Similarity, SuperTCategoryOf, TAffine, Transform, + Translation, UnitDualQuaternion, UnitQuaternion }; /* @@ -14,6 +15,7 @@ use crate::geometry::{ * ============================================= * * Isometry -> Isometry + * Isometry3 -> UnitDualQuaternion * Isometry -> Similarity * Isometry -> Transform * Isometry -> Matrix (homogeneous) @@ -47,6 +49,30 @@ where } } +impl SubsetOf> for Isometry3 +where + N1: RealField, + N2: RealField + SupersetOf, +{ + #[inline] + fn to_superset(&self) -> UnitDualQuaternion { + let dq = UnitDualQuaternion::::from_isometry(self); + dq.to_superset() + } + + #[inline] + fn is_in_subset(dq: &UnitDualQuaternion) -> bool { + crate::is_convertible::<_, UnitQuaternion>(&dq.rotation()) && + crate::is_convertible::<_, Translation>(&dq.translation()) + } + + #[inline] + fn from_superset_unchecked(dq: &UnitDualQuaternion) -> Self { + let dq: UnitDualQuaternion = crate::convert_ref_unchecked(dq); + dq.to_isometry() + } +} + impl SubsetOf> for Isometry where N1: RealField, diff --git a/src/geometry/mod.rs b/src/geometry/mod.rs index 19313b65..5fa8c094 100644 --- a/src/geometry/mod.rs +++ b/src/geometry/mod.rs @@ -36,7 +36,10 @@ mod quaternion_ops; mod quaternion_simba; mod dual_quaternion; +#[cfg(feature = "alga")] +mod dual_quaternion_alga; mod dual_quaternion_construction; +mod dual_quaternion_conversion; mod dual_quaternion_ops; mod unit_complex; diff --git a/src/geometry/quaternion_conversion.rs b/src/geometry/quaternion_conversion.rs index b57cc52b..ff2ab92a 100644 --- a/src/geometry/quaternion_conversion.rs +++ b/src/geometry/quaternion_conversion.rs @@ -10,7 +10,7 @@ use crate::base::dimension::U3; use crate::base::{Matrix3, Matrix4, Scalar, Vector4}; use crate::geometry::{ AbstractRotation, Isometry, Quaternion, Rotation, Rotation3, Similarity, SuperTCategoryOf, - TAffine, Transform, Translation, UnitQuaternion, + TAffine, Transform, Translation, UnitQuaternion, UnitDualQuaternion }; /* @@ -21,6 +21,7 @@ use crate::geometry::{ * UnitQuaternion -> UnitQuaternion * UnitQuaternion -> Rotation * UnitQuaternion -> Isometry + * UnitQuaternion -> UnitDualQuaternion * UnitQuaternion -> Similarity * UnitQuaternion -> Transform * UnitQuaternion -> Matrix (homogeneous) @@ -121,6 +122,28 @@ where } } +impl SubsetOf> for UnitQuaternion +where + N1: RealField, + N2: RealField + SupersetOf +{ + #[inline] + fn to_superset(&self) -> UnitDualQuaternion { + let q: UnitQuaternion = crate::convert_ref(self); + UnitDualQuaternion::from_rotation(q) + } + + #[inline] + fn is_in_subset(dq: &UnitDualQuaternion) -> bool { + dq.translation().vector.is_zero() + } + + #[inline] + fn from_superset_unchecked(dq: &UnitDualQuaternion) -> Self { + crate::convert_unchecked(dq.rotation()) + } +} + impl SubsetOf> for UnitQuaternion where N1: RealField, diff --git a/src/geometry/rotation_conversion.rs b/src/geometry/rotation_conversion.rs index c49b92c4..5e2c6014 100644 --- a/src/geometry/rotation_conversion.rs +++ b/src/geometry/rotation_conversion.rs @@ -12,7 +12,7 @@ use crate::base::{DefaultAllocator, Matrix2, Matrix3, Matrix4, MatrixN, Scalar}; use crate::geometry::{ AbstractRotation, Isometry, Rotation, Rotation2, Rotation3, Similarity, SuperTCategoryOf, - TAffine, Transform, Translation, UnitComplex, UnitQuaternion, + TAffine, Transform, Translation, UnitComplex, UnitQuaternion, UnitDualQuaternion, }; /* @@ -21,6 +21,7 @@ use crate::geometry::{ * * Rotation -> Rotation * Rotation3 -> UnitQuaternion + * Rotation3 -> UnitDualQuaternion * Rotation2 -> UnitComplex * Rotation -> Isometry * Rotation -> Similarity @@ -75,6 +76,31 @@ where } } +impl SubsetOf> for Rotation3 +where + N1: RealField, + N2: RealField + SupersetOf, +{ + #[inline] + fn to_superset(&self) -> UnitDualQuaternion { + let q = UnitQuaternion::::from_rotation_matrix(self); + let dq = UnitDualQuaternion::from_rotation(q); + dq.to_superset() + } + + #[inline] + fn is_in_subset(dq: &UnitDualQuaternion) -> bool { + crate::is_convertible::<_, UnitQuaternion>(&dq.rotation()) && + dq.translation().vector.is_zero() + } + + #[inline] + fn from_superset_unchecked(dq: &UnitDualQuaternion) -> Self { + let dq: UnitDualQuaternion = crate::convert_ref_unchecked(dq); + dq.rotation().to_rotation_matrix() + } +} + impl SubsetOf> for Rotation2 where N1: RealField, diff --git a/src/geometry/translation_conversion.rs b/src/geometry/translation_conversion.rs index 0754a678..f275297e 100644 --- a/src/geometry/translation_conversion.rs +++ b/src/geometry/translation_conversion.rs @@ -9,6 +9,7 @@ use crate::base::{DefaultAllocator, MatrixN, Scalar, VectorN}; use crate::geometry::{ AbstractRotation, Isometry, Similarity, SuperTCategoryOf, TAffine, Transform, Translation, + Translation3, UnitDualQuaternion, UnitQuaternion }; /* @@ -17,6 +18,7 @@ use crate::geometry::{ * * Translation -> Translation * Translation -> Isometry + * Translation3 -> UnitDualQuaternion * Translation -> Similarity * Translation -> Transform * Translation -> Matrix (homogeneous) @@ -69,6 +71,30 @@ where } } +impl SubsetOf> for Translation3 +where + N1: RealField, + N2: RealField + SupersetOf, +{ + #[inline] + fn to_superset(&self) -> UnitDualQuaternion { + let dq = UnitDualQuaternion::::from_parts(self.clone(), UnitQuaternion::identity()); + dq.to_superset() + } + + #[inline] + fn is_in_subset(dq: &UnitDualQuaternion) -> bool { + crate::is_convertible::<_, Translation>(&dq.translation()) && + dq.rotation() == UnitQuaternion::identity() + } + + #[inline] + fn from_superset_unchecked(dq: &UnitDualQuaternion) -> Self { + let dq: UnitDualQuaternion = crate::convert_ref_unchecked(dq); + dq.translation() + } +} + impl SubsetOf> for Translation where N1: RealField, From 6be0365203de7d09f418d0a42dba400a2839578f Mon Sep 17 00:00:00 2001 From: Terence Date: Thu, 28 Jan 2021 18:45:34 -0500 Subject: [PATCH 098/183] add integration test --- src/geometry/dual_quaternion_construction.rs | 29 ++ src/geometry/dual_quaternion_ops.rs | 291 ++++++++++++++++++- tests/geometry/dual_quaternion.rs | 172 +++++++++++ tests/geometry/mod.rs | 1 + 4 files changed, 492 insertions(+), 1 deletion(-) create mode 100644 tests/geometry/dual_quaternion.rs diff --git a/src/geometry/dual_quaternion_construction.rs b/src/geometry/dual_quaternion_construction.rs index 2ec3d6b5..daedc239 100644 --- a/src/geometry/dual_quaternion_construction.rs +++ b/src/geometry/dual_quaternion_construction.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "arbitrary")] +use quickcheck::{Arbitrary, Gen}; use crate::{ DualQuaternion, Quaternion, UnitDualQuaternion, SimdRealField, Isometry3, Translation3, UnitQuaternion @@ -97,6 +99,21 @@ where } } +#[cfg(feature = "arbitrary")] +impl Arbitrary for DualQuaternion +where + N: SimdRealField + Arbitrary + Send, + N::Element: SimdRealField, +{ + #[inline] + fn arbitrary(rng: &mut G) -> Self { + Self::from_real_and_dual( + Arbitrary::arbitrary(rng), + Arbitrary::arbitrary(rng) + ) + } +} + impl UnitDualQuaternion { /// The unit dual quaternion multiplicative identity, which also represents /// the identity transformation as an isometry. @@ -195,3 +212,15 @@ where Self::identity() } } + +#[cfg(feature = "arbitrary")] +impl Arbitrary for UnitDualQuaternion +where + N: SimdRealField + Arbitrary + Send, + N::Element: SimdRealField, +{ + #[inline] + fn arbitrary(rng: &mut G) -> Self { + Self::new_normalize(Arbitrary::arbitrary(rng)) + } +} diff --git a/src/geometry/dual_quaternion_ops.rs b/src/geometry/dual_quaternion_ops.rs index 95991c63..098fde6b 100644 --- a/src/geometry/dual_quaternion_ops.rs +++ b/src/geometry/dual_quaternion_ops.rs @@ -25,7 +25,7 @@ use crate::{ DualQuaternion, SimdRealField, Point3, Point, Vector3, Isometry3, Quaternion, UnitDualQuaternion, UnitQuaternion, U1, U3, U4, Unit, Allocator, - DefaultAllocator, Vector + DefaultAllocator, Vector, Translation3 }; use crate::base::storage::Storage; use std::mem; @@ -362,6 +362,223 @@ dual_quaternion_op_impl!( Output = UnitDualQuaternion => U3, U3; UnitDualQuaternion::::new_unchecked(DualQuaternion::from_real(self.into_inner())) * rhs;); +// UnitDualQuaternion ÷ UnitQuaternion +dual_quaternion_op_impl!( + Div, div; + (U4, U1), (U4, U1); + self: &'a UnitDualQuaternion, rhs: &'b UnitQuaternion, + Output = UnitDualQuaternion => U1, U4; + #[allow(clippy::suspicious_arithmetic_impl)] + { self * UnitDualQuaternion::::from_rotation(rhs.inverse()) }; + 'a, 'b); + +dual_quaternion_op_impl!( + Div, div; + (U4, U1), (U4, U1); + self: &'a UnitDualQuaternion, rhs: UnitQuaternion, + Output = UnitDualQuaternion => U3, U3; + #[allow(clippy::suspicious_arithmetic_impl)] + { self * UnitDualQuaternion::::from_rotation(rhs.inverse()) }; + 'a); + +dual_quaternion_op_impl!( + Div, div; + (U4, U1), (U4, U1); + self: UnitDualQuaternion, rhs: &'b UnitQuaternion, + Output = UnitDualQuaternion => U3, U3; + #[allow(clippy::suspicious_arithmetic_impl)] + { self * UnitDualQuaternion::::from_rotation(rhs.inverse()) }; + 'b); + +dual_quaternion_op_impl!( + Div, div; + (U4, U1), (U4, U1); + self: UnitDualQuaternion, rhs: UnitQuaternion, + Output = UnitDualQuaternion => U3, U3; + #[allow(clippy::suspicious_arithmetic_impl)] + { self * UnitDualQuaternion::::from_rotation(rhs.inverse()) };); + +// UnitQuaternion ÷ UnitDualQuaternion +dual_quaternion_op_impl!( + Div, div; + (U4, U1), (U4, U1); + self: &'a UnitQuaternion, rhs: &'b UnitDualQuaternion, + Output = UnitDualQuaternion => U1, U4; + #[allow(clippy::suspicious_arithmetic_impl)] + { + UnitDualQuaternion::::new_unchecked( + DualQuaternion::from_real(self.into_inner()) + ) * rhs.inverse() + }; 'a, 'b); + +dual_quaternion_op_impl!( + Div, div; + (U4, U1), (U4, U1); + self: &'a UnitQuaternion, rhs: UnitDualQuaternion, + Output = UnitDualQuaternion => U3, U3; + #[allow(clippy::suspicious_arithmetic_impl)] + { + UnitDualQuaternion::::new_unchecked( + DualQuaternion::from_real(self.into_inner()) + ) * rhs.inverse() + }; 'a); + +dual_quaternion_op_impl!( + Div, div; + (U4, U1), (U4, U1); + self: UnitQuaternion, rhs: &'b UnitDualQuaternion, + Output = UnitDualQuaternion => U3, U3; + #[allow(clippy::suspicious_arithmetic_impl)] + { + UnitDualQuaternion::::new_unchecked( + DualQuaternion::from_real(self.into_inner()) + ) * rhs.inverse() + }; 'b); + +dual_quaternion_op_impl!( + Div, div; + (U4, U1), (U4, U1); + self: UnitQuaternion, rhs: UnitDualQuaternion, + Output = UnitDualQuaternion => U3, U3; + #[allow(clippy::suspicious_arithmetic_impl)] + { + UnitDualQuaternion::::new_unchecked( + DualQuaternion::from_real(self.into_inner()) + ) * rhs.inverse() + };); + +// UnitDualQuaternion × Translation3 +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U3, U1); + self: &'a UnitDualQuaternion, rhs: &'b Translation3, + Output = UnitDualQuaternion => U3, U1; + self * UnitDualQuaternion::::from_parts(rhs.clone(), UnitQuaternion::identity()); + 'a, 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U3, U3); + self: &'a UnitDualQuaternion, rhs: Translation3, + Output = UnitDualQuaternion => U3, U1; + self * UnitDualQuaternion::::from_parts(rhs, UnitQuaternion::identity()); + 'a); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U3, U3); + self: UnitDualQuaternion, rhs: &'b Translation3, + Output = UnitDualQuaternion => U3, U1; + self * UnitDualQuaternion::::from_parts(rhs.clone(), UnitQuaternion::identity()); + 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U3, U3); + self: UnitDualQuaternion, rhs: Translation3, + Output = UnitDualQuaternion => U3, U1; + self * UnitDualQuaternion::::from_parts(rhs, UnitQuaternion::identity()); ); + +// UnitDualQuaternion ÷ Translation3 +dual_quaternion_op_impl!( + Div, div; + (U4, U1), (U3, U1); + self: &'a UnitDualQuaternion, rhs: &'b Translation3, + Output = UnitDualQuaternion => U3, U1; + #[allow(clippy::suspicious_arithmetic_impl)] + { self * UnitDualQuaternion::::from_parts(rhs.inverse(), UnitQuaternion::identity()) }; + 'a, 'b); + +dual_quaternion_op_impl!( + Div, div; + (U4, U1), (U3, U3); + self: &'a UnitDualQuaternion, rhs: Translation3, + Output = UnitDualQuaternion => U3, U1; + #[allow(clippy::suspicious_arithmetic_impl)] + { self * UnitDualQuaternion::::from_parts(rhs.inverse(), UnitQuaternion::identity()) }; + 'a); + +dual_quaternion_op_impl!( + Div, div; + (U4, U1), (U3, U3); + self: UnitDualQuaternion, rhs: &'b Translation3, + Output = UnitDualQuaternion => U3, U1; + #[allow(clippy::suspicious_arithmetic_impl)] + { self * UnitDualQuaternion::::from_parts(rhs.inverse(), UnitQuaternion::identity()) }; + 'b); + +dual_quaternion_op_impl!( + Div, div; + (U4, U1), (U3, U3); + self: UnitDualQuaternion, rhs: Translation3, + Output = UnitDualQuaternion => U3, U1; + #[allow(clippy::suspicious_arithmetic_impl)] + { self * UnitDualQuaternion::::from_parts(rhs.inverse(), UnitQuaternion::identity()) };); + +// Translation3 × UnitDualQuaternion +dual_quaternion_op_impl!( + Mul, mul; + (U3, U1), (U4, U1); + self: &'b Translation3, rhs: &'a UnitDualQuaternion, + Output = UnitDualQuaternion => U3, U1; + UnitDualQuaternion::::from_parts(self.clone(), UnitQuaternion::identity()) * rhs; + 'a, 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U3, U1), (U4, U1); + self: &'a Translation3, rhs: UnitDualQuaternion, + Output = UnitDualQuaternion => U3, U1; + UnitDualQuaternion::::from_parts(self.clone(), UnitQuaternion::identity()) * rhs; + 'a); + +dual_quaternion_op_impl!( + Mul, mul; + (U3, U1), (U4, U1); + self: Translation3, rhs: &'b UnitDualQuaternion, + Output = UnitDualQuaternion => U3, U1; + UnitDualQuaternion::::from_parts(self, UnitQuaternion::identity()) * rhs; + 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U3, U1), (U4, U1); + self: Translation3, rhs: UnitDualQuaternion, + Output = UnitDualQuaternion => U3, U1; + UnitDualQuaternion::::from_parts(self, UnitQuaternion::identity()) * rhs;); + +// Translation3 ÷ UnitDualQuaternion +dual_quaternion_op_impl!( + Div, div; + (U3, U1), (U4, U1); + self: &'b Translation3, rhs: &'a UnitDualQuaternion, + Output = UnitDualQuaternion => U3, U1; + UnitDualQuaternion::::from_parts(self.clone(), UnitQuaternion::identity()) / rhs; + 'a, 'b); + +dual_quaternion_op_impl!( + Div, div; + (U3, U1), (U4, U1); + self: &'a Translation3, rhs: UnitDualQuaternion, + Output = UnitDualQuaternion => U3, U1; + UnitDualQuaternion::::from_parts(self.clone(), UnitQuaternion::identity()) / rhs; + 'a); + +dual_quaternion_op_impl!( + Div, div; + (U3, U1), (U4, U1); + self: Translation3, rhs: &'b UnitDualQuaternion, + Output = UnitDualQuaternion => U3, U1; + UnitDualQuaternion::::from_parts(self, UnitQuaternion::identity()) / rhs; + 'b); + +dual_quaternion_op_impl!( + Div, div; + (U3, U1), (U4, U1); + self: Translation3, rhs: UnitDualQuaternion, + Output = UnitDualQuaternion => U3, U1; + UnitDualQuaternion::::from_parts(self, UnitQuaternion::identity()) / rhs;); + // UnitDualQuaternion × Isometry3 dual_quaternion_op_impl!( Mul, mul; @@ -738,6 +955,78 @@ dual_quaternion_op_impl!( self: UnitDualQuaternion, rhs: UnitDualQuaternion; *self /= &rhs; ); +// UnitDualQuaternion ×= UnitQuaternion +dual_quaternion_op_impl!( + MulAssign, mul_assign; + (U4, U1), (U4, U1); + self: UnitDualQuaternion, rhs: UnitQuaternion; + { + let res = &*self * UnitDualQuaternion::from_rotation(rhs); + self.as_mut_unchecked().real.coords.copy_from(&res.as_ref().real.coords); + self.as_mut_unchecked().dual.coords.copy_from(&res.as_ref().dual.coords); + };); + +dual_quaternion_op_impl!( + MulAssign, mul_assign; + (U4, U1), (U4, U1); + self: UnitDualQuaternion, rhs: &'b UnitQuaternion; + *self *= rhs.clone(); 'b); + +// UnitDualQuaternion ÷= UnitQuaternion +dual_quaternion_op_impl!( + DivAssign, div_assign; + (U4, U1), (U4, U1); + self: UnitDualQuaternion, rhs: &'b UnitQuaternion; + #[allow(clippy::suspicious_arithmetic_impl)] + { + let res = &*self * UnitDualQuaternion::from_rotation(rhs.inverse()); + self.as_mut_unchecked().real.coords.copy_from(&res.as_ref().real.coords); + self.as_mut_unchecked().dual.coords.copy_from(&res.as_ref().dual.coords); + }; + 'b); + +dual_quaternion_op_impl!( + DivAssign, div_assign; + (U4, U1), (U4, U1); + self: UnitDualQuaternion, rhs: UnitQuaternion; + *self /= &rhs; ); + +// UnitDualQuaternion ×= Translation3 +dual_quaternion_op_impl!( + MulAssign, mul_assign; + (U4, U1), (U4, U1); + self: UnitDualQuaternion, rhs: Translation3; + { + let res = &*self * UnitDualQuaternion::from_parts(rhs, UnitQuaternion::identity()); + self.as_mut_unchecked().real.coords.copy_from(&res.as_ref().real.coords); + self.as_mut_unchecked().dual.coords.copy_from(&res.as_ref().dual.coords); + };); + +dual_quaternion_op_impl!( + MulAssign, mul_assign; + (U4, U1), (U4, U1); + self: UnitDualQuaternion, rhs: &'b Translation3; + *self *= rhs.clone(); 'b); + +// UnitDualQuaternion ÷= Translation3 +dual_quaternion_op_impl!( + DivAssign, div_assign; + (U4, U1), (U4, U1); + self: UnitDualQuaternion, rhs: &'b Translation3; + #[allow(clippy::suspicious_arithmetic_impl)] + { + let res = &*self * UnitDualQuaternion::from_parts(rhs.inverse(), UnitQuaternion::identity()); + self.as_mut_unchecked().real.coords.copy_from(&res.as_ref().real.coords); + self.as_mut_unchecked().dual.coords.copy_from(&res.as_ref().dual.coords); + }; + 'b); + +dual_quaternion_op_impl!( + DivAssign, div_assign; + (U4, U1), (U4, U1); + self: UnitDualQuaternion, rhs: Translation3; + *self /= &rhs; ); + // UnitDualQuaternion ×= Isometry3 dual_quaternion_op_impl!( MulAssign, mul_assign; diff --git a/tests/geometry/dual_quaternion.rs b/tests/geometry/dual_quaternion.rs new file mode 100644 index 00000000..1baced29 --- /dev/null +++ b/tests/geometry/dual_quaternion.rs @@ -0,0 +1,172 @@ +#![cfg(feature = "arbitrary")] +#![allow(non_snake_case)] + +use na::{ + Isometry3, Point3, Translation3, UnitQuaternion, UnitDualQuaternion, Vector3, +}; + +quickcheck!( + fn isometry_equivalence(iso: Isometry3, p: Point3, v: Vector3) -> bool { + let dq = UnitDualQuaternion::from_isometry(&iso); + + relative_eq!(iso * p, dq * p, epsilon = 1.0e-7) + && relative_eq!(iso * v, dq * v, epsilon = 1.0e-7) + } + + fn inverse_is_identity(i: UnitDualQuaternion, p: Point3, v: Vector3) -> bool { + let ii = i.inverse(); + + relative_eq!(i * ii, UnitDualQuaternion::identity(), epsilon = 1.0e-7) + && relative_eq!(ii * i, UnitDualQuaternion::identity(), epsilon = 1.0e-7) + && relative_eq!((i * ii) * p, p, epsilon = 1.0e-7) + && relative_eq!((ii * i) * p, p, epsilon = 1.0e-7) + && relative_eq!((i * ii) * v, v, epsilon = 1.0e-7) + && relative_eq!((ii * i) * v, v, epsilon = 1.0e-7) + } + + fn multiply_equals_alga_transform( + dq: UnitDualQuaternion, v: Vector3, p: Point3 + ) -> bool { + dq * v == dq.transform_vector(&v) + && dq * p == dq.transform_point(&p) + && relative_eq!( + dq.inverse() * v, + dq.inverse_transform_vector(&v), + epsilon = 1.0e-7 + ) + && relative_eq!( + dq.inverse() * p, + dq.inverse_transform_point(&p), + epsilon = 1.0e-7 + ) + } + + #[cfg_attr(rustfmt, rustfmt_skip)] + fn composition( + dq: UnitDualQuaternion, + uq: UnitQuaternion, + t: Translation3, + v: Vector3, + p: Point3 + ) -> bool { + // (rotation × dual quaternion) * point = rotation × (dual quaternion * point) + relative_eq!((uq * dq) * v, uq * (dq * v), epsilon = 1.0e-7) && + relative_eq!((uq * dq) * p, uq * (dq * p), epsilon = 1.0e-7) && + + // (dual quaternion × rotation) * point = dual quaternion × (rotation * point) + relative_eq!((dq * uq) * v, dq * (uq * v), epsilon = 1.0e-7) && + relative_eq!((dq * uq) * p, dq * (uq * p), epsilon = 1.0e-7) && + + // (translation × dual quaternion) * point = translation × (dual quaternion * point) + relative_eq!((t * dq) * v, (dq * v), epsilon = 1.0e-7) && + relative_eq!((t * dq) * p, t * (dq * p), epsilon = 1.0e-7) && + + // (dual quaternion × translation) * point = dual quaternion × (translation * point) + relative_eq!((dq * t) * v, dq * v, epsilon = 1.0e-7) && + relative_eq!((dq * t) * p, dq * (t * p), epsilon = 1.0e-7) + } + + #[cfg_attr(rustfmt, rustfmt_skip)] + fn all_op_exist( + dq: UnitDualQuaternion, + uq: UnitQuaternion, + t: Translation3, + v: Vector3, + p: Point3 + ) -> bool { + let iMi = dq * dq; + let iMuq = dq * uq; + let iDi = dq / dq; + let iDuq = dq / uq; + + let iMp = dq * p; + let iMv = dq * v; + + let iMt = dq * t; + let tMi = t * dq; + + let tMuq = t * uq; + + let uqMi = uq * dq; + let uqDi = uq / dq; + + let uqMt = uq * t; + + let mut iMt1 = dq; + let mut iMt2 = dq; + + let mut iMi1 = dq; + let mut iMi2 = dq; + + let mut iMuq1 = dq; + let mut iMuq2 = dq; + + let mut iDi1 = dq; + let mut iDi2 = dq; + + let mut iDuq1 = dq; + let mut iDuq2 = dq; + + iMt1 *= t; + iMt2 *= &t; + + iMi1 *= dq; + iMi2 *= &dq; + + iMuq1 *= uq; + iMuq2 *= &uq; + + iDi1 /= dq; + iDi2 /= &dq; + + iDuq1 /= uq; + iDuq2 /= &uq; + + iMt == iMt1 + && iMt == iMt2 + && iMi == iMi1 + && iMi == iMi2 + && iMuq == iMuq1 + && iMuq == iMuq2 + && iDi == iDi1 + && iDi == iDi2 + && iDuq == iDuq1 + && iDuq == iDuq2 + && iMi == &dq * &dq + && iMi == dq * &dq + && iMi == &dq * dq + && iMuq == &dq * &uq + && iMuq == dq * &uq + && iMuq == &dq * uq + && iDi == &dq / &dq + && iDi == dq / &dq + && iDi == &dq / dq + && iDuq == &dq / &uq + && iDuq == dq / &uq + && iDuq == &dq / uq + && iMp == &dq * &p + && iMp == dq * &p + && iMp == &dq * p + && iMv == &dq * &v + && iMv == dq * &v + && iMv == &dq * v + && iMt == &dq * &t + && iMt == dq * &t + && iMt == &dq * t + && tMi == &t * &dq + && tMi == t * &dq + && tMi == &t * dq + && tMuq == &t * &uq + && tMuq == t * &uq + && tMuq == &t * uq + && uqMi == &uq * &dq + && uqMi == uq * &dq + && uqMi == &uq * dq + && uqDi == &uq / &dq + && uqDi == uq / &dq + && uqDi == &uq / dq + && uqMt == &uq * &t + && uqMt == uq * &t + && uqMt == &uq * t + } +); diff --git a/tests/geometry/mod.rs b/tests/geometry/mod.rs index ec9755a0..68d1bd6e 100644 --- a/tests/geometry/mod.rs +++ b/tests/geometry/mod.rs @@ -5,3 +5,4 @@ mod quaternion; mod rotation; mod similarity; mod unit_complex; +mod dual_quaternion; From 388b77108eaa8dc8e99da7c1537048eeb5077e41 Mon Sep 17 00:00:00 2001 From: Terence Date: Thu, 28 Jan 2021 18:46:14 -0500 Subject: [PATCH 099/183] rustfmt --- src/geometry/dual_quaternion.rs | 23 ++++++++------- src/geometry/dual_quaternion_alga.rs | 26 ++++++++++------ src/geometry/dual_quaternion_construction.rs | 31 ++++++++------------ src/geometry/dual_quaternion_conversion.rs | 24 ++++++++------- src/geometry/dual_quaternion_ops.rs | 12 ++++---- src/geometry/isometry_conversion.rs | 6 ++-- src/geometry/quaternion_conversion.rs | 4 +-- src/geometry/rotation_conversion.rs | 6 ++-- src/geometry/translation_conversion.rs | 6 ++-- tests/geometry/dual_quaternion.rs | 8 ++--- tests/geometry/mod.rs | 2 +- 11 files changed, 77 insertions(+), 71 deletions(-) diff --git a/src/geometry/dual_quaternion.rs b/src/geometry/dual_quaternion.rs index 63c5fa03..dac11725 100644 --- a/src/geometry/dual_quaternion.rs +++ b/src/geometry/dual_quaternion.rs @@ -1,11 +1,11 @@ -use std::fmt; -use approx::{AbsDiffEq, RelativeEq, UlpsEq}; use crate::{ - Quaternion, SimdRealField, VectorN, U8, Vector3, Point3, Isometry3, Unit, Matrix4, - Translation3, UnitQuaternion, Scalar, Normed + Isometry3, Matrix4, Normed, Point3, Quaternion, Scalar, SimdRealField, Translation3, Unit, + UnitQuaternion, Vector3, VectorN, U8, }; +use approx::{AbsDiffEq, RelativeEq, UlpsEq}; #[cfg(feature = "serde-serialize")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt; use simba::scalar::{ClosedNeg, RealField}; @@ -432,7 +432,9 @@ where #[inline] #[must_use = "Did you mean to use inverse_mut()?"] pub fn inverse(&self) -> Self { - let real = Unit::new_unchecked(self.as_ref().real).inverse().into_inner(); + let real = Unit::new_unchecked(self.as_ref().real) + .inverse() + .into_inner(); let dual = -real * self.as_ref().dual * real; UnitDualQuaternion::new_unchecked(DualQuaternion { real, dual }) } @@ -634,9 +636,11 @@ where moment * sin + direction * (pitch * half * cos), ); - Some(self * UnitDualQuaternion::new_unchecked( - DualQuaternion::from_real_and_dual(real, dual) - )) + Some( + self * UnitDualQuaternion::new_unchecked(DualQuaternion::from_real_and_dual( + real, dual, + )), + ) } /// Return the rotation part of this unit dual quaternion. @@ -844,8 +848,7 @@ where /// assert_relative_eq!(dq.to_homogeneous(), expected, epsilon = 1.0e-6); /// ``` #[inline] - pub fn to_homogeneous(&self) -> Matrix4 - { + pub fn to_homogeneous(&self) -> Matrix4 { self.to_isometry().to_homogeneous() } } diff --git a/src/geometry/dual_quaternion_alga.rs b/src/geometry/dual_quaternion_alga.rs index d0dfdb24..f6ee9e10 100644 --- a/src/geometry/dual_quaternion_alga.rs +++ b/src/geometry/dual_quaternion_alga.rs @@ -7,13 +7,12 @@ use alga::general::{ }; use alga::linear::{ AffineTransformation, DirectIsometry, FiniteDimVectorSpace, Isometry, NormedSpace, - ProjectiveTransformation, Similarity, Transformation, - VectorSpace, + ProjectiveTransformation, Similarity, Transformation, VectorSpace, }; use crate::base::Vector3; use crate::geometry::{ - Point3, Quaternion, UnitQuaternion, DualQuaternion, UnitDualQuaternion, Translation3 + DualQuaternion, Point3, Quaternion, Translation3, UnitDualQuaternion, UnitQuaternion, }; impl Identity for DualQuaternion { @@ -103,12 +102,12 @@ impl FiniteDimVectorSpace for DualQuate if i < 4 { DualQuaternion::from_real_and_dual( Quaternion::canonical_basis_element(i), - Quaternion::zero() + Quaternion::zero(), ) } else { DualQuaternion::from_real_and_dual( Quaternion::zero(), - Quaternion::canonical_basis_element(i - 4) + Quaternion::canonical_basis_element(i - 4), ) } } @@ -157,7 +156,10 @@ impl NormedSpace for DualQuaternion fn try_normalize(&self, min_norm: N) -> Option { let real_norm = self.real.norm(); if real_norm > min_norm { - Some(Self::from_real_and_dual(self.real / real_norm, self.dual / real_norm)) + Some(Self::from_real_and_dual( + self.real / real_norm, + self.dual / real_norm, + )) } else { None } @@ -188,7 +190,9 @@ impl Identity for UnitD } } -impl AbstractMagma for UnitDualQuaternion { +impl AbstractMagma + for UnitDualQuaternion +{ #[inline] fn operate(&self, rhs: &Self) -> Self { self * rhs @@ -253,7 +257,12 @@ impl AffineTransformation> #[inline] fn decompose(&self) -> (Self::Translation, Self::Rotation, Id, Self::Rotation) { - (self.translation(), self.rotation(), Id::new(), UnitQuaternion::identity()) + ( + self.translation(), + self.rotation(), + Id::new(), + UnitQuaternion::identity(), + ) } #[inline] @@ -313,4 +322,3 @@ macro_rules! marker_impl( ); marker_impl!(Isometry, DirectIsometry); - diff --git a/src/geometry/dual_quaternion_construction.rs b/src/geometry/dual_quaternion_construction.rs index daedc239..95c208ce 100644 --- a/src/geometry/dual_quaternion_construction.rs +++ b/src/geometry/dual_quaternion_construction.rs @@ -1,10 +1,10 @@ -#[cfg(feature = "arbitrary")] -use quickcheck::{Arbitrary, Gen}; use crate::{ - DualQuaternion, Quaternion, UnitDualQuaternion, SimdRealField, Isometry3, - Translation3, UnitQuaternion + DualQuaternion, Isometry3, Quaternion, SimdRealField, Translation3, UnitDualQuaternion, + UnitQuaternion, }; use num::{One, Zero}; +#[cfg(feature = "arbitrary")] +use quickcheck::{Arbitrary, Gen}; impl DualQuaternion { /// Creates a dual quaternion from its rotation and translation components. @@ -50,9 +50,8 @@ impl DualQuaternion { impl DualQuaternion where - N::Element: SimdRealField + N::Element: SimdRealField, { - /// Creates a dual quaternion from only its real part, with no translation /// component. /// @@ -67,7 +66,10 @@ where /// ``` #[inline] pub fn from_real(real: Quaternion) -> Self { - Self { real, dual: Quaternion::zero() } + Self { + real, + dual: Quaternion::zero(), + } } } @@ -87,10 +89,7 @@ where { #[inline] fn zero() -> Self { - DualQuaternion::from_real_and_dual( - Quaternion::zero(), - Quaternion::zero() - ) + DualQuaternion::from_real_and_dual(Quaternion::zero(), Quaternion::zero()) } #[inline] @@ -107,10 +106,7 @@ where { #[inline] fn arbitrary(rng: &mut G) -> Self { - Self::from_real_and_dual( - Arbitrary::arbitrary(rng), - Arbitrary::arbitrary(rng) - ) + Self::from_real_and_dual(Arbitrary::arbitrary(rng), Arbitrary::arbitrary(rng)) } } @@ -151,10 +147,7 @@ where /// assert_relative_eq!(dq * point, Point3::new(1.0, 0.0, 2.0), epsilon = 1.0e-6); /// ``` #[inline] - pub fn from_parts( - translation: Translation3, - rotation: UnitQuaternion - ) -> Self { + pub fn from_parts(translation: Translation3, rotation: UnitQuaternion) -> Self { let half: N = crate::convert(0.5f64); UnitDualQuaternion::new_unchecked(DualQuaternion { real: rotation.clone().into_inner(), diff --git a/src/geometry/dual_quaternion_conversion.rs b/src/geometry/dual_quaternion_conversion.rs index 41548391..20c6895a 100644 --- a/src/geometry/dual_quaternion_conversion.rs +++ b/src/geometry/dual_quaternion_conversion.rs @@ -4,8 +4,8 @@ use simba::simd::SimdRealField; use crate::base::dimension::U3; use crate::base::{Matrix4, Vector4}; use crate::geometry::{ - Isometry3, DualQuaternion, Similarity3, SuperTCategoryOf, - TAffine, Transform, Translation3, UnitQuaternion, UnitDualQuaternion + DualQuaternion, Isometry3, Similarity3, SuperTCategoryOf, TAffine, Transform, Translation3, + UnitDualQuaternion, UnitQuaternion, }; /* @@ -35,14 +35,15 @@ where #[inline] fn is_in_subset(dq: &DualQuaternion) -> bool { - crate::is_convertible::<_, Vector4>(&dq.real.coords) && - crate::is_convertible::<_, Vector4>(&dq.dual.coords) + crate::is_convertible::<_, Vector4>(&dq.real.coords) + && crate::is_convertible::<_, Vector4>(&dq.dual.coords) } #[inline] fn from_superset_unchecked(dq: &DualQuaternion) -> Self { DualQuaternion::from_real_and_dual( - dq.real.to_subset_unchecked(), dq.dual.to_subset_unchecked() + dq.real.to_subset_unchecked(), + dq.dual.to_subset_unchecked(), ) } } @@ -71,7 +72,7 @@ where impl SubsetOf> for UnitDualQuaternion where N1: RealField, - N2: RealField + SupersetOf + N2: RealField + SupersetOf, { #[inline] fn to_superset(&self) -> Isometry3 { @@ -82,8 +83,8 @@ where #[inline] fn is_in_subset(iso: &Isometry3) -> bool { - crate::is_convertible::<_, UnitQuaternion>(&iso.rotation) && - crate::is_convertible::<_, Translation3>(&iso.translation) + crate::is_convertible::<_, UnitQuaternion>(&iso.rotation) + && crate::is_convertible::<_, Translation3>(&iso.translation) } #[inline] @@ -96,7 +97,7 @@ where impl SubsetOf> for UnitDualQuaternion where N1: RealField, - N2: RealField + SupersetOf + N2: RealField + SupersetOf, { #[inline] fn to_superset(&self) -> Similarity3 { @@ -136,7 +137,9 @@ where } } -impl> SubsetOf> for UnitDualQuaternion { +impl> SubsetOf> + for UnitDualQuaternion +{ #[inline] fn to_superset(&self) -> Matrix4 { self.to_homogeneous().to_superset() @@ -183,4 +186,3 @@ where Self::from_isometry(&iso) } } - diff --git a/src/geometry/dual_quaternion_ops.rs b/src/geometry/dual_quaternion_ops.rs index 098fde6b..17644302 100644 --- a/src/geometry/dual_quaternion_ops.rs +++ b/src/geometry/dual_quaternion_ops.rs @@ -22,15 +22,15 @@ * - https://cs.gmu.edu/~jmlien/teaching/cs451/uploads/Main/dual-quaternion.pdf */ -use crate::{ - DualQuaternion, SimdRealField, Point3, Point, Vector3, Isometry3, Quaternion, - UnitDualQuaternion, UnitQuaternion, U1, U3, U4, Unit, Allocator, - DefaultAllocator, Vector, Translation3 -}; use crate::base::storage::Storage; +use crate::{ + Allocator, DefaultAllocator, DualQuaternion, Isometry3, Point, Point3, Quaternion, + SimdRealField, Translation3, Unit, UnitDualQuaternion, UnitQuaternion, Vector, Vector3, U1, U3, + U4, +}; use std::mem; use std::ops::{ - Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub, SubAssign + Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub, SubAssign, }; impl AsRef<[N; 8]> for DualQuaternion { diff --git a/src/geometry/isometry_conversion.rs b/src/geometry/isometry_conversion.rs index 5939c2c1..0c89958a 100644 --- a/src/geometry/isometry_conversion.rs +++ b/src/geometry/isometry_conversion.rs @@ -7,7 +7,7 @@ use crate::base::{DefaultAllocator, MatrixN, Scalar}; use crate::geometry::{ AbstractRotation, Isometry, Isometry3, Similarity, SuperTCategoryOf, TAffine, Transform, - Translation, UnitDualQuaternion, UnitQuaternion + Translation, UnitDualQuaternion, UnitQuaternion, }; /* @@ -62,8 +62,8 @@ where #[inline] fn is_in_subset(dq: &UnitDualQuaternion) -> bool { - crate::is_convertible::<_, UnitQuaternion>(&dq.rotation()) && - crate::is_convertible::<_, Translation>(&dq.translation()) + crate::is_convertible::<_, UnitQuaternion>(&dq.rotation()) + && crate::is_convertible::<_, Translation>(&dq.translation()) } #[inline] diff --git a/src/geometry/quaternion_conversion.rs b/src/geometry/quaternion_conversion.rs index ff2ab92a..2707419e 100644 --- a/src/geometry/quaternion_conversion.rs +++ b/src/geometry/quaternion_conversion.rs @@ -10,7 +10,7 @@ use crate::base::dimension::U3; use crate::base::{Matrix3, Matrix4, Scalar, Vector4}; use crate::geometry::{ AbstractRotation, Isometry, Quaternion, Rotation, Rotation3, Similarity, SuperTCategoryOf, - TAffine, Transform, Translation, UnitQuaternion, UnitDualQuaternion + TAffine, Transform, Translation, UnitDualQuaternion, UnitQuaternion, }; /* @@ -125,7 +125,7 @@ where impl SubsetOf> for UnitQuaternion where N1: RealField, - N2: RealField + SupersetOf + N2: RealField + SupersetOf, { #[inline] fn to_superset(&self) -> UnitDualQuaternion { diff --git a/src/geometry/rotation_conversion.rs b/src/geometry/rotation_conversion.rs index 5e2c6014..accf9a41 100644 --- a/src/geometry/rotation_conversion.rs +++ b/src/geometry/rotation_conversion.rs @@ -12,7 +12,7 @@ use crate::base::{DefaultAllocator, Matrix2, Matrix3, Matrix4, MatrixN, Scalar}; use crate::geometry::{ AbstractRotation, Isometry, Rotation, Rotation2, Rotation3, Similarity, SuperTCategoryOf, - TAffine, Transform, Translation, UnitComplex, UnitQuaternion, UnitDualQuaternion, + TAffine, Transform, Translation, UnitComplex, UnitDualQuaternion, UnitQuaternion, }; /* @@ -90,8 +90,8 @@ where #[inline] fn is_in_subset(dq: &UnitDualQuaternion) -> bool { - crate::is_convertible::<_, UnitQuaternion>(&dq.rotation()) && - dq.translation().vector.is_zero() + crate::is_convertible::<_, UnitQuaternion>(&dq.rotation()) + && dq.translation().vector.is_zero() } #[inline] diff --git a/src/geometry/translation_conversion.rs b/src/geometry/translation_conversion.rs index f275297e..9e915073 100644 --- a/src/geometry/translation_conversion.rs +++ b/src/geometry/translation_conversion.rs @@ -9,7 +9,7 @@ use crate::base::{DefaultAllocator, MatrixN, Scalar, VectorN}; use crate::geometry::{ AbstractRotation, Isometry, Similarity, SuperTCategoryOf, TAffine, Transform, Translation, - Translation3, UnitDualQuaternion, UnitQuaternion + Translation3, UnitDualQuaternion, UnitQuaternion, }; /* @@ -84,8 +84,8 @@ where #[inline] fn is_in_subset(dq: &UnitDualQuaternion) -> bool { - crate::is_convertible::<_, Translation>(&dq.translation()) && - dq.rotation() == UnitQuaternion::identity() + crate::is_convertible::<_, Translation>(&dq.translation()) + && dq.rotation() == UnitQuaternion::identity() } #[inline] diff --git a/tests/geometry/dual_quaternion.rs b/tests/geometry/dual_quaternion.rs index 1baced29..9cfca879 100644 --- a/tests/geometry/dual_quaternion.rs +++ b/tests/geometry/dual_quaternion.rs @@ -1,9 +1,7 @@ #![cfg(feature = "arbitrary")] #![allow(non_snake_case)] -use na::{ - Isometry3, Point3, Translation3, UnitQuaternion, UnitDualQuaternion, Vector3, -}; +use na::{Isometry3, Point3, Translation3, UnitDualQuaternion, UnitQuaternion, Vector3}; quickcheck!( fn isometry_equivalence(iso: Isometry3, p: Point3, v: Vector3) -> bool { @@ -25,7 +23,9 @@ quickcheck!( } fn multiply_equals_alga_transform( - dq: UnitDualQuaternion, v: Vector3, p: Point3 + dq: UnitDualQuaternion, + v: Vector3, + p: Point3, ) -> bool { dq * v == dq.transform_vector(&v) && dq * p == dq.transform_point(&p) diff --git a/tests/geometry/mod.rs b/tests/geometry/mod.rs index 68d1bd6e..2363d411 100644 --- a/tests/geometry/mod.rs +++ b/tests/geometry/mod.rs @@ -1,3 +1,4 @@ +mod dual_quaternion; mod isometry; mod point; mod projection; @@ -5,4 +6,3 @@ mod quaternion; mod rotation; mod similarity; mod unit_complex; -mod dual_quaternion; From ecda74f6b2f2ec0473accd6e3723abf80245215c Mon Sep 17 00:00:00 2001 From: Terence Date: Thu, 28 Jan 2021 18:50:34 -0500 Subject: [PATCH 100/183] clippify --- src/geometry/dual_quaternion_ops.rs | 4 ++-- tests/geometry/dual_quaternion.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/geometry/dual_quaternion_ops.rs b/src/geometry/dual_quaternion_ops.rs index 17644302..d6cb35b6 100644 --- a/src/geometry/dual_quaternion_ops.rs +++ b/src/geometry/dual_quaternion_ops.rs @@ -977,7 +977,7 @@ dual_quaternion_op_impl!( DivAssign, div_assign; (U4, U1), (U4, U1); self: UnitDualQuaternion, rhs: &'b UnitQuaternion; - #[allow(clippy::suspicious_arithmetic_impl)] + #[allow(clippy::suspicious_op_assign_impl)] { let res = &*self * UnitDualQuaternion::from_rotation(rhs.inverse()); self.as_mut_unchecked().real.coords.copy_from(&res.as_ref().real.coords); @@ -1013,7 +1013,7 @@ dual_quaternion_op_impl!( DivAssign, div_assign; (U4, U1), (U4, U1); self: UnitDualQuaternion, rhs: &'b Translation3; - #[allow(clippy::suspicious_arithmetic_impl)] + #[allow(clippy::suspicious_op_assign_impl)] { let res = &*self * UnitDualQuaternion::from_parts(rhs.inverse(), UnitQuaternion::identity()); self.as_mut_unchecked().real.coords.copy_from(&res.as_ref().real.coords); diff --git a/tests/geometry/dual_quaternion.rs b/tests/geometry/dual_quaternion.rs index 9cfca879..727e6aa1 100644 --- a/tests/geometry/dual_quaternion.rs +++ b/tests/geometry/dual_quaternion.rs @@ -25,7 +25,7 @@ quickcheck!( fn multiply_equals_alga_transform( dq: UnitDualQuaternion, v: Vector3, - p: Point3, + p: Point3 ) -> bool { dq * v == dq.transform_vector(&v) && dq * p == dq.transform_point(&p) From ac92e684862316e8c7df3eef7a37a8c120643ebe Mon Sep 17 00:00:00 2001 From: Terence Date: Thu, 28 Jan 2021 19:27:40 -0500 Subject: [PATCH 101/183] add new operators --- src/geometry/dual_quaternion_ops.rs | 128 ++++++++++++++++++++++ tests/geometry/dual_quaternion.rs | 159 ++++++++++++++++------------ 2 files changed, 222 insertions(+), 65 deletions(-) diff --git a/src/geometry/dual_quaternion_ops.rs b/src/geometry/dual_quaternion_ops.rs index d6cb35b6..2cf91b0f 100644 --- a/src/geometry/dual_quaternion_ops.rs +++ b/src/geometry/dual_quaternion_ops.rs @@ -242,6 +242,66 @@ dual_quaternion_op_impl!( self: DualQuaternion, rhs: DualQuaternion, Output = DualQuaternion; &self * &rhs; ); +// DualQuaternion × UnitDualQuaternion +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U4, U1); + self: &'a DualQuaternion, rhs: &'b UnitDualQuaternion, Output = DualQuaternion; + self * rhs.dual_quaternion(); + 'a, 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U4, U1); + self: &'a DualQuaternion, rhs: UnitDualQuaternion, Output = DualQuaternion; + self * rhs.dual_quaternion(); + 'a); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U4, U1); + self: DualQuaternion, rhs: &'b UnitDualQuaternion, Output = DualQuaternion; + self * rhs.dual_quaternion(); + 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U4, U1); + self: DualQuaternion, rhs: UnitDualQuaternion, Output = DualQuaternion; + self * rhs.dual_quaternion();); + +// DualQuaternion ÷ UnitDualQuaternion +dual_quaternion_op_impl!( + Div, div; + (U4, U1), (U4, U1); + self: &'a DualQuaternion, rhs: &'b UnitDualQuaternion, Output = DualQuaternion; + #[allow(clippy::suspicious_arithmetic_impl)] + { self * rhs.inverse().dual_quaternion() }; + 'a, 'b); + +dual_quaternion_op_impl!( + Div, div; + (U4, U1), (U4, U1); + self: &'a DualQuaternion, rhs: UnitDualQuaternion, Output = DualQuaternion; + #[allow(clippy::suspicious_arithmetic_impl)] + { self * rhs.inverse().dual_quaternion() }; + 'a); + +dual_quaternion_op_impl!( + Div, div; + (U4, U1), (U4, U1); + self: DualQuaternion, rhs: &'b UnitDualQuaternion, Output = DualQuaternion; + #[allow(clippy::suspicious_arithmetic_impl)] + { self * rhs.inverse().dual_quaternion() }; + 'b); + +dual_quaternion_op_impl!( + Div, div; + (U4, U1), (U4, U1); + self: DualQuaternion, rhs: UnitDualQuaternion, Output = DualQuaternion; + #[allow(clippy::suspicious_arithmetic_impl)] + { self * rhs.inverse().dual_quaternion() };); + // UnitDualQuaternion × UnitDualQuaternion dual_quaternion_op_impl!( Mul, mul; @@ -298,6 +358,38 @@ dual_quaternion_op_impl!( self: UnitDualQuaternion, rhs: UnitDualQuaternion, Output = UnitDualQuaternion; &self / &rhs; ); +// UnitDualQuaternion × DualQuaternion +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U4, U1); + self: &'a UnitDualQuaternion, rhs: &'b DualQuaternion, + Output = DualQuaternion => U1, U4; + self.dual_quaternion() * rhs; + 'a, 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U4, U1); + self: &'a UnitDualQuaternion, rhs: DualQuaternion, + Output = DualQuaternion => U3, U3; + self.dual_quaternion() * rhs; + 'a); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U4, U1); + self: UnitDualQuaternion, rhs: &'b DualQuaternion, + Output = DualQuaternion => U3, U3; + self.dual_quaternion() * rhs; + 'b); + +dual_quaternion_op_impl!( + Mul, mul; + (U4, U1), (U4, U1); + self: UnitDualQuaternion, rhs: DualQuaternion, + Output = DualQuaternion => U3, U3; + self.dual_quaternion() * rhs;); + // UnitDualQuaternion × UnitQuaternion dual_quaternion_op_impl!( Mul, mul; @@ -917,8 +1009,44 @@ dual_quaternion_op_impl!( MulAssign, mul_assign; (U4, U1), (U4, U1); self: DualQuaternion, rhs: DualQuaternion; + *self *= &rhs;); + +// DualQuaternion ×= UnitDualQuaternion +dual_quaternion_op_impl!( + MulAssign, mul_assign; + (U4, U1), (U4, U1); + self: DualQuaternion, rhs: &'b UnitDualQuaternion; + { + let res = &*self * rhs; + self.real.coords.copy_from(&res.real.coords); + self.dual.coords.copy_from(&res.dual.coords); + }; + 'b); + +dual_quaternion_op_impl!( + MulAssign, mul_assign; + (U4, U1), (U4, U1); + self: DualQuaternion, rhs: UnitDualQuaternion; *self *= &rhs; ); +// DualQuaternion ÷= UnitDualQuaternion +dual_quaternion_op_impl!( + DivAssign, div_assign; + (U4, U1), (U4, U1); + self: DualQuaternion, rhs: &'b UnitDualQuaternion; + { + let res = &*self / rhs; + self.real.coords.copy_from(&res.real.coords); + self.dual.coords.copy_from(&res.dual.coords); + }; + 'b); + +dual_quaternion_op_impl!( + DivAssign, div_assign; + (U4, U1), (U4, U1); + self: DualQuaternion, rhs: UnitDualQuaternion; + *self /= &rhs; ); + // UnitDualQuaternion ×= UnitDualQuaternion dual_quaternion_op_impl!( MulAssign, mul_assign; diff --git a/tests/geometry/dual_quaternion.rs b/tests/geometry/dual_quaternion.rs index 727e6aa1..c3254ce6 100644 --- a/tests/geometry/dual_quaternion.rs +++ b/tests/geometry/dual_quaternion.rs @@ -1,7 +1,7 @@ #![cfg(feature = "arbitrary")] #![allow(non_snake_case)] -use na::{Isometry3, Point3, Translation3, UnitDualQuaternion, UnitQuaternion, Vector3}; +use na::{DualQuaternion, Isometry3, Point3, Translation3, UnitDualQuaternion, UnitQuaternion, Vector3}; quickcheck!( fn isometry_equivalence(iso: Isometry3, p: Point3, v: Vector3) -> bool { @@ -68,61 +68,86 @@ quickcheck!( #[cfg_attr(rustfmt, rustfmt_skip)] fn all_op_exist( - dq: UnitDualQuaternion, + dq: DualQuaternion, + udq: UnitDualQuaternion, uq: UnitQuaternion, + s: f64, t: Translation3, v: Vector3, p: Point3 ) -> bool { - let iMi = dq * dq; - let iMuq = dq * uq; - let iDi = dq / dq; - let iDuq = dq / uq; + let dqMs: DualQuaternion<_> = dq * s; - let iMp = dq * p; - let iMv = dq * v; + let dqMdq: DualQuaternion<_> = dq * dq; + let dqMudq: DualQuaternion<_> = dq * udq; + let udqMdq: DualQuaternion<_> = udq * dq; - let iMt = dq * t; - let tMi = t * dq; + let iMi: UnitDualQuaternion<_> = udq * udq; + let iMuq: UnitDualQuaternion<_> = udq * uq; + let iDi: UnitDualQuaternion<_> = udq / udq; + let iDuq: UnitDualQuaternion<_> = udq / uq; - let tMuq = t * uq; + let iMp: Point3<_> = udq * p; + let iMv: Vector3<_> = udq * v; - let uqMi = uq * dq; - let uqDi = uq / dq; + let iMt: UnitDualQuaternion<_> = udq * t; + let tMi: UnitDualQuaternion<_> = t * udq; - let uqMt = uq * t; + let uqMi: UnitDualQuaternion<_> = uq * udq; + let uqDi: UnitDualQuaternion<_> = uq / udq; - let mut iMt1 = dq; - let mut iMt2 = dq; + let mut dqMs1 = dq; - let mut iMi1 = dq; - let mut iMi2 = dq; + let mut dqMdq1 = dq; + let mut dqMdq2 = dq; - let mut iMuq1 = dq; - let mut iMuq2 = dq; + let mut dqMudq1 = dq; + let mut dqMudq2 = dq; - let mut iDi1 = dq; - let mut iDi2 = dq; + let mut iMt1 = udq; + let mut iMt2 = udq; - let mut iDuq1 = dq; - let mut iDuq2 = dq; + let mut iMi1 = udq; + let mut iMi2 = udq; + + let mut iMuq1 = udq; + let mut iMuq2 = udq; + + let mut iDi1 = udq; + let mut iDi2 = udq; + + let mut iDuq1 = udq; + let mut iDuq2 = udq; + + dqMs1 *= s; + + dqMdq1 *= dq; + dqMdq2 *= &dq; + + dqMudq1 *= udq; + dqMudq2 *= &udq; iMt1 *= t; iMt2 *= &t; - iMi1 *= dq; - iMi2 *= &dq; + iMi1 *= udq; + iMi2 *= &udq; iMuq1 *= uq; iMuq2 *= &uq; - iDi1 /= dq; - iDi2 /= &dq; + iDi1 /= udq; + iDi2 /= &udq; iDuq1 /= uq; iDuq2 /= &uq; - iMt == iMt1 + dqMs == dqMs1 + && dqMdq == dqMdq1 + && dqMdq == dqMdq2 + && dqMudq == dqMudq1 + && dqMudq == dqMudq2 + && iMt == iMt1 && iMt == iMt2 && iMi == iMi1 && iMi == iMi2 @@ -132,41 +157,45 @@ quickcheck!( && iDi == iDi2 && iDuq == iDuq1 && iDuq == iDuq2 - && iMi == &dq * &dq - && iMi == dq * &dq - && iMi == &dq * dq - && iMuq == &dq * &uq - && iMuq == dq * &uq - && iMuq == &dq * uq - && iDi == &dq / &dq - && iDi == dq / &dq - && iDi == &dq / dq - && iDuq == &dq / &uq - && iDuq == dq / &uq - && iDuq == &dq / uq - && iMp == &dq * &p - && iMp == dq * &p - && iMp == &dq * p - && iMv == &dq * &v - && iMv == dq * &v - && iMv == &dq * v - && iMt == &dq * &t - && iMt == dq * &t - && iMt == &dq * t - && tMi == &t * &dq - && tMi == t * &dq - && tMi == &t * dq - && tMuq == &t * &uq - && tMuq == t * &uq - && tMuq == &t * uq - && uqMi == &uq * &dq - && uqMi == uq * &dq - && uqMi == &uq * dq - && uqDi == &uq / &dq - && uqDi == uq / &dq - && uqDi == &uq / dq - && uqMt == &uq * &t - && uqMt == uq * &t - && uqMt == &uq * t + && dqMs == &dq * s + && dqMdq == &dq * &dq + && dqMdq == dq * &dq + && dqMdq == &dq * dq + && dqMudq == &dq * &udq + && dqMudq == dq * &udq + && dqMudq == &dq * udq + && udqMdq == &udq * &dq + && udqMdq == udq * &dq + && udqMdq == &udq * dq + && iMi == &udq * &udq + && iMi == udq * &udq + && iMi == &udq * udq + && iMuq == &udq * &uq + && iMuq == udq * &uq + && iMuq == &udq * uq + && iDi == &udq / &udq + && iDi == udq / &udq + && iDi == &udq / udq + && iDuq == &udq / &uq + && iDuq == udq / &uq + && iDuq == &udq / uq + && iMp == &udq * &p + && iMp == udq * &p + && iMp == &udq * p + && iMv == &udq * &v + && iMv == udq * &v + && iMv == &udq * v + && iMt == &udq * &t + && iMt == udq * &t + && iMt == &udq * t + && tMi == &t * &udq + && tMi == t * &udq + && tMi == &t * udq + && uqMi == &uq * &udq + && uqMi == uq * &udq + && uqMi == &uq * udq + && uqDi == &uq / &udq + && uqDi == uq / &udq + && uqDi == &uq / udq } ); From 8c52e4dfdcef04c164a568fc1d0b35243aeb4542 Mon Sep 17 00:00:00 2001 From: Terence Date: Thu, 28 Jan 2021 19:29:12 -0500 Subject: [PATCH 102/183] rustfmt --- tests/geometry/dual_quaternion.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/geometry/dual_quaternion.rs b/tests/geometry/dual_quaternion.rs index c3254ce6..2884f7f4 100644 --- a/tests/geometry/dual_quaternion.rs +++ b/tests/geometry/dual_quaternion.rs @@ -1,7 +1,9 @@ #![cfg(feature = "arbitrary")] #![allow(non_snake_case)] -use na::{DualQuaternion, Isometry3, Point3, Translation3, UnitDualQuaternion, UnitQuaternion, Vector3}; +use na::{ + DualQuaternion, Isometry3, Point3, Translation3, UnitDualQuaternion, UnitQuaternion, Vector3, +}; quickcheck!( fn isometry_equivalence(iso: Isometry3, p: Point3, v: Vector3) -> bool { @@ -22,6 +24,7 @@ quickcheck!( && relative_eq!((ii * i) * v, v, epsilon = 1.0e-7) } + #[cfg_attr(rustfmt, rustfmt_skip)] fn multiply_equals_alga_transform( dq: UnitDualQuaternion, v: Vector3, From c408e09e28471c8554a40f02583f52594c75cfa8 Mon Sep 17 00:00:00 2001 From: Terence Date: Thu, 28 Jan 2021 19:41:55 -0500 Subject: [PATCH 103/183] update header comment for operators --- src/geometry/dual_quaternion_ops.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/geometry/dual_quaternion_ops.rs b/src/geometry/dual_quaternion_ops.rs index 2cf91b0f..a9e83764 100644 --- a/src/geometry/dual_quaternion_ops.rs +++ b/src/geometry/dual_quaternion_ops.rs @@ -10,10 +10,32 @@ * * (Assignment Operators) * + * -DualQuaternion * DualQuaternion × Scalar * DualQuaternion × DualQuaternion * DualQuaternion + DualQuaternion * DualQuaternion - DualQuaternion + * DualQuaternion × UnitDualQuaternion + * DualQuaternion ÷ UnitDualQuaternion + * -UnitDualQuaternion + * UnitDualQuaternion × DualQuaternion + * UnitDualQuaternion × UnitDualQuaternion + * UnitDualQuaternion ÷ UnitDualQuaternion + * UnitDualQuaternion × Translation3 + * UnitDualQuaternion ÷ Translation3 + * UnitDualQuaternion × UnitQuaternion + * UnitDualQuaternion ÷ UnitQuaternion + * Translation3 × UnitDualQuaternion + * Translation3 ÷ UnitDualQuaternion + * UnitQuaternion × UnitDualQuaternion + * UnitQuaternion ÷ UnitDualQuaternion + * UnitDualQuaternion × Isometry3 + * UnitDualQuaternion ÷ Isometry3 + * Isometry3 × UnitDualQuaternion + * Isometry3 ÷ UnitDualQuaternion + * UnitDualQuaternion × Point + * UnitDualQuaternion × Vector + * UnitDualQuaternion × Unit * * --- * @@ -703,7 +725,7 @@ dual_quaternion_op_impl!( Output = UnitDualQuaternion => U3, U1; self * UnitDualQuaternion::::from_isometry(&rhs); ); -// UnitDualQuaternion ÷ Isometry +// UnitDualQuaternion ÷ Isometry3 dual_quaternion_op_impl!( Div, div; (U4, U1), (U3, U1); From 7d5cc4912dc8928e5d476d75a6b9f430468b0cdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Fri, 29 Jan 2021 13:33:37 +0100 Subject: [PATCH 104/183] Update cargo badge and categories. --- CHANGELOG.md | 2 +- Cargo.toml | 14 ++++++++------ nalgebra-glm/Cargo.toml | 9 ++++++--- nalgebra-lapack/Cargo.toml | 20 ++++++++++++-------- tests/geometry/projection.rs | 2 +- 5 files changed, 28 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfc0e080..d9825cd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,7 +67,7 @@ In this release, we are no longer relying on traits from the __alga__ crate for Instead, we use traits from the new [simba](https://crates.io/crates/simba) crate which are both simpler, and allow for significant optimizations like AoSoA SIMD. -Refer to the [monthly Rustsim blogpost](https://www.rustsim.org/blog/2020/04/01/this-month-in-rustsim/) +Refer to the [monthly dimforge blogpost](https://www.dimforge.org/blog/2020/04/01/this-month-in-dimforge/) for details about this switch and its benefits. ### Added diff --git a/Cargo.toml b/Cargo.toml index 615942a8..c3ecd3e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,18 +3,20 @@ name = "nalgebra" version = "0.24.0" authors = [ "Sébastien Crozet " ] -description = "Linear algebra library with transformations and statically-sized or dynamically-sized matrices." -documentation = "https://nalgebra.org/rustdoc/nalgebra/index.html" +description = "General-purpose linear algebra library with transformations and statically-sized or dynamically-sized matrices." +documentation = "https://www.nalgebra.org/docs" homepage = "https://nalgebra.org" -repository = "https://github.com/rustsim/nalgebra" +repository = "https://github.com/dimforge/nalgebra" readme = "README.md" -categories = [ "science" ] +categories = [ "science", "mathematics", "wasm", "no standard library" ] keywords = [ "linear", "algebra", "matrix", "vector", "math" ] -license = "Apache-2.0" +license = "BSD-3-Clause" edition = "2018" - exclude = ["/ci/*", "/.travis.yml", "/Makefile"] +[badges] +maintenance = { status = "actively-developed" } + [lib] name = "nalgebra" path = "src/lib.rs" diff --git a/nalgebra-glm/Cargo.toml b/nalgebra-glm/Cargo.toml index 6cbf7ab7..97e7f8b3 100644 --- a/nalgebra-glm/Cargo.toml +++ b/nalgebra-glm/Cargo.toml @@ -4,15 +4,18 @@ version = "0.10.0" authors = ["sebcrozet "] description = "A computer-graphics oriented API for nalgebra, inspired by the C++ GLM library." -documentation = "https://www.nalgebra.org/rustdoc_glm/nalgebra_glm/index.html" +documentation = "https://www.nalgebra.org/docs" homepage = "https://nalgebra.org" -repository = "https://github.com/rustsim/nalgebra" +repository = "https://github.com/dimforge/nalgebra" readme = "../README.md" -categories = [ "science" ] +categories = [ "science", "mathematics", "wasm", "no standard library" ] keywords = [ "linear", "algebra", "matrix", "vector", "math" ] license = "BSD-3-Clause" edition = "2018" +[badges] +maintenance = { status = "actively-developed" } + [features] default = [ "std" ] std = [ "nalgebra/std", "simba/std" ] diff --git a/nalgebra-lapack/Cargo.toml b/nalgebra-lapack/Cargo.toml index 1ea68287..58c92dd0 100644 --- a/nalgebra-lapack/Cargo.toml +++ b/nalgebra-lapack/Cargo.toml @@ -3,14 +3,18 @@ name = "nalgebra-lapack" version = "0.15.0" authors = [ "Sébastien Crozet ", "Andrew Straw " ] -description = "Linear algebra library with transformations and satically-sized or dynamically-sized matrices." -documentation = "https://nalgebra.org/doc/nalgebra/index.html" -homepage = "https://nalgebra.org" -repository = "https://github.com/rustsim/nalgebra" -readme = "README.md" -keywords = [ "linear", "algebra", "matrix", "vector" ] -license = "BSD-3-Clause" -edition = "2018" +description = "Matrix decompositions using nalgebra matrices and Lapack bindings." +documentation = "https://www.nalgebra.org/docs" +homepage = "https://nalgebra.org" +repository = "https://github.com/dimforge/nalgebra" +readme = "../README.md" +categories = [ "science", "mathematics" ] +keywords = [ "linear", "algebra", "matrix", "vector", "math", "lapack" ] +license = "BSD-3-Clause" +edition = "2018" + +[badges] +maintenance = { status = "actively-developed" } [features] serde-serialize = [ "serde", "serde_derive" ] diff --git a/tests/geometry/projection.rs b/tests/geometry/projection.rs index 626c4ffb..e4081996 100644 --- a/tests/geometry/projection.rs +++ b/tests/geometry/projection.rs @@ -22,7 +22,7 @@ fn orthographic_inverse() { #[test] fn perspective_matrix_point_transformation() { - // https://github.com/rustsim/nalgebra/issues/640 + // https://github.com/dimforge/nalgebra/issues/640 let proj = Perspective3::new(4.0 / 3.0, 90.0, 0.1, 100.0); let perspective_inv = proj.as_matrix().try_inverse().unwrap(); let some_point = Point3::new(1.0, 2.0, 0.0); From fb26d4d0fb79d1c2d48905ee943328d74a25013a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Fri, 29 Jan 2021 13:56:40 +0100 Subject: [PATCH 105/183] Update the htaml_root_url. --- src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 12a329be..7b2f08a6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,7 +15,8 @@ Simply add the following to your `Cargo.toml` file: ```.ignore [dependencies] -nalgebra = "0.23" +// TODO: replace the * by the latest version. +nalgebra = "*" ``` @@ -82,7 +83,7 @@ an optimized set of tools for computer graphics and physics. Those features incl #![deny(missing_docs)] #![doc( html_favicon_url = "https://nalgebra.org/img/favicon.ico", - html_root_url = "https://nalgebra.org/rustdoc" + html_root_url = "https://docs.rs/nalgebra/0.24.0" )] #![cfg_attr(not(feature = "std"), no_std)] #![cfg_attr(all(feature = "alloc", not(feature = "std")), feature(alloc))] From d45e6eafab96c17a2dcd1b11b92c811845a9c696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Fri, 29 Jan 2021 13:57:19 +0100 Subject: [PATCH 106/183] Release v0.24.1 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c3ecd3e3..7d5a9db9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nalgebra" -version = "0.24.0" +version = "0.24.1" authors = [ "Sébastien Crozet " ] description = "General-purpose linear algebra library with transformations and statically-sized or dynamically-sized matrices." From 8e04f4db56e170317d0846f7878ced7d5ccb71e2 Mon Sep 17 00:00:00 2001 From: paq <89paku@gmail.com> Date: Sun, 31 Jan 2021 06:15:51 -0900 Subject: [PATCH 107/183] Fix rustdoc link --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a885855b..08350907 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@

- Users guide | Documentation | Forum + Users guide | Documentation | Forum

@@ -36,4 +36,4 @@ Rapier is supported by: -

\ No newline at end of file +

From 2d11b90149c4ed30961b6db48719515de3fbb6df Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 1 Feb 2021 08:41:37 +0100 Subject: [PATCH 108/183] Address review concerns: doc link, Csr/CScMatrix::pattern docs --- nalgebra-sparse/src/csc.rs | 6 +----- nalgebra-sparse/src/csr.rs | 6 +----- nalgebra-sparse/src/lib.rs | 2 +- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/nalgebra-sparse/src/csc.rs b/nalgebra-sparse/src/csc.rs index 89cb88cd..fb454a43 100644 --- a/nalgebra-sparse/src/csc.rs +++ b/nalgebra-sparse/src/csc.rs @@ -369,11 +369,7 @@ impl CscMatrix { self.cs.pattern_and_values_mut() } - /// Returns the underlying sparsity pattern. - /// - /// The sparsity pattern is stored internally inside an `Arc`. This allows users to re-use - /// the same sparsity pattern for multiple matrices without storing the same pattern multiple - /// times in memory. + /// Returns a reference to the underlying sparsity pattern. pub fn pattern(&self) -> &SparsityPattern { self.cs.pattern() } diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index 24c45e74..2e95460f 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -371,11 +371,7 @@ impl CsrMatrix { self.cs.pattern_and_values_mut() } - /// Returns the underlying sparsity pattern. - /// - /// The sparsity pattern is stored internally inside an `Arc`. This allows users to re-use - /// the same sparsity pattern for multiple matrices without storing the same pattern multiple - /// times in memory. + /// Returns a reference to the underlying sparsity pattern. pub fn pattern(&self) -> &SparsityPattern { self.cs.pattern() } diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index 1de835c7..a2783545 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -55,7 +55,7 @@ //! | ------------------------|--------------------------------------------- | //! | [COO](`coo::CooMatrix`) | Well-suited for matrix construction.
Ill-suited for algebraic operations. | //! | [CSR](`csr::CsrMatrix`) | Immutable sparsity pattern, suitable for algebraic operations.
Fast row access. | -//! | [CSC](`csr::CscMatrix`) | Immutable sparsity pattern, suitable for algebraic operations.
Fast column access. | +//! | [CSC](`csc::CscMatrix`) | Immutable sparsity pattern, suitable for algebraic operations.
Fast column access. | //! //! What format is best to use depends on the application. The most common use case for sparse //! matrices in science is the solution of sparse linear systems. Here we can differentiate between From 0936c4fad9f59749ec1d99cab8f4b04e1b0d81b8 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 1 Feb 2021 08:52:13 +0100 Subject: [PATCH 109/183] Add tests for Csr/CscMatrix::identity --- nalgebra-sparse/tests/unit_tests/csc.rs | 7 +++++++ nalgebra-sparse/tests/unit_tests/csr.rs | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/nalgebra-sparse/tests/unit_tests/csc.rs b/nalgebra-sparse/tests/unit_tests/csc.rs index b27f19be..534adb4f 100644 --- a/nalgebra-sparse/tests/unit_tests/csc.rs +++ b/nalgebra-sparse/tests/unit_tests/csc.rs @@ -375,4 +375,11 @@ proptest! { prop_assert_eq!(d_entries, csc_diagonal_entries); } + + #[test] + fn csc_identity(n in 0 ..= 6usize) { + let csc = CscMatrix::::identity(n); + prop_assert_eq!(csc.nnz(), n); + prop_assert_eq!(DMatrix::from(&csc), DMatrix::identity(n, n)); + } } diff --git a/nalgebra-sparse/tests/unit_tests/csr.rs b/nalgebra-sparse/tests/unit_tests/csr.rs index 2de93e51..d1e62bed 100644 --- a/nalgebra-sparse/tests/unit_tests/csr.rs +++ b/nalgebra-sparse/tests/unit_tests/csr.rs @@ -375,4 +375,11 @@ proptest! { prop_assert_eq!(d_entries, csr_diagonal_entries); } + + #[test] + fn csr_identity(n in 0 ..= 6usize) { + let csr = CsrMatrix::::identity(n); + prop_assert_eq!(csr.nnz(), n); + prop_assert_eq!(DMatrix::from(&csr), DMatrix::identity(n, n)); + } } From 1ebb612d46d115b5e6319b3c977505990d2d72fe Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 1 Feb 2021 09:27:33 +0100 Subject: [PATCH 110/183] Test Csr/CscMatrix::{index_entry, index_entry_mut, get_entry, get_entry_mut} --- nalgebra-sparse/tests/unit_tests/csc.rs | 93 ++++++++++++++++++++++++- nalgebra-sparse/tests/unit_tests/csr.rs | 93 ++++++++++++++++++++++++- 2 files changed, 180 insertions(+), 6 deletions(-) diff --git a/nalgebra-sparse/tests/unit_tests/csc.rs b/nalgebra-sparse/tests/unit_tests/csc.rs index 534adb4f..45158d65 100644 --- a/nalgebra-sparse/tests/unit_tests/csc.rs +++ b/nalgebra-sparse/tests/unit_tests/csc.rs @@ -1,10 +1,11 @@ use nalgebra::DMatrix; use nalgebra_sparse::csc::CscMatrix; -use nalgebra_sparse::SparseFormatErrorKind; +use nalgebra_sparse::{SparseEntry, SparseEntryMut, SparseFormatErrorKind}; use proptest::prelude::*; use proptest::sample::subsequence; +use crate::assert_panics; use crate::common::csc_strategy; use std::collections::HashSet; @@ -298,9 +299,95 @@ fn csc_disassemble_avoids_clone_when_owned() { assert_eq!(values.as_ptr(), values_ptr); } +// Rustfmt makes this test much harder to read by expanding some of the one-liners to 4-liners, +// so for now we skip rustfmt... +#[rustfmt::skip] #[test] -fn csc_matrix_get_index() { - // TODO: Implement tests for ::get() and index() +fn csc_matrix_get_index_entry() { + // Test .get_entry(_mut) and .index_entry(_mut) methods + + #[rustfmt::skip] + let dense = DMatrix::from_row_slice(2, 3, &[ + 1, 0, 3, + 0, 5, 6 + ]); + let csc = CscMatrix::from(&dense); + + assert_eq!(csc.get_entry(0, 0), Some(SparseEntry::NonZero(&1))); + assert_eq!(csc.index_entry(0, 0), SparseEntry::NonZero(&1)); + assert_eq!(csc.get_entry(0, 1), Some(SparseEntry::Zero)); + assert_eq!(csc.index_entry(0, 1), SparseEntry::Zero); + assert_eq!(csc.get_entry(0, 2), Some(SparseEntry::NonZero(&3))); + assert_eq!(csc.index_entry(0, 2), SparseEntry::NonZero(&3)); + assert_eq!(csc.get_entry(1, 0), Some(SparseEntry::Zero)); + assert_eq!(csc.index_entry(1, 0), SparseEntry::Zero); + assert_eq!(csc.get_entry(1, 1), Some(SparseEntry::NonZero(&5))); + assert_eq!(csc.index_entry(1, 1), SparseEntry::NonZero(&5)); + assert_eq!(csc.get_entry(1, 2), Some(SparseEntry::NonZero(&6))); + assert_eq!(csc.index_entry(1, 2), SparseEntry::NonZero(&6)); + + // Check some out of bounds with .get_entry + assert_eq!(csc.get_entry(0, 3), None); + assert_eq!(csc.get_entry(0, 4), None); + assert_eq!(csc.get_entry(1, 3), None); + assert_eq!(csc.get_entry(1, 4), None); + assert_eq!(csc.get_entry(2, 0), None); + assert_eq!(csc.get_entry(2, 1), None); + assert_eq!(csc.get_entry(2, 2), None); + assert_eq!(csc.get_entry(2, 3), None); + assert_eq!(csc.get_entry(2, 4), None); + + // Check that out of bounds with .index_entry panics + assert_panics!(csc.index_entry(0, 3)); + assert_panics!(csc.index_entry(0, 4)); + assert_panics!(csc.index_entry(1, 3)); + assert_panics!(csc.index_entry(1, 4)); + assert_panics!(csc.index_entry(2, 0)); + assert_panics!(csc.index_entry(2, 1)); + assert_panics!(csc.index_entry(2, 2)); + assert_panics!(csc.index_entry(2, 3)); + assert_panics!(csc.index_entry(2, 4)); + + { + // Check mutable versions of the above functions + let mut csc = csc; + + assert_eq!(csc.get_entry_mut(0, 0), Some(SparseEntryMut::NonZero(&mut 1))); + assert_eq!(csc.index_entry_mut(0, 0), SparseEntryMut::NonZero(&mut 1)); + assert_eq!(csc.get_entry_mut(0, 1), Some(SparseEntryMut::Zero)); + assert_eq!(csc.index_entry_mut(0, 1), SparseEntryMut::Zero); + assert_eq!(csc.get_entry_mut(0, 2), Some(SparseEntryMut::NonZero(&mut 3))); + assert_eq!(csc.index_entry_mut(0, 2), SparseEntryMut::NonZero(&mut 3)); + assert_eq!(csc.get_entry_mut(1, 0), Some(SparseEntryMut::Zero)); + assert_eq!(csc.index_entry_mut(1, 0), SparseEntryMut::Zero); + assert_eq!(csc.get_entry_mut(1, 1), Some(SparseEntryMut::NonZero(&mut 5))); + assert_eq!(csc.index_entry_mut(1, 1), SparseEntryMut::NonZero(&mut 5)); + assert_eq!(csc.get_entry_mut(1, 2), Some(SparseEntryMut::NonZero(&mut 6))); + assert_eq!(csc.index_entry_mut(1, 2), SparseEntryMut::NonZero(&mut 6)); + + // Check some out of bounds with .get_entry_mut + assert_eq!(csc.get_entry_mut(0, 3), None); + assert_eq!(csc.get_entry_mut(0, 4), None); + assert_eq!(csc.get_entry_mut(1, 3), None); + assert_eq!(csc.get_entry_mut(1, 4), None); + assert_eq!(csc.get_entry_mut(2, 0), None); + assert_eq!(csc.get_entry_mut(2, 1), None); + assert_eq!(csc.get_entry_mut(2, 2), None); + assert_eq!(csc.get_entry_mut(2, 3), None); + assert_eq!(csc.get_entry_mut(2, 4), None); + + // Check that out of bounds with .index_entry_mut panics + // Note: the cloning is necessary because a mutable reference is not UnwindSafe + assert_panics!({ let mut csc = csc.clone(); csc.index_entry_mut(0, 3); }); + assert_panics!({ let mut csc = csc.clone(); csc.index_entry_mut(0, 4); }); + assert_panics!({ let mut csc = csc.clone(); csc.index_entry_mut(1, 3); }); + assert_panics!({ let mut csc = csc.clone(); csc.index_entry_mut(1, 4); }); + assert_panics!({ let mut csc = csc.clone(); csc.index_entry_mut(2, 0); }); + assert_panics!({ let mut csc = csc.clone(); csc.index_entry_mut(2, 1); }); + assert_panics!({ let mut csc = csc.clone(); csc.index_entry_mut(2, 2); }); + assert_panics!({ let mut csc = csc.clone(); csc.index_entry_mut(2, 3); }); + assert_panics!({ let mut csc = csc.clone(); csc.index_entry_mut(2, 4); }); + } } #[test] diff --git a/nalgebra-sparse/tests/unit_tests/csr.rs b/nalgebra-sparse/tests/unit_tests/csr.rs index d1e62bed..2fe3c910 100644 --- a/nalgebra-sparse/tests/unit_tests/csr.rs +++ b/nalgebra-sparse/tests/unit_tests/csr.rs @@ -1,10 +1,11 @@ use nalgebra::DMatrix; use nalgebra_sparse::csr::CsrMatrix; -use nalgebra_sparse::SparseFormatErrorKind; +use nalgebra_sparse::{SparseEntry, SparseEntryMut, SparseFormatErrorKind}; use proptest::prelude::*; use proptest::sample::subsequence; +use crate::assert_panics; use crate::common::csr_strategy; use std::collections::HashSet; @@ -298,9 +299,95 @@ fn csr_disassemble_avoids_clone_when_owned() { assert_eq!(values.as_ptr(), values_ptr); } +// Rustfmt makes this test much harder to read by expanding some of the one-liners to 4-liners, +// so for now we skip rustfmt... +#[rustfmt::skip] #[test] -fn csr_matrix_get_index() { - // TODO: Implement tests for ::get() and index() +fn csr_matrix_get_index_entry() { + // Test .get_entry(_mut) and .index_entry(_mut) methods + + #[rustfmt::skip] + let dense = DMatrix::from_row_slice(2, 3, &[ + 1, 0, 3, + 0, 5, 6 + ]); + let csr = CsrMatrix::from(&dense); + + assert_eq!(csr.get_entry(0, 0), Some(SparseEntry::NonZero(&1))); + assert_eq!(csr.index_entry(0, 0), SparseEntry::NonZero(&1)); + assert_eq!(csr.get_entry(0, 1), Some(SparseEntry::Zero)); + assert_eq!(csr.index_entry(0, 1), SparseEntry::Zero); + assert_eq!(csr.get_entry(0, 2), Some(SparseEntry::NonZero(&3))); + assert_eq!(csr.index_entry(0, 2), SparseEntry::NonZero(&3)); + assert_eq!(csr.get_entry(1, 0), Some(SparseEntry::Zero)); + assert_eq!(csr.index_entry(1, 0), SparseEntry::Zero); + assert_eq!(csr.get_entry(1, 1), Some(SparseEntry::NonZero(&5))); + assert_eq!(csr.index_entry(1, 1), SparseEntry::NonZero(&5)); + assert_eq!(csr.get_entry(1, 2), Some(SparseEntry::NonZero(&6))); + assert_eq!(csr.index_entry(1, 2), SparseEntry::NonZero(&6)); + + // Check some out of bounds with .get_entry + assert_eq!(csr.get_entry(0, 3), None); + assert_eq!(csr.get_entry(0, 4), None); + assert_eq!(csr.get_entry(1, 3), None); + assert_eq!(csr.get_entry(1, 4), None); + assert_eq!(csr.get_entry(2, 0), None); + assert_eq!(csr.get_entry(2, 1), None); + assert_eq!(csr.get_entry(2, 2), None); + assert_eq!(csr.get_entry(2, 3), None); + assert_eq!(csr.get_entry(2, 4), None); + + // Check that out of bounds with .index_entry panics + assert_panics!(csr.index_entry(0, 3)); + assert_panics!(csr.index_entry(0, 4)); + assert_panics!(csr.index_entry(1, 3)); + assert_panics!(csr.index_entry(1, 4)); + assert_panics!(csr.index_entry(2, 0)); + assert_panics!(csr.index_entry(2, 1)); + assert_panics!(csr.index_entry(2, 2)); + assert_panics!(csr.index_entry(2, 3)); + assert_panics!(csr.index_entry(2, 4)); + + { + // Check mutable versions of the above functions + let mut csr = csr; + + assert_eq!(csr.get_entry_mut(0, 0), Some(SparseEntryMut::NonZero(&mut 1))); + assert_eq!(csr.index_entry_mut(0, 0), SparseEntryMut::NonZero(&mut 1)); + assert_eq!(csr.get_entry_mut(0, 1), Some(SparseEntryMut::Zero)); + assert_eq!(csr.index_entry_mut(0, 1), SparseEntryMut::Zero); + assert_eq!(csr.get_entry_mut(0, 2), Some(SparseEntryMut::NonZero(&mut 3))); + assert_eq!(csr.index_entry_mut(0, 2), SparseEntryMut::NonZero(&mut 3)); + assert_eq!(csr.get_entry_mut(1, 0), Some(SparseEntryMut::Zero)); + assert_eq!(csr.index_entry_mut(1, 0), SparseEntryMut::Zero); + assert_eq!(csr.get_entry_mut(1, 1), Some(SparseEntryMut::NonZero(&mut 5))); + assert_eq!(csr.index_entry_mut(1, 1), SparseEntryMut::NonZero(&mut 5)); + assert_eq!(csr.get_entry_mut(1, 2), Some(SparseEntryMut::NonZero(&mut 6))); + assert_eq!(csr.index_entry_mut(1, 2), SparseEntryMut::NonZero(&mut 6)); + + // Check some out of bounds with .get_entry_mut + assert_eq!(csr.get_entry_mut(0, 3), None); + assert_eq!(csr.get_entry_mut(0, 4), None); + assert_eq!(csr.get_entry_mut(1, 3), None); + assert_eq!(csr.get_entry_mut(1, 4), None); + assert_eq!(csr.get_entry_mut(2, 0), None); + assert_eq!(csr.get_entry_mut(2, 1), None); + assert_eq!(csr.get_entry_mut(2, 2), None); + assert_eq!(csr.get_entry_mut(2, 3), None); + assert_eq!(csr.get_entry_mut(2, 4), None); + + // Check that out of bounds with .index_entry_mut panics + // Note: the cloning is necessary because a mutable reference is not UnwindSafe + assert_panics!({ let mut csr = csr.clone(); csr.index_entry_mut(0, 3); }); + assert_panics!({ let mut csr = csr.clone(); csr.index_entry_mut(0, 4); }); + assert_panics!({ let mut csr = csr.clone(); csr.index_entry_mut(1, 3); }); + assert_panics!({ let mut csr = csr.clone(); csr.index_entry_mut(1, 4); }); + assert_panics!({ let mut csr = csr.clone(); csr.index_entry_mut(2, 0); }); + assert_panics!({ let mut csr = csr.clone(); csr.index_entry_mut(2, 1); }); + assert_panics!({ let mut csr = csr.clone(); csr.index_entry_mut(2, 2); }); + assert_panics!({ let mut csr = csr.clone(); csr.index_entry_mut(2, 3); }); + assert_panics!({ let mut csr = csr.clone(); csr.index_entry_mut(2, 4); }); + } } #[test] From 66b9185ec13415d44a3081af76e5235ed96f6264 Mon Sep 17 00:00:00 2001 From: Andreas Longva Date: Mon, 1 Feb 2021 14:34:22 +0100 Subject: [PATCH 111/183] Test CsrMatrix::row_iter(_mut) and CscMatrix::col_iter(_mut) --- nalgebra-sparse/tests/unit_tests/csc.rs | 135 +++++++++++++++++++++++- nalgebra-sparse/tests/unit_tests/csr.rs | 131 ++++++++++++++++++++++- 2 files changed, 264 insertions(+), 2 deletions(-) diff --git a/nalgebra-sparse/tests/unit_tests/csc.rs b/nalgebra-sparse/tests/unit_tests/csc.rs index 45158d65..7fb0de54 100644 --- a/nalgebra-sparse/tests/unit_tests/csc.rs +++ b/nalgebra-sparse/tests/unit_tests/csc.rs @@ -392,7 +392,140 @@ fn csc_matrix_get_index_entry() { #[test] fn csc_matrix_col_iter() { - // TODO + // Note: this is the transpose of the matrix used for the similar csr_matrix_row_iter test + // (this way the actual tests are almost identical, due to the transposed relationship + // between CSR and CSC) + #[rustfmt::skip] + let dense = DMatrix::from_row_slice(4, 3, &[ + 0, 3, 0, + 1, 0, 4, + 2, 0, 0, + 0, 0, 5, + ]); + let csc = CscMatrix::from(&dense); + + // Immutable iterator + { + let mut col_iter = csc.col_iter(); + + { + let col = col_iter.next().unwrap(); + assert_eq!(col.nrows(), 4); + assert_eq!(col.nnz(), 2); + assert_eq!(col.row_indices(), &[1, 2]); + assert_eq!(col.values(), &[1, 2]); + assert_eq!(col.get_entry(0), Some(SparseEntry::Zero)); + assert_eq!(col.get_entry(1), Some(SparseEntry::NonZero(&1))); + assert_eq!(col.get_entry(2), Some(SparseEntry::NonZero(&2))); + assert_eq!(col.get_entry(3), Some(SparseEntry::Zero)); + assert_eq!(col.get_entry(4), None); + } + + { + let col = col_iter.next().unwrap(); + assert_eq!(col.nrows(), 4); + assert_eq!(col.nnz(), 1); + assert_eq!(col.row_indices(), &[0]); + assert_eq!(col.values(), &[3]); + assert_eq!(col.get_entry(0), Some(SparseEntry::NonZero(&3))); + assert_eq!(col.get_entry(1), Some(SparseEntry::Zero)); + assert_eq!(col.get_entry(2), Some(SparseEntry::Zero)); + assert_eq!(col.get_entry(3), Some(SparseEntry::Zero)); + assert_eq!(col.get_entry(4), None); + } + + { + let col = col_iter.next().unwrap(); + assert_eq!(col.nrows(), 4); + assert_eq!(col.nnz(), 2); + assert_eq!(col.row_indices(), &[1, 3]); + assert_eq!(col.values(), &[4, 5]); + assert_eq!(col.get_entry(0), Some(SparseEntry::Zero)); + assert_eq!(col.get_entry(1), Some(SparseEntry::NonZero(&4))); + assert_eq!(col.get_entry(2), Some(SparseEntry::Zero)); + assert_eq!(col.get_entry(3), Some(SparseEntry::NonZero(&5))); + assert_eq!(col.get_entry(4), None); + } + + assert!(col_iter.next().is_none()); + } + + // Mutable iterator + { + let mut csc = csc; + let mut col_iter = csc.col_iter_mut(); + + { + let mut col = col_iter.next().unwrap(); + assert_eq!(col.nrows(), 4); + assert_eq!(col.nnz(), 2); + assert_eq!(col.row_indices(), &[1, 2]); + assert_eq!(col.values(), &[1, 2]); + assert_eq!(col.get_entry(0), Some(SparseEntry::Zero)); + assert_eq!(col.get_entry(1), Some(SparseEntry::NonZero(&1))); + assert_eq!(col.get_entry(2), Some(SparseEntry::NonZero(&2))); + assert_eq!(col.get_entry(3), Some(SparseEntry::Zero)); + assert_eq!(col.get_entry(4), None); + + assert_eq!(col.values_mut(), &mut [1, 2]); + assert_eq!( + col.rows_and_values_mut(), + ([1, 2].as_ref(), [1, 2].as_mut()) + ); + assert_eq!(col.get_entry_mut(0), Some(SparseEntryMut::Zero)); + assert_eq!(col.get_entry_mut(1), Some(SparseEntryMut::NonZero(&mut 1))); + assert_eq!(col.get_entry_mut(2), Some(SparseEntryMut::NonZero(&mut 2))); + assert_eq!(col.get_entry_mut(3), Some(SparseEntryMut::Zero)); + assert_eq!(col.get_entry_mut(4), None); + } + + { + let mut col = col_iter.next().unwrap(); + assert_eq!(col.nrows(), 4); + assert_eq!(col.nnz(), 1); + assert_eq!(col.row_indices(), &[0]); + assert_eq!(col.values(), &[3]); + assert_eq!(col.get_entry(0), Some(SparseEntry::NonZero(&3))); + assert_eq!(col.get_entry(1), Some(SparseEntry::Zero)); + assert_eq!(col.get_entry(2), Some(SparseEntry::Zero)); + assert_eq!(col.get_entry(3), Some(SparseEntry::Zero)); + assert_eq!(col.get_entry(4), None); + + assert_eq!(col.values_mut(), &mut [3]); + assert_eq!(col.rows_and_values_mut(), ([0].as_ref(), [3].as_mut())); + assert_eq!(col.get_entry_mut(0), Some(SparseEntryMut::NonZero(&mut 3))); + assert_eq!(col.get_entry_mut(1), Some(SparseEntryMut::Zero)); + assert_eq!(col.get_entry_mut(2), Some(SparseEntryMut::Zero)); + assert_eq!(col.get_entry_mut(3), Some(SparseEntryMut::Zero)); + assert_eq!(col.get_entry_mut(4), None); + } + + { + let mut col = col_iter.next().unwrap(); + assert_eq!(col.nrows(), 4); + assert_eq!(col.nnz(), 2); + assert_eq!(col.row_indices(), &[1, 3]); + assert_eq!(col.values(), &[4, 5]); + assert_eq!(col.get_entry(0), Some(SparseEntry::Zero)); + assert_eq!(col.get_entry(1), Some(SparseEntry::NonZero(&4))); + assert_eq!(col.get_entry(2), Some(SparseEntry::Zero)); + assert_eq!(col.get_entry(3), Some(SparseEntry::NonZero(&5))); + assert_eq!(col.get_entry(4), None); + + assert_eq!(col.values_mut(), &mut [4, 5]); + assert_eq!( + col.rows_and_values_mut(), + ([1, 3].as_ref(), [4, 5].as_mut()) + ); + assert_eq!(col.get_entry_mut(0), Some(SparseEntryMut::Zero)); + assert_eq!(col.get_entry_mut(1), Some(SparseEntryMut::NonZero(&mut 4))); + assert_eq!(col.get_entry_mut(2), Some(SparseEntryMut::Zero)); + assert_eq!(col.get_entry_mut(3), Some(SparseEntryMut::NonZero(&mut 5))); + assert_eq!(col.get_entry_mut(4), None); + } + + assert!(col_iter.next().is_none()); + } } proptest! { diff --git a/nalgebra-sparse/tests/unit_tests/csr.rs b/nalgebra-sparse/tests/unit_tests/csr.rs index 2fe3c910..dee1ae1e 100644 --- a/nalgebra-sparse/tests/unit_tests/csr.rs +++ b/nalgebra-sparse/tests/unit_tests/csr.rs @@ -392,7 +392,136 @@ fn csr_matrix_get_index_entry() { #[test] fn csr_matrix_row_iter() { - // TODO + #[rustfmt::skip] + let dense = DMatrix::from_row_slice(3, 4, &[ + 0, 1, 2, 0, + 3, 0, 0, 0, + 0, 4, 0, 5 + ]); + let csr = CsrMatrix::from(&dense); + + // Immutable iterator + { + let mut row_iter = csr.row_iter(); + + { + let row = row_iter.next().unwrap(); + assert_eq!(row.ncols(), 4); + assert_eq!(row.nnz(), 2); + assert_eq!(row.col_indices(), &[1, 2]); + assert_eq!(row.values(), &[1, 2]); + assert_eq!(row.get_entry(0), Some(SparseEntry::Zero)); + assert_eq!(row.get_entry(1), Some(SparseEntry::NonZero(&1))); + assert_eq!(row.get_entry(2), Some(SparseEntry::NonZero(&2))); + assert_eq!(row.get_entry(3), Some(SparseEntry::Zero)); + assert_eq!(row.get_entry(4), None); + } + + { + let row = row_iter.next().unwrap(); + assert_eq!(row.ncols(), 4); + assert_eq!(row.nnz(), 1); + assert_eq!(row.col_indices(), &[0]); + assert_eq!(row.values(), &[3]); + assert_eq!(row.get_entry(0), Some(SparseEntry::NonZero(&3))); + assert_eq!(row.get_entry(1), Some(SparseEntry::Zero)); + assert_eq!(row.get_entry(2), Some(SparseEntry::Zero)); + assert_eq!(row.get_entry(3), Some(SparseEntry::Zero)); + assert_eq!(row.get_entry(4), None); + } + + { + let row = row_iter.next().unwrap(); + assert_eq!(row.ncols(), 4); + assert_eq!(row.nnz(), 2); + assert_eq!(row.col_indices(), &[1, 3]); + assert_eq!(row.values(), &[4, 5]); + assert_eq!(row.get_entry(0), Some(SparseEntry::Zero)); + assert_eq!(row.get_entry(1), Some(SparseEntry::NonZero(&4))); + assert_eq!(row.get_entry(2), Some(SparseEntry::Zero)); + assert_eq!(row.get_entry(3), Some(SparseEntry::NonZero(&5))); + assert_eq!(row.get_entry(4), None); + } + + assert!(row_iter.next().is_none()); + } + + // Mutable iterator + { + let mut csr = csr; + let mut row_iter = csr.row_iter_mut(); + + { + let mut row = row_iter.next().unwrap(); + assert_eq!(row.ncols(), 4); + assert_eq!(row.nnz(), 2); + assert_eq!(row.col_indices(), &[1, 2]); + assert_eq!(row.values(), &[1, 2]); + assert_eq!(row.get_entry(0), Some(SparseEntry::Zero)); + assert_eq!(row.get_entry(1), Some(SparseEntry::NonZero(&1))); + assert_eq!(row.get_entry(2), Some(SparseEntry::NonZero(&2))); + assert_eq!(row.get_entry(3), Some(SparseEntry::Zero)); + assert_eq!(row.get_entry(4), None); + + assert_eq!(row.values_mut(), &mut [1, 2]); + assert_eq!( + row.cols_and_values_mut(), + ([1, 2].as_ref(), [1, 2].as_mut()) + ); + assert_eq!(row.get_entry_mut(0), Some(SparseEntryMut::Zero)); + assert_eq!(row.get_entry_mut(1), Some(SparseEntryMut::NonZero(&mut 1))); + assert_eq!(row.get_entry_mut(2), Some(SparseEntryMut::NonZero(&mut 2))); + assert_eq!(row.get_entry_mut(3), Some(SparseEntryMut::Zero)); + assert_eq!(row.get_entry_mut(4), None); + } + + { + let mut row = row_iter.next().unwrap(); + assert_eq!(row.ncols(), 4); + assert_eq!(row.nnz(), 1); + assert_eq!(row.col_indices(), &[0]); + assert_eq!(row.values(), &[3]); + assert_eq!(row.get_entry(0), Some(SparseEntry::NonZero(&3))); + assert_eq!(row.get_entry(1), Some(SparseEntry::Zero)); + assert_eq!(row.get_entry(2), Some(SparseEntry::Zero)); + assert_eq!(row.get_entry(3), Some(SparseEntry::Zero)); + assert_eq!(row.get_entry(4), None); + + assert_eq!(row.values_mut(), &mut [3]); + assert_eq!(row.cols_and_values_mut(), ([0].as_ref(), [3].as_mut())); + assert_eq!(row.get_entry_mut(0), Some(SparseEntryMut::NonZero(&mut 3))); + assert_eq!(row.get_entry_mut(1), Some(SparseEntryMut::Zero)); + assert_eq!(row.get_entry_mut(2), Some(SparseEntryMut::Zero)); + assert_eq!(row.get_entry_mut(3), Some(SparseEntryMut::Zero)); + assert_eq!(row.get_entry_mut(4), None); + } + + { + let mut row = row_iter.next().unwrap(); + assert_eq!(row.ncols(), 4); + assert_eq!(row.nnz(), 2); + assert_eq!(row.col_indices(), &[1, 3]); + assert_eq!(row.values(), &[4, 5]); + assert_eq!(row.get_entry(0), Some(SparseEntry::Zero)); + assert_eq!(row.get_entry(1), Some(SparseEntry::NonZero(&4))); + assert_eq!(row.get_entry(2), Some(SparseEntry::Zero)); + assert_eq!(row.get_entry(3), Some(SparseEntry::NonZero(&5))); + assert_eq!(row.get_entry(4), None); + + assert_eq!(row.values_mut(), &mut [4, 5]); + assert_eq!( + row.cols_and_values_mut(), + ([1, 3].as_ref(), [4, 5].as_mut()) + ); + assert_eq!(row.get_entry_mut(0), Some(SparseEntryMut::Zero)); + assert_eq!(row.get_entry_mut(1), Some(SparseEntryMut::NonZero(&mut 4))); + assert_eq!(row.get_entry_mut(2), Some(SparseEntryMut::Zero)); + assert_eq!(row.get_entry_mut(3), Some(SparseEntryMut::NonZero(&mut 5))); + assert_eq!(row.get_entry_mut(4), None); + } + + assert!(row_iter.next().is_none()); + } } proptest! { From c667b1f9c80d5859acf7515394c9892680357596 Mon Sep 17 00:00:00 2001 From: iMplode nZ Date: Wed, 10 Feb 2021 20:12:09 -0800 Subject: [PATCH 112/183] Added bytemuck implementations for static storages. --- Cargo.toml | 2 +- src/base/matrix.rs | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7d5a9db9..3fd9ae81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,6 @@ compare = [ "matrixcompare-core" ] libm = [ "simba/libm" ] libm-force = [ "simba/libm_force" ] - [dependencies] typenum = "1.12" generic-array = "0.14" @@ -55,6 +54,7 @@ mint = { version = "0.5", optional = true } quickcheck = { version = "0.9", optional = true } pest = { version = "2", optional = true } pest_derive = { version = "2", optional = true } +bytemuck = { version = "1.5", optional = true } matrixcompare-core = { version = "0.1", optional = true } [dev-dependencies] diff --git a/src/base/matrix.rs b/src/base/matrix.rs index 8035d2f8..73001536 100644 --- a/src/base/matrix.rs +++ b/src/base/matrix.rs @@ -279,6 +279,25 @@ impl> matrixcompare_core::DenseAc } } +#[cfg(feature = "bytemuck")] +unsafe impl + bytemuck::Zeroable for Matrix> +where + R::Value: core::ops::Mul, + >::Output: generic_array::ArrayLength, +{ +} + +#[cfg(feature = "bytemuck")] +unsafe impl + bytemuck::Pod for Matrix> +where + R::Value: core::ops::Mul, + >::Output: generic_array::ArrayLength, + <::Value>>::Output as generic_array::ArrayLength>::ArrayType: Copy +{ +} + impl Matrix { /// Creates a new matrix with the given data without statically checking that the matrix /// dimension matches the storage dimension. From 1c0891bbbb06a09ab2b3000463965b95c92b53bc Mon Sep 17 00:00:00 2001 From: iMplode nZ Date: Fri, 12 Feb 2021 15:30:12 -0800 Subject: [PATCH 113/183] Added bytemuck for Unit and Quaternion. --- src/base/unit.rs | 6 ++++++ src/geometry/quaternion.rs | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/base/unit.rs b/src/base/unit.rs index 2483307a..70e3a927 100644 --- a/src/base/unit.rs +++ b/src/base/unit.rs @@ -30,6 +30,12 @@ pub struct Unit { pub(crate) value: T, } +#[cfg(feature = "bytemuck")] +unsafe impl bytemuck::Zeroable for Unit where T: bytemuck::Zeroable {} + +#[cfg(feature = "bytemuck")] +unsafe impl bytemuck::Pod for Unit where T: bytemuck::Pod {} + #[cfg(feature = "serde-serialize")] impl Serialize for Unit { fn serialize(&self, serializer: S) -> Result diff --git a/src/geometry/quaternion.rs b/src/geometry/quaternion.rs index a5db1c69..78b95332 100755 --- a/src/geometry/quaternion.rs +++ b/src/geometry/quaternion.rs @@ -40,6 +40,12 @@ impl Default for Quaternion { } } +#[cfg(feature = "bytemuck")] +unsafe impl bytemuck::Zeroable for Quaternion where Vector4: bytemuck::Zeroable {} + +#[cfg(feature = "bytemuck")] +unsafe impl bytemuck::Pod for Quaternion where Vector4: bytemuck::Pod, N: Copy {} + #[cfg(feature = "abomonation-serialize")] impl Abomonation for Quaternion where From 6139372c384b14f2f43e01c6c9a3f02a7dc422b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Mon, 22 Feb 2021 14:26:25 +0100 Subject: [PATCH 114/183] Add from_basis_unchecked to rotation types. --- src/geometry/quaternion_construction.rs | 11 +++++++++++ src/geometry/rotation_specialization.rs | 24 ++++++++++++++++++++++- src/geometry/unit_complex_construction.rs | 14 ++++++++++++- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/geometry/quaternion_construction.rs b/src/geometry/quaternion_construction.rs index 03d87d39..a26deb1a 100644 --- a/src/geometry/quaternion_construction.rs +++ b/src/geometry/quaternion_construction.rs @@ -266,6 +266,17 @@ where Self::new_unchecked(q) } + /// Builds an unit quaternion from a basis assumed to be orthonormal. + /// + /// In order to get a valid unit-quaternion, the input must be an + /// orthonormal basis, i.e., all vectors are normalized, and the are + /// all orthogonal to each other. These invariants are not checked + /// by this method. + pub fn from_basis_unchecked(basis: &[Vector3; 3]) -> Self { + let rot = Rotation3::from_basis_unchecked(basis); + Self::from_rotation_matrix(&rot) + } + /// Builds an unit quaternion from a rotation matrix. /// /// # Example diff --git a/src/geometry/rotation_specialization.rs b/src/geometry/rotation_specialization.rs index afc180d8..fcaa8667 100644 --- a/src/geometry/rotation_specialization.rs +++ b/src/geometry/rotation_specialization.rs @@ -12,7 +12,7 @@ use std::ops::Neg; use crate::base::dimension::{U1, U2, U3}; use crate::base::storage::Storage; -use crate::base::{Matrix2, Matrix3, MatrixN, Unit, Vector, Vector1, Vector3, VectorN}; +use crate::base::{Matrix2, Matrix3, MatrixN, Unit, Vector, Vector1, Vector2, Vector3, VectorN}; use crate::geometry::{Rotation2, Rotation3, UnitComplex, UnitQuaternion}; @@ -53,6 +53,17 @@ impl Rotation2 { /// # Construction from an existing 2D matrix or rotations impl Rotation2 { + /// Builds a rotation from a basis assumed to be orthonormal. + /// + /// In order to get a valid unit-quaternion, the input must be an + /// orthonormal basis, i.e., all vectors are normalized, and the are + /// all orthogonal to each other. These invariants are not checked + /// by this method. + pub fn from_basis_unchecked(basis: &[Vector2; 2]) -> Self { + let mat = Matrix2::from_columns(&basis[..]); + Self::from_matrix_unchecked(mat) + } + /// Builds a rotation matrix by extracting the rotation part of the given transformation `m`. /// /// This is an iterative method. See `.from_matrix_eps` to provide mover @@ -655,6 +666,17 @@ where } } + /// Builds a rotation from a basis assumed to be orthonormal. + /// + /// In order to get a valid unit-quaternion, the input must be an + /// orthonormal basis, i.e., all vectors are normalized, and the are + /// all orthogonal to each other. These invariants are not checked + /// by this method. + pub fn from_basis_unchecked(basis: &[Vector3; 3]) -> Self { + let mat = Matrix3::from_columns(&basis[..]); + Self::from_matrix_unchecked(mat) + } + /// Builds a rotation matrix by extracting the rotation part of the given transformation `m`. /// /// This is an iterative method. See `.from_matrix_eps` to provide mover diff --git a/src/geometry/unit_complex_construction.rs b/src/geometry/unit_complex_construction.rs index 65d36888..acdbac8a 100644 --- a/src/geometry/unit_complex_construction.rs +++ b/src/geometry/unit_complex_construction.rs @@ -8,7 +8,7 @@ use rand::Rng; use crate::base::dimension::{U1, U2}; use crate::base::storage::Storage; -use crate::base::{Matrix2, Unit, Vector}; +use crate::base::{Matrix2, Unit, Vector, Vector2}; use crate::geometry::{Rotation2, UnitComplex}; use simba::scalar::RealField; use simba::simd::SimdRealField; @@ -164,6 +164,18 @@ where Self::new_unchecked(Complex::new(rotmat[(0, 0)], rotmat[(1, 0)])) } + /// Builds a rotation from a basis assumed to be orthonormal. + /// + /// In order to get a valid unit-quaternion, the input must be an + /// orthonormal basis, i.e., all vectors are normalized, and the are + /// all orthogonal to each other. These invariants are not checked + /// by this method. + pub fn from_basis_unchecked(basis: &[Vector2; 2]) -> Self { + let mat = Matrix2::from_columns(&basis[..]); + let rot = Rotation2::from_matrix_unchecked(mat); + Self::from_rotation_matrix(&rot) + } + /// Builds an unit complex by extracting the rotation part of the given transformation `m`. /// /// This is an iterative method. See `.from_matrix_eps` to provide mover From 9d930eb21a22ee6628d21b5035458c9909a09a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Mon, 22 Feb 2021 14:26:40 +0100 Subject: [PATCH 115/183] Add a method to cap the magnitude of a vector. --- src/base/norm.rs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/base/norm.rs b/src/base/norm.rs index a7fa66e9..9717e031 100644 --- a/src/base/norm.rs +++ b/src/base/norm.rs @@ -8,7 +8,7 @@ use crate::allocator::Allocator; use crate::base::{DefaultAllocator, Dim, DimName, Matrix, MatrixMN, Normed, VectorN}; use crate::constraint::{SameNumberOfColumns, SameNumberOfRows, ShapeConstraint}; use crate::storage::{Storage, StorageMut}; -use crate::{ComplexField, Scalar, SimdComplexField, Unit}; +use crate::{ComplexField, RealField, Scalar, SimdComplexField, Unit}; use simba::scalar::ClosedNeg; use simba::simd::{SimdOption, SimdPartialOrd}; @@ -334,11 +334,27 @@ impl> Matrix { { let n = self.norm(); - if n >= min_magnitude { + if n > min_magnitude { self.scale_mut(magnitude / n) } } + /// Returns a new vector with the same magnitude as `self` clamped between `0.0` and `max`. + #[inline] + pub fn cap_magnitude(&self, max: N::RealField) -> MatrixMN + where + N: RealField, + DefaultAllocator: Allocator, + { + let n = self.norm(); + + if n > max { + self.scale(max / n) + } else { + self.clone_owned() + } + } + /// Returns a normalized version of this matrix unless its norm as smaller or equal to `eps`. /// /// The components of this matrix cannot be SIMD types (see `simd_try_normalize`) instead. From 478921881fe8d759a136b7b6c6d60d870c705ac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Mon, 22 Feb 2021 14:27:08 +0100 Subject: [PATCH 116/183] Add approximate rotation composition for unit-quaternion. --- src/geometry/quaternion.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/geometry/quaternion.rs b/src/geometry/quaternion.rs index a5db1c69..a56b69c3 100755 --- a/src/geometry/quaternion.rs +++ b/src/geometry/quaternion.rs @@ -1542,6 +1542,17 @@ where pub fn inverse_transform_unit_vector(&self, v: &Unit>) -> Unit> { self.inverse() * v } + + /// Appends to `self` a rotation given in the axis-angle form, using a linearized formulation. + /// + /// This is faster, but approximate, way to compute `UnitQuaternion::new(axisangle) * self`. + #[inline] + pub fn append_axisangle_linearized(&self, axisangle: &Vector3) -> Self { + let half: N = crate::convert(0.5); + let q1 = self.into_inner(); + let q2 = Quaternion::from_imag(axisangle * half); + Unit::new_normalize(q1 + q2 * q1) + } } impl Default for UnitQuaternion { From bafa1dcd979cb5a3063ed7404057d290186bd536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Mon, 22 Feb 2021 14:27:18 +0100 Subject: [PATCH 117/183] Re-export simba::SimdValue. --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 7b2f08a6..a55a8c3b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -152,7 +152,7 @@ pub use num_complex::Complex; pub use simba::scalar::{ ClosedAdd, ClosedDiv, ClosedMul, ClosedSub, ComplexField, Field, RealField, }; -pub use simba::simd::{SimdBool, SimdComplexField, SimdPartialOrd, SimdRealField}; +pub use simba::simd::{SimdBool, SimdComplexField, SimdPartialOrd, SimdRealField, SimdValue}; /// Gets the multiplicative identity element. /// From 424897f55bb1907315ceff90d7bcc33f4e7542c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Mon, 22 Feb 2021 14:27:31 +0100 Subject: [PATCH 118/183] Fix no-std cargo category. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7d5a9db9..ac4c1ac3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ documentation = "https://www.nalgebra.org/docs" homepage = "https://nalgebra.org" repository = "https://github.com/dimforge/nalgebra" readme = "README.md" -categories = [ "science", "mathematics", "wasm", "no standard library" ] +categories = [ "science", "mathematics", "wasm", "no-std" ] keywords = [ "linear", "algebra", "matrix", "vector", "math" ] license = "BSD-3-Clause" edition = "2018" From 8d00378d6c0c71dd1eaceb8c0dcf34ab134efcc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Mon, 22 Feb 2021 14:32:04 +0100 Subject: [PATCH 119/183] Update the changelog. --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9825cd2..03c4e776 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ documented here. This project adheres to [Semantic Versioning](https://semver.org/). +## [0.25.0] - WIP +### Added +* Add `from_basis_unchecked` to all the rotation types. This builds a rotation from a set of basis vectors (representing the columns of the corresponding rotation matrix). +* Add `Matrix::cap_magnitude` to cap the magnitude of a vector. +* Add `UnitQuaternion::append_axisangle_linearized` to approximately append a rotation represented as an axis-angle to a rotation represented as an unit quaternion. +* Re-export `simba::simd::SimdValue` at the root of the `nalgebra` crate. + ## [0.24.0] ### Added From 4e4eeb264108279c1bd387f020773ffacfc10a47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Thu, 25 Feb 2021 11:00:48 +0100 Subject: [PATCH 120/183] Don't pin the proptest version when running tests: this breaks all no-std builds. This is extremely unfortunate, but we cannot pin the version of proptest because of the Cargo bug #4866 will cause a breakage of #[no-std] builds. --- Cargo.toml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9d689dac..c281e5e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,11 +72,6 @@ rand_isaac = "0.2" # For matrix comparison macro matrixcompare = "0.2.0" - -# Make sure that we use a specific version of proptest for tests. The reason is that we use a deterministic -# RNG for certain tests. However, different versions of proptest may give different sequences of numbers, -# which may cause more brittle tests (although ideally they should take enough samples for it not to matter). -proptest = { version = "=0.10.1" } itertools = "0.9" [workspace] From 98ae4f3818b43d7b56920c4605df96959eb98017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Thu, 25 Feb 2021 11:03:27 +0100 Subject: [PATCH 121/183] nalgebra-sparse: reexport CooMatrix, CscMatrix, and CsrMatrix at the root of the crate. --- nalgebra-sparse/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index a2783545..ebf2f842 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -155,6 +155,10 @@ use num_traits::Zero; use std::error::Error; use std::fmt; +pub use self::coo::CooMatrix; +pub use self::csc::CscMatrix; +pub use self::csr::CsrMatrix; + /// Errors produced by functions that expect well-formed sparse format data. #[derive(Debug)] pub struct SparseFormatError { From c6f7cae32667e5ff502e9aca756cc46434b64178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Thu, 25 Feb 2021 11:11:29 +0100 Subject: [PATCH 122/183] Move COO, CSC, CSR constructor at the top of the impls. --- nalgebra-sparse/src/coo.rs | 9 +++ nalgebra-sparse/src/csc.rs | 121 ++++++++++++++++++------------------- nalgebra-sparse/src/csr.rs | 121 ++++++++++++++++++------------------- 3 files changed, 129 insertions(+), 122 deletions(-) diff --git a/nalgebra-sparse/src/coo.rs b/nalgebra-sparse/src/coo.rs index 6b641513..50c68677 100644 --- a/nalgebra-sparse/src/coo.rs +++ b/nalgebra-sparse/src/coo.rs @@ -61,6 +61,15 @@ impl CooMatrix { } } + /// Construct a zero COO matrix of the given dimensions. + /// + /// Specifically, the collection of triplets - corresponding to explicitly stored entries - + /// is empty, so that the matrix (implicitly) represented by the COO matrix consists of all + /// zero entries. + pub fn zeros(nrows: usize, ncols: usize) -> Self { + Self::new(nrows, ncols) + } + /// Try to construct a COO matrix from the given dimensions and a collection of /// (i, j, v) triplets. /// diff --git a/nalgebra-sparse/src/csc.rs b/nalgebra-sparse/src/csc.rs index fb454a43..3a4d3f6f 100644 --- a/nalgebra-sparse/src/csc.rs +++ b/nalgebra-sparse/src/csc.rs @@ -127,6 +127,17 @@ pub struct CscMatrix { } impl CscMatrix { + /// Constructs a CSC representation of the (square) `n x n` identity matrix. + #[inline] + pub fn identity(n: usize) -> Self + where + T: Scalar + One, + { + Self { + cs: CsMatrix::identity(n), + } + } + /// Create a zero CSC matrix with no explicitly stored entries. pub fn zeros(nrows: usize, ncols: usize) -> Self { Self { @@ -134,6 +145,51 @@ impl CscMatrix { } } + /// Try to construct a CSC matrix from raw CSC data. + /// + /// It is assumed that each column contains unique and sorted row indices that are in + /// bounds with respect to the number of rows in the matrix. If this is not the case, + /// an error is returned to indicate the failure. + /// + /// An error is returned if the data given does not conform to the CSC storage format. + /// See the documentation for [CscMatrix](struct.CscMatrix.html) for more information. + pub fn try_from_csc_data( + num_rows: usize, + num_cols: usize, + col_offsets: Vec, + row_indices: Vec, + values: Vec, + ) -> Result { + let pattern = SparsityPattern::try_from_offsets_and_indices( + num_cols, + num_rows, + col_offsets, + row_indices, + ) + .map_err(pattern_format_error_to_csc_error)?; + Self::try_from_pattern_and_values(pattern, values) + } + + /// Try to construct a CSC matrix from a sparsity pattern and associated non-zero values. + /// + /// Returns an error if the number of values does not match the number of minor indices + /// in the pattern. + pub fn try_from_pattern_and_values( + pattern: SparsityPattern, + values: Vec, + ) -> Result { + if pattern.nnz() == values.len() { + Ok(Self { + cs: CsMatrix::from_pattern_and_values(pattern, values), + }) + } else { + Err(SparseFormatError::from_kind_and_msg( + SparseFormatErrorKind::InvalidStructure, + "Number of values and row indices must be the same", + )) + } + } + /// The number of rows in the matrix. #[inline] pub fn nrows(&self) -> usize { @@ -180,51 +236,6 @@ impl CscMatrix { self.cs.values_mut() } - /// Try to construct a CSC matrix from raw CSC data. - /// - /// It is assumed that each column contains unique and sorted row indices that are in - /// bounds with respect to the number of rows in the matrix. If this is not the case, - /// an error is returned to indicate the failure. - /// - /// An error is returned if the data given does not conform to the CSC storage format. - /// See the documentation for [CscMatrix](struct.CscMatrix.html) for more information. - pub fn try_from_csc_data( - num_rows: usize, - num_cols: usize, - col_offsets: Vec, - row_indices: Vec, - values: Vec, - ) -> Result { - let pattern = SparsityPattern::try_from_offsets_and_indices( - num_cols, - num_rows, - col_offsets, - row_indices, - ) - .map_err(pattern_format_error_to_csc_error)?; - Self::try_from_pattern_and_values(pattern, values) - } - - /// Try to construct a CSC matrix from a sparsity pattern and associated non-zero values. - /// - /// Returns an error if the number of values does not match the number of minor indices - /// in the pattern. - pub fn try_from_pattern_and_values( - pattern: SparsityPattern, - values: Vec, - ) -> Result { - if pattern.nnz() == values.len() { - Ok(Self { - cs: CsMatrix::from_pattern_and_values(pattern, values), - }) - } else { - Err(SparseFormatError::from_kind_and_msg( - SparseFormatErrorKind::InvalidStructure, - "Number of values and row indices must be the same", - )) - } - } - /// An iterator over non-zero triplets (i, j, v). /// /// The iteration happens in column-major fashion, meaning that j increases monotonically, @@ -485,28 +496,16 @@ impl CscMatrix { cs: self.cs.diagonal_as_matrix(), } } -} -impl CscMatrix -where - T: Scalar, -{ /// Compute the transpose of the matrix. - pub fn transpose(&self) -> CscMatrix { + pub fn transpose(&self) -> CscMatrix + where + T: Scalar, + { CsrMatrix::from(self).transpose_as_csc() } } -impl CscMatrix { - /// Constructs a CSC representation of the (square) `n x n` identity matrix. - #[inline] - pub fn identity(n: usize) -> Self { - Self { - cs: CsMatrix::identity(n), - } - } -} - /// Convert pattern format errors into more meaningful CSC-specific errors. /// /// This ensures that the terminology is consistent: we are talking about rows and columns, diff --git a/nalgebra-sparse/src/csr.rs b/nalgebra-sparse/src/csr.rs index 2e95460f..ded189eb 100644 --- a/nalgebra-sparse/src/csr.rs +++ b/nalgebra-sparse/src/csr.rs @@ -127,6 +127,17 @@ pub struct CsrMatrix { } impl CsrMatrix { + /// Constructs a CSR representation of the (square) `n x n` identity matrix. + #[inline] + pub fn identity(n: usize) -> Self + where + T: Scalar + One, + { + Self { + cs: CsMatrix::identity(n), + } + } + /// Create a zero CSR matrix with no explicitly stored entries. pub fn zeros(nrows: usize, ncols: usize) -> Self { Self { @@ -134,6 +145,51 @@ impl CsrMatrix { } } + /// Try to construct a CSR matrix from raw CSR data. + /// + /// It is assumed that each row contains unique and sorted column indices that are in + /// bounds with respect to the number of columns in the matrix. If this is not the case, + /// an error is returned to indicate the failure. + /// + /// An error is returned if the data given does not conform to the CSR storage format. + /// See the documentation for [CsrMatrix](struct.CsrMatrix.html) for more information. + pub fn try_from_csr_data( + num_rows: usize, + num_cols: usize, + row_offsets: Vec, + col_indices: Vec, + values: Vec, + ) -> Result { + let pattern = SparsityPattern::try_from_offsets_and_indices( + num_rows, + num_cols, + row_offsets, + col_indices, + ) + .map_err(pattern_format_error_to_csr_error)?; + Self::try_from_pattern_and_values(pattern, values) + } + + /// Try to construct a CSR matrix from a sparsity pattern and associated non-zero values. + /// + /// Returns an error if the number of values does not match the number of minor indices + /// in the pattern. + pub fn try_from_pattern_and_values( + pattern: SparsityPattern, + values: Vec, + ) -> Result { + if pattern.nnz() == values.len() { + Ok(Self { + cs: CsMatrix::from_pattern_and_values(pattern, values), + }) + } else { + Err(SparseFormatError::from_kind_and_msg( + SparseFormatErrorKind::InvalidStructure, + "Number of values and column indices must be the same", + )) + } + } + /// The number of rows in the matrix. #[inline] pub fn nrows(&self) -> usize { @@ -182,51 +238,6 @@ impl CsrMatrix { self.cs.values_mut() } - /// Try to construct a CSR matrix from raw CSR data. - /// - /// It is assumed that each row contains unique and sorted column indices that are in - /// bounds with respect to the number of columns in the matrix. If this is not the case, - /// an error is returned to indicate the failure. - /// - /// An error is returned if the data given does not conform to the CSR storage format. - /// See the documentation for [CsrMatrix](struct.CsrMatrix.html) for more information. - pub fn try_from_csr_data( - num_rows: usize, - num_cols: usize, - row_offsets: Vec, - col_indices: Vec, - values: Vec, - ) -> Result { - let pattern = SparsityPattern::try_from_offsets_and_indices( - num_rows, - num_cols, - row_offsets, - col_indices, - ) - .map_err(pattern_format_error_to_csr_error)?; - Self::try_from_pattern_and_values(pattern, values) - } - - /// Try to construct a CSR matrix from a sparsity pattern and associated non-zero values. - /// - /// Returns an error if the number of values does not match the number of minor indices - /// in the pattern. - pub fn try_from_pattern_and_values( - pattern: SparsityPattern, - values: Vec, - ) -> Result { - if pattern.nnz() == values.len() { - Ok(Self { - cs: CsMatrix::from_pattern_and_values(pattern, values), - }) - } else { - Err(SparseFormatError::from_kind_and_msg( - SparseFormatErrorKind::InvalidStructure, - "Number of values and column indices must be the same", - )) - } - } - /// An iterator over non-zero triplets (i, j, v). /// /// The iteration happens in row-major fashion, meaning that i increases monotonically, @@ -485,28 +496,16 @@ impl CsrMatrix { cs: self.cs.diagonal_as_matrix(), } } -} -impl CsrMatrix -where - T: Scalar, -{ /// Compute the transpose of the matrix. - pub fn transpose(&self) -> CsrMatrix { + pub fn transpose(&self) -> CsrMatrix + where + T: Scalar, + { CscMatrix::from(self).transpose_as_csr() } } -impl CsrMatrix { - /// Constructs a CSR representation of the (square) `n x n` identity matrix. - #[inline] - pub fn identity(n: usize) -> Self { - Self { - cs: CsMatrix::identity(n), - } - } -} - /// Convert pattern format errors into more meaningful CSR-specific errors. /// /// This ensures that the terminology is consistent: we are talking about rows and columns, From 660106255ced785d29e7f4a044b5582a35a9042a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Thu, 25 Feb 2021 11:14:25 +0100 Subject: [PATCH 123/183] nalgebra-sparse: re-export nalgebra. --- nalgebra-sparse/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/nalgebra-sparse/src/lib.rs b/nalgebra-sparse/src/lib.rs index ebf2f842..4b96717c 100644 --- a/nalgebra-sparse/src/lib.rs +++ b/nalgebra-sparse/src/lib.rs @@ -135,6 +135,7 @@ #![deny(unused_results)] #![deny(missing_docs)] +pub extern crate nalgebra as na; pub mod convert; pub mod coo; pub mod csc; From f8c0195f0f3ec543aa55ac344b71fd96ea3ee1b3 Mon Sep 17 00:00:00 2001 From: russellb23 Date: Mon, 24 Jun 2019 07:56:35 +0530 Subject: [PATCH 124/183] QR factorization with column pivoting --- src/linalg/qrp.rs | 328 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 328 insertions(+) create mode 100644 src/linalg/qrp.rs diff --git a/src/linalg/qrp.rs b/src/linalg/qrp.rs new file mode 100644 index 00000000..d251cc5d --- /dev/null +++ b/src/linalg/qrp.rs @@ -0,0 +1,328 @@ +#[cfg(feature = "serde-serialize")] +use serde::{Deserialize, Serialize}; +use num::Zero; + +use alga::general::ComplexField; +use crate::allocator::{Allocator, Reallocator}; +use crate::base::{DefaultAllocator, Matrix, MatrixMN, MatrixN, Unit, VectorN}; +use crate::constraint::{SameNumberOfRows, ShapeConstraint}; +use crate::dimension::{Dim, DimMin, DimMinimum, U1}; +use crate::storage::{Storage, StorageMut}; + +use crate::geometry::Reflection; +use crate::linalg::householder; + +//============================================================================= +use alga::general::RealField; +use crate::linalg::PermutationSequence; +use crate::base::VecStorage; +use crate::base::Dynamic; +use crate::base::DVector; +//============================================================================= + +/// The QRP decomposition of a general matrix. +#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-serialize", + serde(bound( + serialize = "DefaultAllocator: Allocator + + Allocator>, + MatrixMN: Serialize, + PermutationSequence>: Serialize, + VectorN>: Serialize" + )) +)] +#[cfg_attr( + feature = "serde-serialize", + serde(bound( + deserialize = "DefaultAllocator: Allocator + + Allocator>, + MatrixMN: Deserialize<'de>, + PermutationSequence>: Deserialize<'de>, + VectorN>: Deserialize<'de>" + )) +)] +#[derive(Clone, Debug)] +pub struct QRP, C: Dim> +where DefaultAllocator: Allocator + Allocator> + + Allocator<(usize, usize), DimMinimum>, +{ + qrp: MatrixMN, + p: PermutationSequence>, + diag: VectorN>, +} + +impl, C: Dim> Copy for QRP +where + DefaultAllocator: Allocator + Allocator> + + Allocator<(usize, usize), DimMinimum>, + MatrixMN: Copy, + PermutationSequence>: Copy, + VectorN>: Copy, +{} + +impl, C: Dim> QRP +where DefaultAllocator: Allocator + Allocator + Allocator> + Allocator<(usize, usize), DimMinimum> +{ + /// Computes the QRP decomposition using householder reflections. + pub fn new(mut matrix: MatrixMN) -> Self { + let (nrows, ncols) = matrix.data.shape(); + let min_nrows_ncols = nrows.min(ncols); + let mut p = PermutationSequence::identity_generic(min_nrows_ncols); + let mut diag = unsafe { MatrixMN::new_uninitialized_generic(min_nrows_ncols, U1) }; + + if min_nrows_ncols.value() == 0 { + return QRP { + qrp: matrix, + p: p, + diag: diag, + }; + } + + for ite in 0..min_nrows_ncols.value() { + let piv = matrix.slice_range(ite.., ite..).icamax_full(); + let col_piv = piv.1 + ite; + matrix.swap_columns(ite, col_piv); + p.append_permutation(ite, col_piv); + + householder::clear_column_unchecked(&mut matrix, &mut diag[ite], ite, 0, None); + } + + QRP { + qrp: matrix, + p: p, + diag: diag, + } + } + + /// Retrieves the upper trapezoidal submatrix `R` of this decomposition. + #[inline] + pub fn r(&self) -> MatrixMN, C> + where + DefaultAllocator: Allocator, C>, + { + let (nrows, ncols) = self.qrp.data.shape(); + let mut res = self.qrp.rows_generic(0, nrows.min(ncols)).upper_triangle(); + res.set_partial_diagonal(self.diag.iter().map(|e| N::from_real(e.modulus()))); + res + } + + /// Retrieves the upper trapezoidal submatrix `R` of this decomposition. + /// + /// This is usually faster than `r` but consumes `self`. + #[inline] + pub fn unpack_r(self) -> MatrixMN, C> + where + DefaultAllocator: Reallocator, C>, + { + let (nrows, ncols) = self.qrp.data.shape(); + let mut res = self.qrp.resize_generic(nrows.min(ncols), ncols, N::zero()); + res.fill_lower_triangle(N::zero(), 1); + res.set_partial_diagonal(self.diag.iter().map(|e| N::from_real(e.modulus()))); + res + } + + /// Computes the orthogonal matrix `Q` of this decomposition. + pub fn q(&self) -> MatrixMN> + where DefaultAllocator: Allocator> { + let (nrows, ncols) = self.qrp.data.shape(); + + // NOTE: we could build the identity matrix and call q_mul on it. + // Instead we don't so that we take in account the matrix sparseness. + let mut res = Matrix::identity_generic(nrows, nrows.min(ncols)); + let dim = self.diag.len(); + + for i in (0..dim).rev() { + let axis = self.qrp.slice_range(i.., i); + // FIXME: sometimes, the axis might have a zero magnitude. + let refl = Reflection::new(Unit::new_unchecked(axis), N::zero()); + + let mut res_rows = res.slice_range_mut(i.., i..); + refl.reflect_with_sign(&mut res_rows, self.diag[i].signum()); + } + + res + } + /// The column permutations of this decomposition. + #[inline] + pub fn p(&self) -> &PermutationSequence> { + &self.p + } + + /// Unpacks this decomposition into its two matrix factors. + pub fn unpack( + self, + ) -> ( + MatrixMN>, + MatrixMN, C>, + ) + where + DimMinimum: DimMin>, + DefaultAllocator: + Allocator> + Reallocator, C>, + { + (self.q(), self.unpack_r()) + } + + #[doc(hidden)] + pub fn qrp_internal(&self) -> &MatrixMN { + &self.qrp + } + + /// Multiplies the provided matrix by the transpose of the `Q` matrix of this decomposition. + pub fn q_tr_mul(&self, rhs: &mut Matrix) + // FIXME: do we need a static constraint on the number of rows of rhs? + where S2: StorageMut { + let dim = self.diag.len(); + + for i in 0..dim { + let axis = self.qrp.slice_range(i.., i); + let refl = Reflection::new(Unit::new_unchecked(axis), N::zero()); + + let mut rhs_rows = rhs.rows_range_mut(i..); + refl.reflect_with_sign(&mut rhs_rows, self.diag[i].signum().conjugate()); + } + } +} + +impl> QRP +where DefaultAllocator: Allocator + Allocator + + Allocator<(usize, usize), DimMinimum> +{ + /// Solves the linear system `self * x = b`, where `x` is the unknown to be determined. + /// + /// Returns `None` if `self` is not invertible. + pub fn solve( + &self, + b: &Matrix, + ) -> Option> + where + S2: StorageMut, + ShapeConstraint: SameNumberOfRows, + DefaultAllocator: Allocator, + { + let mut res = b.clone_owned(); + + if self.solve_mut(&mut res) { + Some(res) + } else { + None + } + } + + /// Solves the linear system `self * x = b`, where `x` is the unknown to be determined. + /// + /// If the decomposed matrix is not invertible, this returns `false` and its input `b` is + /// overwritten with garbage. + pub fn solve_mut(&self, b: &mut Matrix) -> bool + where + S2: StorageMut, + ShapeConstraint: SameNumberOfRows, + { + assert_eq!( + self.qrp.nrows(), + b.nrows(), + "QRP solve matrix dimension mismatch." + ); + assert!( + self.qrp.is_square(), + "QRP solve: unable to solve a non-square system." + ); + + self.q_tr_mul(b); + self.solve_upper_triangular_mut(b) + } + + // FIXME: duplicate code from the `solve` module. + fn solve_upper_triangular_mut( + &self, + b: &mut Matrix, + ) -> bool + where + S2: StorageMut, + ShapeConstraint: SameNumberOfRows, + { + let dim = self.qrp.nrows(); + + for k in 0..b.ncols() { + let mut b = b.column_mut(k); + for i in (0..dim).rev() { + let coeff; + + unsafe { + let diag = self.diag.vget_unchecked(i).modulus(); + + if diag.is_zero() { + return false; + } + + coeff = b.vget_unchecked(i).unscale(diag); + *b.vget_unchecked_mut(i) = coeff; + } + + b.rows_range_mut(..i) + .axpy(-coeff, &self.qrp.slice_range(..i, i), N::one()); + } + } + + true + } + + /// Computes the inverse of the decomposed matrix. + /// + /// Returns `None` if the decomposed matrix is not invertible. + pub fn try_inverse(&self) -> Option> { + assert!( + self.qrp.is_square(), + "QRP inverse: unable to compute the inverse of a non-square matrix." + ); + + // FIXME: is there a less naive method ? + let (nrows, ncols) = self.qrp.data.shape(); + let mut res = MatrixN::identity_generic(nrows, ncols); + + if self.solve_mut(&mut res) { + Some(res) + } else { + None + } + } + + /// Indicates if the decomposed matrix is invertible. + pub fn is_invertible(&self) -> bool { + assert!( + self.qrp.is_square(), + "QRP: unable to test the invertibility of a non-square matrix." + ); + + for i in 0..self.diag.len() { + if self.diag[i].is_zero() { + return false; + } + } + + true + } + + // /// Computes the determinant of the decomposed matrix. + // pub fn determinant(&self) -> N { + // let dim = self.qrp.nrows(); + // assert!(self.qrp.is_square(), "QRP determinant: unable to compute the determinant of a non-square matrix."); + + // let mut res = N::one(); + // for i in 0 .. dim { + // res *= unsafe { *self.diag.vget_unchecked(i) }; + // } + + // res self.q_determinant() + // } +} + +impl, C: Dim, S: Storage> Matrix +where DefaultAllocator: Allocator + Allocator + Allocator> + Allocator<(usize, usize), DimMinimum> +{ + /// Computes the QRP decomposition of this matrix. + pub fn qrp(self) -> QRP { + QRP::new(self.into_owned()) + } +} From a2f3e1ac26ceff96d5ac0a0e2e2204c92374ca59 Mon Sep 17 00:00:00 2001 From: russellb23 Date: Mon, 24 Jun 2019 08:49:57 +0530 Subject: [PATCH 125/183] Inverted sign in householder --- src/linalg/householder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linalg/householder.rs b/src/linalg/householder.rs index ebbffd30..3692fe31 100644 --- a/src/linalg/householder.rs +++ b/src/linalg/householder.rs @@ -34,7 +34,7 @@ pub fn reflection_axis_mut>( if !factor.is_zero() { column.unscale_mut(factor.sqrt()); - (-signed_norm, true) + (signed_norm, true) } else { // TODO: not sure why we don't have a - sign here. (signed_norm, false) From 13161336255ac3aa59aec80c176831604cb96204 Mon Sep 17 00:00:00 2001 From: russellb23 Date: Mon, 24 Jun 2019 09:20:44 +0530 Subject: [PATCH 126/183] Removed unused imports --- src/linalg/qrp.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/linalg/qrp.rs b/src/linalg/qrp.rs index d251cc5d..c71ac578 100644 --- a/src/linalg/qrp.rs +++ b/src/linalg/qrp.rs @@ -13,11 +13,7 @@ use crate::geometry::Reflection; use crate::linalg::householder; //============================================================================= -use alga::general::RealField; use crate::linalg::PermutationSequence; -use crate::base::VecStorage; -use crate::base::Dynamic; -use crate::base::DVector; //============================================================================= /// The QRP decomposition of a general matrix. From 63a34528e020edf037ab86b2cdf95642a01dc43d Mon Sep 17 00:00:00 2001 From: russellb23 Date: Mon, 24 Jun 2019 12:20:14 +0530 Subject: [PATCH 127/183] Added test for QR factorization and fixed unpack issue --- src/linalg/qrp.rs | 38 ++++++----- tests/linalg/qrp.rs | 156 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+), 16 deletions(-) create mode 100644 tests/linalg/qrp.rs diff --git a/src/linalg/qrp.rs b/src/linalg/qrp.rs index c71ac578..c7d2f27b 100644 --- a/src/linalg/qrp.rs +++ b/src/linalg/qrp.rs @@ -11,10 +11,7 @@ use crate::storage::{Storage, StorageMut}; use crate::geometry::Reflection; use crate::linalg::householder; - -//============================================================================= use crate::linalg::PermutationSequence; -//============================================================================= /// The QRP decomposition of a general matrix. #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] @@ -65,7 +62,9 @@ where DefaultAllocator: Allocator + Allocator + Allocator + Allocator + Allocator + Allocator + Allocator &PermutationSequence> { &self.p @@ -151,13 +156,14 @@ where DefaultAllocator: Allocator + Allocator + Allocator ( MatrixMN>, MatrixMN, C>, + PermutationSequence>, ) where DimMinimum: DimMin>, DefaultAllocator: - Allocator> + Reallocator, C>, + Allocator> + Reallocator, C> + Allocator<(usize, usize), DimMinimum>, { - (self.q(), self.unpack_r()) + (self.q(), self.r(), self.p) } #[doc(hidden)] @@ -300,18 +306,18 @@ where DefaultAllocator: Allocator + Allocator + true } - // /// Computes the determinant of the decomposed matrix. - // pub fn determinant(&self) -> N { - // let dim = self.qrp.nrows(); - // assert!(self.qrp.is_square(), "QRP determinant: unable to compute the determinant of a non-square matrix."); + /// Computes the determinant of the decomposed matrix. + pub fn determinant(&self) -> N { + let dim = self.qrp.nrows(); + assert!(self.qrp.is_square(), "QRP determinant: unable to compute the determinant of a non-square matrix."); - // let mut res = N::one(); - // for i in 0 .. dim { - // res *= unsafe { *self.diag.vget_unchecked(i) }; - // } + let mut res = N::one(); + for i in 0 .. dim { + res *= unsafe { *self.diag.vget_unchecked(i) }; + } - // res self.q_determinant() - // } + res * self.p.determinant() + } } impl, C: Dim, S: Storage> Matrix diff --git a/tests/linalg/qrp.rs b/tests/linalg/qrp.rs new file mode 100644 index 00000000..78b96b74 --- /dev/null +++ b/tests/linalg/qrp.rs @@ -0,0 +1,156 @@ +#[cfg_attr(rustfmt, rustfmt_skip)] + +use na::Matrix4; + +#[test] +fn qrp() { + let m = Matrix4::new ( + 1.0, -1.0, 2.0, 1.0, + -1.0, 3.0, -1.0, -1.0, + 3.0, -5.0, 5.0, 3.0, + 1.0, 2.0, 1.0, -2.0); + let qrp = m.qrp(); + assert!(relative_eq!(qrp.determinant(), 0.0, epsilon = 1.0e-7)); + + let (q, r, p) = qrp.unpack(); + + let mut qr = q * r; + p.inv_permute_columns(& mut qr); + + assert!(relative_eq!(m, qr, epsilon = 1.0e-7)); +} + +#[cfg(feature = "arbitrary")] +mod quickcheck_tests { + macro_rules! gen_tests( + ($module: ident, $scalar: ty) => { + mod $module { + use na::{DMatrix, DVector, Matrix3x5, Matrix4, Matrix4x3, Matrix5x3, Vector4}; + use std::cmp; + #[allow(unused_imports)] + use crate::core::helper::{RandScalar, RandComplex}; + + quickcheck! { + fn qrp(m: DMatrix<$scalar>) -> bool { + let m = m.map(|e| e.0); + let qrp = m.clone().qrp(); + let q = qrp.q(); + let r = qrp.r(); + + println!("m: {}", m); + println!("qrp: {}", &q * &r); + + relative_eq!(m, &q * r, epsilon = 1.0e-7) && + q.is_orthogonal(1.0e-7) + } + + fn qrp_static_5_3(m: Matrix5x3<$scalar>) -> bool { + let m = m.map(|e| e.0); + let qrp = m.qrp(); + let q = qrp.q(); + let r = qrp.r(); + + relative_eq!(m, q * r, epsilon = 1.0e-7) && + q.is_orthogonal(1.0e-7) + } + + fn qrp_static_3_5(m: Matrix3x5<$scalar>) -> bool { + let m = m.map(|e| e.0); + let qrp = m.qrp(); + let q = qrp.q(); + let r = qrp.r(); + + relative_eq!(m, q * r, epsilon = 1.0e-7) && + q.is_orthogonal(1.0e-7) + } + + fn qrp_static_square(m: Matrix4<$scalar>) -> bool { + let m = m.map(|e| e.0); + let qrp = m.qrp(); + let q = qrp.q(); + let r = qrp.r(); + + println!("{}{}{}{}", q, r, q * r, m); + + relative_eq!(m, q * r, epsilon = 1.0e-7) && + q.is_orthogonal(1.0e-7) + } + + fn qrp_solve(n: usize, nb: usize) -> bool { + if n != 0 && nb != 0 { + let n = cmp::min(n, 50); // To avoid slowing down the test too much. + let nb = cmp::min(nb, 50); // To avoid slowing down the test too much. + let m = DMatrix::<$scalar>::new_random(n, n).map(|e| e.0); + + let qrp = m.clone().qrp(); + let b1 = DVector::<$scalar>::new_random(n).map(|e| e.0); + let b2 = DMatrix::<$scalar>::new_random(n, nb).map(|e| e.0); + + if qrp.is_invertible() { + let sol1 = qrp.solve(&b1).unwrap(); + let sol2 = qrp.solve(&b2).unwrap(); + + return relative_eq!(&m * sol1, b1, epsilon = 1.0e-6) && + relative_eq!(&m * sol2, b2, epsilon = 1.0e-6) + } + } + + return true; + } + + fn qrp_solve_static(m: Matrix4<$scalar>) -> bool { + let m = m.map(|e| e.0); + let qrp = m.qrp(); + let b1 = Vector4::<$scalar>::new_random().map(|e| e.0); + let b2 = Matrix4x3::<$scalar>::new_random().map(|e| e.0); + + if qrp.is_invertible() { + let sol1 = qrp.solve(&b1).unwrap(); + let sol2 = qrp.solve(&b2).unwrap(); + + relative_eq!(m * sol1, b1, epsilon = 1.0e-6) && + relative_eq!(m * sol2, b2, epsilon = 1.0e-6) + } + else { + false + } + } + + fn qrp_inverse(n: usize) -> bool { + let n = cmp::max(1, cmp::min(n, 15)); // To avoid slowing down the test too much. + let m = DMatrix::<$scalar>::new_random(n, n).map(|e| e.0); + + if let Some(m1) = m.clone().qrp().try_inverse() { + let id1 = &m * &m1; + let id2 = &m1 * &m; + + id1.is_identity(1.0e-5) && id2.is_identity(1.0e-5) + } + else { + true + } + } + + fn qrp_inverse_static(m: Matrix4<$scalar>) -> bool { + let m = m.map(|e| e.0); + let qrp = m.qrp(); + + if let Some(m1) = qrp.try_inverse() { + let id1 = &m * &m1; + let id2 = &m1 * &m; + + id1.is_identity(1.0e-5) && id2.is_identity(1.0e-5) + } + else { + true + } + } + } + } + } + ); + + gen_tests!(complex, RandComplex); + gen_tests!(f64, RandScalar); +} + From 308d95386e1f775f437001331eecf2393d992668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Thu, 25 Feb 2021 12:06:04 +0100 Subject: [PATCH 128/183] Fix all tests and the ColPivQR::solve. --- src/linalg/{qrp.rs => col_piv_qr.rs} | 200 +++++++++++++------------ src/linalg/householder.rs | 2 +- src/linalg/mod.rs | 2 + src/linalg/qr.rs | 4 +- tests/linalg/{qrp.rs => col_piv_qr.rs} | 163 ++++++++++---------- tests/linalg/mod.rs | 1 + 6 files changed, 198 insertions(+), 174 deletions(-) rename src/linalg/{qrp.rs => col_piv_qr.rs} (60%) rename tests/linalg/{qrp.rs => col_piv_qr.rs} (52%) diff --git a/src/linalg/qrp.rs b/src/linalg/col_piv_qr.rs similarity index 60% rename from src/linalg/qrp.rs rename to src/linalg/col_piv_qr.rs index c7d2f27b..3f8ecda1 100644 --- a/src/linalg/qrp.rs +++ b/src/linalg/col_piv_qr.rs @@ -1,98 +1,94 @@ +use num::Zero; #[cfg(feature = "serde-serialize")] use serde::{Deserialize, Serialize}; -use num::Zero; -use alga::general::ComplexField; use crate::allocator::{Allocator, Reallocator}; use crate::base::{DefaultAllocator, Matrix, MatrixMN, MatrixN, Unit, VectorN}; use crate::constraint::{SameNumberOfRows, ShapeConstraint}; use crate::dimension::{Dim, DimMin, DimMinimum, U1}; use crate::storage::{Storage, StorageMut}; +use crate::ComplexField; use crate::geometry::Reflection; -use crate::linalg::householder; -use crate::linalg::PermutationSequence; +use crate::linalg::{householder, PermutationSequence}; -/// The QRP decomposition of a general matrix. +/// The QR decomposition (with column pivoting) of a general matrix. #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] #[cfg_attr( feature = "serde-serialize", - serde(bound( - serialize = "DefaultAllocator: Allocator + + serde(bound(serialize = "DefaultAllocator: Allocator + Allocator>, MatrixMN: Serialize, PermutationSequence>: Serialize, - VectorN>: Serialize" - )) + VectorN>: Serialize")) )] #[cfg_attr( feature = "serde-serialize", - serde(bound( - deserialize = "DefaultAllocator: Allocator + + serde(bound(deserialize = "DefaultAllocator: Allocator + Allocator>, MatrixMN: Deserialize<'de>, PermutationSequence>: Deserialize<'de>, - VectorN>: Deserialize<'de>" - )) + VectorN>: Deserialize<'de>")) )] #[derive(Clone, Debug)] -pub struct QRP, C: Dim> -where DefaultAllocator: Allocator + Allocator> + - Allocator<(usize, usize), DimMinimum>, +pub struct ColPivQR, C: Dim> +where + DefaultAllocator: Allocator + + Allocator> + + Allocator<(usize, usize), DimMinimum>, { - qrp: MatrixMN, + col_piv_qr: MatrixMN, p: PermutationSequence>, diag: VectorN>, } -impl, C: Dim> Copy for QRP +impl, C: Dim> Copy for ColPivQR where - DefaultAllocator: Allocator + Allocator> + - Allocator<(usize, usize), DimMinimum>, + DefaultAllocator: Allocator + + Allocator> + + Allocator<(usize, usize), DimMinimum>, MatrixMN: Copy, PermutationSequence>: Copy, VectorN>: Copy, -{} - -impl, C: Dim> QRP -where DefaultAllocator: Allocator + Allocator + Allocator> + Allocator<(usize, usize), DimMinimum> { - /// Computes the QRP decomposition using householder reflections. +} + +impl, C: Dim> ColPivQR +where + DefaultAllocator: Allocator + + Allocator + + Allocator> + + Allocator<(usize, usize), DimMinimum>, +{ + /// Computes the ColPivQR decomposition using householder reflections. pub fn new(mut matrix: MatrixMN) -> Self { let (nrows, ncols) = matrix.data.shape(); let min_nrows_ncols = nrows.min(ncols); let mut p = PermutationSequence::identity_generic(min_nrows_ncols); let mut diag = unsafe { MatrixMN::new_uninitialized_generic(min_nrows_ncols, U1) }; - println!("diag: {:?}", &diag); if min_nrows_ncols.value() == 0 { - return QRP { - qrp: matrix, - p: p, - diag: diag, + return ColPivQR { + col_piv_qr: matrix, + p, + diag, }; } - for ite in 0..min_nrows_ncols.value() { - let mut col_norm = Vec::new(); - for column in matrix.column_iter() { - col_norm.push(column.norm_squared()); - } - - let piv = matrix.slice_range(ite.., ite..).icamax_full(); - let col_piv = piv.1 + ite; - matrix.swap_columns(ite, col_piv); - p.append_permutation(ite, col_piv); + for i in 0..min_nrows_ncols.value() { + let piv = matrix.slice_range(i.., i..).icamax_full(); + let col_piv = piv.1 + i; + matrix.swap_columns(i, col_piv); + p.append_permutation(i, col_piv); - householder::clear_column_unchecked(&mut matrix, &mut diag[ite], ite, 0, None); - println!("matrix: {:?}", &matrix.data); + householder::clear_column_unchecked(&mut matrix, &mut diag[i], i, 0, None); } - QRP { - qrp: matrix, - p: p, - diag: diag, + ColPivQR { + col_piv_qr: matrix, + p, + diag, } } @@ -102,8 +98,11 @@ where DefaultAllocator: Allocator + Allocator + Allocator, C>, { - let (nrows, ncols) = self.qrp.data.shape(); - let mut res = self.qrp.rows_generic(0, nrows.min(ncols)).upper_triangle(); + let (nrows, ncols) = self.col_piv_qr.data.shape(); + let mut res = self + .col_piv_qr + .rows_generic(0, nrows.min(ncols)) + .upper_triangle(); res.set_partial_diagonal(self.diag.iter().map(|e| N::from_real(e.modulus()))); res } @@ -116,8 +115,10 @@ where DefaultAllocator: Allocator + Allocator + Allocator, C>, { - let (nrows, ncols) = self.qrp.data.shape(); - let mut res = self.qrp.resize_generic(nrows.min(ncols), ncols, N::zero()); + let (nrows, ncols) = self.col_piv_qr.data.shape(); + let mut res = self + .col_piv_qr + .resize_generic(nrows.min(ncols), ncols, N::zero()); res.fill_lower_triangle(N::zero(), 1); res.set_partial_diagonal(self.diag.iter().map(|e| N::from_real(e.modulus()))); res @@ -125,8 +126,10 @@ where DefaultAllocator: Allocator + Allocator + Allocator MatrixMN> - where DefaultAllocator: Allocator> { - let (nrows, ncols) = self.qrp.data.shape(); + where + DefaultAllocator: Allocator>, + { + let (nrows, ncols) = self.col_piv_qr.data.shape(); // NOTE: we could build the identity matrix and call q_mul on it. // Instead we don't so that we take in account the matrix sparseness. @@ -134,8 +137,8 @@ where DefaultAllocator: Allocator + Allocator + Allocator + Allocator + Allocator: DimMin>, - DefaultAllocator: - Allocator> + Reallocator, C> + Allocator<(usize, usize), DimMinimum>, + DefaultAllocator: Allocator> + + Reallocator, C> + + Allocator<(usize, usize), DimMinimum>, { (self.q(), self.r(), self.p) } #[doc(hidden)] - pub fn qrp_internal(&self) -> &MatrixMN { - &self.qrp + pub fn col_piv_qr_internal(&self) -> &MatrixMN { + &self.col_piv_qr } /// Multiplies the provided matrix by the transpose of the `Q` matrix of this decomposition. pub fn q_tr_mul(&self, rhs: &mut Matrix) - // FIXME: do we need a static constraint on the number of rows of rhs? - where S2: StorageMut { + where + S2: StorageMut, + { let dim = self.diag.len(); for i in 0..dim { - let axis = self.qrp.slice_range(i.., i); + let axis = self.col_piv_qr.slice_range(i.., i); let refl = Reflection::new(Unit::new_unchecked(axis), N::zero()); let mut rhs_rows = rhs.rows_range_mut(i..); @@ -187,9 +192,10 @@ where DefaultAllocator: Allocator + Allocator + Allocator> QRP -where DefaultAllocator: Allocator + Allocator + - Allocator<(usize, usize), DimMinimum> +impl> ColPivQR +where + DefaultAllocator: + Allocator + Allocator + Allocator<(usize, usize), DimMinimum>, { /// Solves the linear system `self * x = b`, where `x` is the unknown to be determined. /// @@ -222,20 +228,23 @@ where DefaultAllocator: Allocator + Allocator + ShapeConstraint: SameNumberOfRows, { assert_eq!( - self.qrp.nrows(), + self.col_piv_qr.nrows(), b.nrows(), - "QRP solve matrix dimension mismatch." + "ColPivQR solve matrix dimension mismatch." ); assert!( - self.qrp.is_square(), - "QRP solve: unable to solve a non-square system." + self.col_piv_qr.is_square(), + "ColPivQR solve: unable to solve a non-square system." ); self.q_tr_mul(b); - self.solve_upper_triangular_mut(b) + let solved = self.solve_upper_triangular_mut(b); + self.p.inv_permute_rows(b); + + solved } - // FIXME: duplicate code from the `solve` module. + // TODO: duplicate code from the `solve` module. fn solve_upper_triangular_mut( &self, b: &mut Matrix, @@ -244,7 +253,7 @@ where DefaultAllocator: Allocator + Allocator + S2: StorageMut, ShapeConstraint: SameNumberOfRows, { - let dim = self.qrp.nrows(); + let dim = self.col_piv_qr.nrows(); for k in 0..b.ncols() { let mut b = b.column_mut(k); @@ -263,7 +272,7 @@ where DefaultAllocator: Allocator + Allocator + } b.rows_range_mut(..i) - .axpy(-coeff, &self.qrp.slice_range(..i, i), N::one()); + .axpy(-coeff, &self.col_piv_qr.slice_range(..i, i), N::one()); } } @@ -275,12 +284,12 @@ where DefaultAllocator: Allocator + Allocator + /// Returns `None` if the decomposed matrix is not invertible. pub fn try_inverse(&self) -> Option> { assert!( - self.qrp.is_square(), - "QRP inverse: unable to compute the inverse of a non-square matrix." + self.col_piv_qr.is_square(), + "ColPivQR inverse: unable to compute the inverse of a non-square matrix." ); - // FIXME: is there a less naive method ? - let (nrows, ncols) = self.qrp.data.shape(); + // TODO: is there a less naive method ? + let (nrows, ncols) = self.col_piv_qr.data.shape(); let mut res = MatrixN::identity_generic(nrows, ncols); if self.solve_mut(&mut res) { @@ -293,8 +302,8 @@ where DefaultAllocator: Allocator + Allocator + /// Indicates if the decomposed matrix is invertible. pub fn is_invertible(&self) -> bool { assert!( - self.qrp.is_square(), - "QRP: unable to test the invertibility of a non-square matrix." + self.col_piv_qr.is_square(), + "ColPivQR: unable to test the invertibility of a non-square matrix." ); for i in 0..self.diag.len() { @@ -306,25 +315,32 @@ where DefaultAllocator: Allocator + Allocator + true } - /// Computes the determinant of the decomposed matrix. - pub fn determinant(&self) -> N { - let dim = self.qrp.nrows(); - assert!(self.qrp.is_square(), "QRP determinant: unable to compute the determinant of a non-square matrix."); + /// Computes the determinant of the decomposed matrix. + pub fn determinant(&self) -> N { + let dim = self.col_piv_qr.nrows(); + assert!( + self.col_piv_qr.is_square(), + "ColPivQR determinant: unable to compute the determinant of a non-square matrix." + ); - let mut res = N::one(); - for i in 0 .. dim { - res *= unsafe { *self.diag.vget_unchecked(i) }; - } + let mut res = N::one(); + for i in 0..dim { + res *= unsafe { *self.diag.vget_unchecked(i) }; + } - res * self.p.determinant() - } + res * self.p.determinant() + } } impl, C: Dim, S: Storage> Matrix -where DefaultAllocator: Allocator + Allocator + Allocator> + Allocator<(usize, usize), DimMinimum> +where + DefaultAllocator: Allocator + + Allocator + + Allocator> + + Allocator<(usize, usize), DimMinimum>, { - /// Computes the QRP decomposition of this matrix. - pub fn qrp(self) -> QRP { - QRP::new(self.into_owned()) + /// Computes the QR decomposition (with column pivoting) of this matrix. + pub fn col_piv_qr(self) -> ColPivQR { + ColPivQR::new(self.into_owned()) } } diff --git a/src/linalg/householder.rs b/src/linalg/householder.rs index 3692fe31..ebbffd30 100644 --- a/src/linalg/householder.rs +++ b/src/linalg/householder.rs @@ -34,7 +34,7 @@ pub fn reflection_axis_mut>( if !factor.is_zero() { column.unscale_mut(factor.sqrt()); - (signed_norm, true) + (-signed_norm, true) } else { // TODO: not sure why we don't have a - sign here. (signed_norm, false) diff --git a/src/linalg/mod.rs b/src/linalg/mod.rs index c602bfa7..81cf1f38 100644 --- a/src/linalg/mod.rs +++ b/src/linalg/mod.rs @@ -19,6 +19,7 @@ mod inverse; mod lu; mod permutation_sequence; mod qr; +mod col_piv_qr; mod schur; mod solve; mod svd; @@ -39,6 +40,7 @@ pub use self::hessenberg::*; pub use self::lu::*; pub use self::permutation_sequence::*; pub use self::qr::*; +pub use self::col_piv_qr::*; pub use self::schur::*; pub use self::svd::*; pub use self::symmetric_eigen::*; diff --git a/src/linalg/qr.rs b/src/linalg/qr.rs index f404aa5a..fdf6b70a 100644 --- a/src/linalg/qr.rs +++ b/src/linalg/qr.rs @@ -60,8 +60,8 @@ where return QR { qr: matrix, diag }; } - for ite in 0..min_nrows_ncols.value() { - householder::clear_column_unchecked(&mut matrix, &mut diag[ite], ite, 0, None); + for i in 0..min_nrows_ncols.value() { + householder::clear_column_unchecked(&mut matrix, &mut diag[i], i, 0, None); } QR { qr: matrix, diag } diff --git a/tests/linalg/qrp.rs b/tests/linalg/col_piv_qr.rs similarity index 52% rename from tests/linalg/qrp.rs rename to tests/linalg/col_piv_qr.rs index 78b96b74..e9ffba86 100644 --- a/tests/linalg/qrp.rs +++ b/tests/linalg/col_piv_qr.rs @@ -3,19 +3,21 @@ use na::Matrix4; #[test] -fn qrp() { - let m = Matrix4::new ( - 1.0, -1.0, 2.0, 1.0, - -1.0, 3.0, -1.0, -1.0, - 3.0, -5.0, 5.0, 3.0, - 1.0, 2.0, 1.0, -2.0); - let qrp = m.qrp(); - assert!(relative_eq!(qrp.determinant(), 0.0, epsilon = 1.0e-7)); +fn col_piv_qr() { + let m = Matrix4::new( + 1.0, -1.0, 2.0, 1.0, -1.0, 3.0, -1.0, -1.0, 3.0, -5.0, 5.0, 3.0, 1.0, 2.0, 1.0, -2.0, + ); + let col_piv_qr = m.col_piv_qr(); + assert!(relative_eq!( + col_piv_qr.determinant(), + 0.0, + epsilon = 1.0e-7 + )); - let (q, r, p) = qrp.unpack(); + let (q, r, p) = col_piv_qr.unpack(); let mut qr = q * r; - p.inv_permute_columns(& mut qr); + p.inv_permute_columns(&mut qr); assert!(relative_eq!(m, qr, epsilon = 1.0e-7)); } @@ -29,85 +31,89 @@ mod quickcheck_tests { use std::cmp; #[allow(unused_imports)] use crate::core::helper::{RandScalar, RandComplex}; - + quickcheck! { - fn qrp(m: DMatrix<$scalar>) -> bool { + fn col_piv_qr(m: DMatrix<$scalar>) -> bool { let m = m.map(|e| e.0); - let qrp = m.clone().qrp(); - let q = qrp.q(); - let r = qrp.r(); - + let col_piv_qr = m.clone().col_piv_qr(); + let (q, r, p) = col_piv_qr.unpack(); + let mut qr = &q * &r; + p.inv_permute_columns(&mut qr); + println!("m: {}", m); - println!("qrp: {}", &q * &r); - - relative_eq!(m, &q * r, epsilon = 1.0e-7) && - q.is_orthogonal(1.0e-7) - } - - fn qrp_static_5_3(m: Matrix5x3<$scalar>) -> bool { - let m = m.map(|e| e.0); - let qrp = m.qrp(); - let q = qrp.q(); - let r = qrp.r(); - - relative_eq!(m, q * r, epsilon = 1.0e-7) && - q.is_orthogonal(1.0e-7) - } - - fn qrp_static_3_5(m: Matrix3x5<$scalar>) -> bool { - let m = m.map(|e| e.0); - let qrp = m.qrp(); - let q = qrp.q(); - let r = qrp.r(); + println!("col_piv_qr: {}", &q * &r); - relative_eq!(m, q * r, epsilon = 1.0e-7) && - q.is_orthogonal(1.0e-7) - } - - fn qrp_static_square(m: Matrix4<$scalar>) -> bool { - let m = m.map(|e| e.0); - let qrp = m.qrp(); - let q = qrp.q(); - let r = qrp.r(); - - println!("{}{}{}{}", q, r, q * r, m); - - relative_eq!(m, q * r, epsilon = 1.0e-7) && + relative_eq!(m, &qr, epsilon = 1.0e-7) && q.is_orthogonal(1.0e-7) } - - fn qrp_solve(n: usize, nb: usize) -> bool { + + fn col_piv_qr_static_5_3(m: Matrix5x3<$scalar>) -> bool { + let m = m.map(|e| e.0); + let col_piv_qr = m.col_piv_qr(); + let (q, r, p) = col_piv_qr.unpack(); + let mut qr = q * r; + p.inv_permute_columns(&mut qr); + + relative_eq!(m, qr, epsilon = 1.0e-7) && + q.is_orthogonal(1.0e-7) + } + + fn col_piv_qr_static_3_5(m: Matrix3x5<$scalar>) -> bool { + let m = m.map(|e| e.0); + let col_piv_qr = m.col_piv_qr(); + let (q, r, p) = col_piv_qr.unpack(); + let mut qr = q * r; + p.inv_permute_columns(&mut qr); + + relative_eq!(m, qr, epsilon = 1.0e-7) && + q.is_orthogonal(1.0e-7) + } + + fn col_piv_qr_static_square(m: Matrix4<$scalar>) -> bool { + let m = m.map(|e| e.0); + let col_piv_qr = m.col_piv_qr(); + let (q, r, p) = col_piv_qr.unpack(); + let mut qr = q * r; + p.inv_permute_columns(&mut qr); + + println!("{}{}{}{}", q, r, qr, m); + + relative_eq!(m, qr, epsilon = 1.0e-7) && + q.is_orthogonal(1.0e-7) + } + + fn col_piv_qr_solve(n: usize, nb: usize) -> bool { if n != 0 && nb != 0 { let n = cmp::min(n, 50); // To avoid slowing down the test too much. let nb = cmp::min(nb, 50); // To avoid slowing down the test too much. let m = DMatrix::<$scalar>::new_random(n, n).map(|e| e.0); - - let qrp = m.clone().qrp(); + + let col_piv_qr = m.clone().col_piv_qr(); let b1 = DVector::<$scalar>::new_random(n).map(|e| e.0); let b2 = DMatrix::<$scalar>::new_random(n, nb).map(|e| e.0); - - if qrp.is_invertible() { - let sol1 = qrp.solve(&b1).unwrap(); - let sol2 = qrp.solve(&b2).unwrap(); - + + if col_piv_qr.is_invertible() { + let sol1 = col_piv_qr.solve(&b1).unwrap(); + let sol2 = col_piv_qr.solve(&b2).unwrap(); + return relative_eq!(&m * sol1, b1, epsilon = 1.0e-6) && relative_eq!(&m * sol2, b2, epsilon = 1.0e-6) } } - + return true; } - - fn qrp_solve_static(m: Matrix4<$scalar>) -> bool { + + fn col_piv_qr_solve_static(m: Matrix4<$scalar>) -> bool { let m = m.map(|e| e.0); - let qrp = m.qrp(); + let col_piv_qr = m.col_piv_qr(); let b1 = Vector4::<$scalar>::new_random().map(|e| e.0); let b2 = Matrix4x3::<$scalar>::new_random().map(|e| e.0); - - if qrp.is_invertible() { - let sol1 = qrp.solve(&b1).unwrap(); - let sol2 = qrp.solve(&b2).unwrap(); - + + if col_piv_qr.is_invertible() { + let sol1 = col_piv_qr.solve(&b1).unwrap(); + let sol2 = col_piv_qr.solve(&b2).unwrap(); + relative_eq!(m * sol1, b1, epsilon = 1.0e-6) && relative_eq!(m * sol2, b2, epsilon = 1.0e-6) } @@ -116,29 +122,29 @@ mod quickcheck_tests { } } - fn qrp_inverse(n: usize) -> bool { + fn col_piv_qr_inverse(n: usize) -> bool { let n = cmp::max(1, cmp::min(n, 15)); // To avoid slowing down the test too much. let m = DMatrix::<$scalar>::new_random(n, n).map(|e| e.0); - - if let Some(m1) = m.clone().qrp().try_inverse() { + + if let Some(m1) = m.clone().col_piv_qr().try_inverse() { let id1 = &m * &m1; let id2 = &m1 * &m; - + id1.is_identity(1.0e-5) && id2.is_identity(1.0e-5) } else { true } } - - fn qrp_inverse_static(m: Matrix4<$scalar>) -> bool { + + fn col_piv_qr_inverse_static(m: Matrix4<$scalar>) -> bool { let m = m.map(|e| e.0); - let qrp = m.qrp(); - - if let Some(m1) = qrp.try_inverse() { + let col_piv_qr = m.col_piv_qr(); + + if let Some(m1) = col_piv_qr.try_inverse() { let id1 = &m * &m1; let id2 = &m1 * &m; - + id1.is_identity(1.0e-5) && id2.is_identity(1.0e-5) } else { @@ -153,4 +159,3 @@ mod quickcheck_tests { gen_tests!(complex, RandComplex); gen_tests!(f64, RandScalar); } - diff --git a/tests/linalg/mod.rs b/tests/linalg/mod.rs index 7fc01396..66a0c06a 100644 --- a/tests/linalg/mod.rs +++ b/tests/linalg/mod.rs @@ -1,6 +1,7 @@ mod balancing; mod bidiagonal; mod cholesky; +mod col_piv_qr; mod convolution; mod eigen; mod exp; From 693e6d003534b5b4db8b9cb1fd610761baa934c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Thu, 25 Feb 2021 12:59:14 +0100 Subject: [PATCH 129/183] Run cargo fmt. --- src/linalg/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/linalg/mod.rs b/src/linalg/mod.rs index 81cf1f38..df9ae7de 100644 --- a/src/linalg/mod.rs +++ b/src/linalg/mod.rs @@ -8,6 +8,7 @@ mod determinant; // TODO: this should not be needed. However, the exp uses // explicit float operations on `f32` and `f64`. We need to // get rid of these to allow exp to be used on a no-std context. +mod col_piv_qr; mod decomposition; #[cfg(feature = "std")] mod exp; @@ -19,7 +20,6 @@ mod inverse; mod lu; mod permutation_sequence; mod qr; -mod col_piv_qr; mod schur; mod solve; mod svd; @@ -32,6 +32,7 @@ mod symmetric_tridiagonal; pub use self::bidiagonal::*; pub use self::cholesky::*; +pub use self::col_piv_qr::*; pub use self::convolution::*; #[cfg(feature = "std")] pub use self::exp::*; @@ -40,7 +41,6 @@ pub use self::hessenberg::*; pub use self::lu::*; pub use self::permutation_sequence::*; pub use self::qr::*; -pub use self::col_piv_qr::*; pub use self::schur::*; pub use self::svd::*; pub use self::symmetric_eigen::*; From 598c217d759c77b8e92d03f899eb4d041d6dec86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Thu, 25 Feb 2021 13:28:42 +0100 Subject: [PATCH 130/183] Move the col_piv_qr method to the decomposition module. --- src/linalg/col_piv_qr.rs | 13 ------------- src/linalg/decomposition.rs | 21 +++++++++++++++++---- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/linalg/col_piv_qr.rs b/src/linalg/col_piv_qr.rs index 3f8ecda1..4fa97482 100644 --- a/src/linalg/col_piv_qr.rs +++ b/src/linalg/col_piv_qr.rs @@ -331,16 +331,3 @@ where res * self.p.determinant() } } - -impl, C: Dim, S: Storage> Matrix -where - DefaultAllocator: Allocator - + Allocator - + Allocator> - + Allocator<(usize, usize), DimMinimum>, -{ - /// Computes the QR decomposition (with column pivoting) of this matrix. - pub fn col_piv_qr(self) -> ColPivQR { - ColPivQR::new(self.into_owned()) - } -} diff --git a/src/linalg/decomposition.rs b/src/linalg/decomposition.rs index 67cc4c6a..7890748c 100644 --- a/src/linalg/decomposition.rs +++ b/src/linalg/decomposition.rs @@ -1,8 +1,8 @@ use crate::storage::Storage; use crate::{ - Allocator, Bidiagonal, Cholesky, ComplexField, DefaultAllocator, Dim, DimDiff, DimMin, - DimMinimum, DimSub, FullPivLU, Hessenberg, Matrix, Schur, SymmetricEigen, SymmetricTridiagonal, - LU, QR, SVD, U1, + Allocator, Bidiagonal, Cholesky, ColPivQR, ComplexField, DefaultAllocator, Dim, DimDiff, + DimMin, DimMinimum, DimSub, FullPivLU, Hessenberg, Matrix, Schur, SymmetricEigen, + SymmetricTridiagonal, LU, QR, SVD, U1, }; /// # Rectangular matrix decomposition @@ -13,8 +13,9 @@ use crate::{ /// | Decomposition | Factors | Details | /// | -------------------------|---------------------|--------------| /// | QR | `Q * R` | `Q` is an unitary matrix, and `R` is upper-triangular. | +/// | QR with column pivoting | `Q * R * P⁻¹` | `Q` is an unitary matrix, and `R` is upper-triangular. `P` is a permutation matrix. | /// | LU with partial pivoting | `P⁻¹ * L * U` | `L` is lower-triangular with a diagonal filled with `1` and `U` is upper-triangular. `P` is a permutation matrix. | -/// | LU with full pivoting | `P⁻¹ * L * U ~ Q⁻¹` | `L` is lower-triangular with a diagonal filled with `1` and `U` is upper-triangular. `P` and `Q` are permutation matrices. | +/// | LU with full pivoting | `P⁻¹ * L * U * Q⁻¹` | `L` is lower-triangular with a diagonal filled with `1` and `U` is upper-triangular. `P` and `Q` are permutation matrices. | /// | SVD | `U * Σ * Vᵀ` | `U` and `V` are two orthogonal matrices and `Σ` is a diagonal matrix containing the singular values. | impl> Matrix { /// Computes the bidiagonalization using householder reflections. @@ -60,6 +61,18 @@ impl> Matrix { QR::new(self.into_owned()) } + /// Computes the QR decomposition (with column pivoting) of this matrix. + pub fn col_piv_qr(self) -> ColPivQR + where + R: DimMin, + DefaultAllocator: Allocator + + Allocator + + Allocator> + + Allocator<(usize, usize), DimMinimum>, + { + ColPivQR::new(self.into_owned()) + } + /// Computes the Singular Value Decomposition using implicit shift. pub fn svd(self, compute_u: bool, compute_v: bool) -> SVD where From dcd87287bf1eb91a1c1656c645c8608ddd0cf607 Mon Sep 17 00:00:00 2001 From: Remco Bloemen Date: Thu, 19 Nov 2020 09:27:34 -0800 Subject: [PATCH 131/183] Add DoubleEndedIterator test --- tests/core/matrix.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/core/matrix.rs b/tests/core/matrix.rs index ed7b26d2..6b248593 100644 --- a/tests/core/matrix.rs +++ b/tests/core/matrix.rs @@ -21,6 +21,15 @@ fn iter() { assert_eq!(*it.next().unwrap(), 6.0); assert!(it.next().is_none()); + let mut it = a.iter(); + assert_eq!(*it.next().unwrap(), 1.0); + assert_eq!(*it.next_back().unwrap(), 6.0); + assert_eq!(*it.next_back().unwrap(), 3.0); + assert_eq!(*it.next_back().unwrap(), 5.0); + assert_eq!(*it.next().unwrap(), 4.0); + assert_eq!(*it.next().unwrap(), 2.0); + assert!(it.next().is_none()); + let row = a.row(0); let mut it = row.iter(); assert_eq!(*it.next().unwrap(), 1.0); From 73d6ba0ca1195d883c03092fd0446e2c5533b5b1 Mon Sep 17 00:00:00 2001 From: Remco Bloemen Date: Thu, 19 Nov 2020 09:35:43 -0800 Subject: [PATCH 132/183] Dummy implementation of DoubleEndedIterator --- src/base/iter.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/base/iter.rs b/src/base/iter.rs index 1f330d95..8a2a09ef 100644 --- a/src/base/iter.rs +++ b/src/base/iter.rs @@ -119,6 +119,15 @@ macro_rules! iterator { self.size } } + + impl<'a, N: Scalar, R: Dim, C: Dim, S: 'a + $Storage> DoubleEndedIterator + for $Name<'a, N, R, C, S> + { + #[inline] + fn next_back(&mut self) -> Option<$Ref> { + todo!() + } + } }; } From dd31f09105010739b5fb2dfa4a97f18329890eeb Mon Sep 17 00:00:00 2001 From: Remco Bloemen Date: Thu, 19 Nov 2020 11:27:48 -0800 Subject: [PATCH 133/183] Test reverse --- tests/core/matrix.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/core/matrix.rs b/tests/core/matrix.rs index 6b248593..80ab3554 100644 --- a/tests/core/matrix.rs +++ b/tests/core/matrix.rs @@ -11,6 +11,7 @@ use na::{ #[test] fn iter() { let a = Matrix2x3::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0); + dbg!(a); let mut it = a.iter(); assert_eq!(*it.next().unwrap(), 1.0); @@ -30,6 +31,15 @@ fn iter() { assert_eq!(*it.next().unwrap(), 2.0); assert!(it.next().is_none()); + let mut it = a.iter().rev(); + assert_eq!(*it.next().unwrap(), 6.0); + assert_eq!(*it.next().unwrap(), 3.0); + assert_eq!(*it.next().unwrap(), 5.0); + assert_eq!(*it.next().unwrap(), 2.0); + assert_eq!(*it.next().unwrap(), 4.0); + assert_eq!(*it.next().unwrap(), 1.0); + assert!(it.next().is_none()); + let row = a.row(0); let mut it = row.iter(); assert_eq!(*it.next().unwrap(), 1.0); From d49af8e8b287a85fc3321e592866ec09825cc14e Mon Sep 17 00:00:00 2001 From: Remco Bloemen Date: Thu, 19 Nov 2020 11:27:59 -0800 Subject: [PATCH 134/183] Implement next_back --- src/base/iter.rs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/base/iter.rs b/src/base/iter.rs index 8a2a09ef..501ca459 100644 --- a/src/base/iter.rs +++ b/src/base/iter.rs @@ -125,7 +125,35 @@ macro_rules! iterator { { #[inline] fn next_back(&mut self) -> Option<$Ref> { - todo!() + unsafe { + if self.size == 0 { + None + } else { + // Pre-decrement `size` such that it now counts to the + // element we want to return. + self.size -= 1; + + // Fetch strides + let inner_stride = self.strides.0.value(); + let outer_stride = self.strides.1.value(); + debug_assert_eq!(outer_stride % inner_stride, 0); + let num_rows = outer_stride / inner_stride; + + // Compute rows and cols remaining + let outer_remaining = self.size / num_rows; + let inner_remaining = self.size % num_rows; + + // Compute pointer to last element + let last = self.ptr.offset( + (outer_remaining * outer_stride + inner_remaining * inner_stride) + as isize, + ); + + // We want either `& *last` or `&mut *last` here, depending + // on the mutability of `$Ref`. + Some(mem::transmute(last)) + } + } } } }; From 59f4e8a7d4495746b8f19c4d570df32c1885986a Mon Sep 17 00:00:00 2001 From: Remco Bloemen Date: Thu, 19 Nov 2020 11:34:10 -0800 Subject: [PATCH 135/183] Remove dbg statement --- tests/core/matrix.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/core/matrix.rs b/tests/core/matrix.rs index 80ab3554..14eaeacd 100644 --- a/tests/core/matrix.rs +++ b/tests/core/matrix.rs @@ -11,7 +11,6 @@ use na::{ #[test] fn iter() { let a = Matrix2x3::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0); - dbg!(a); let mut it = a.iter(); assert_eq!(*it.next().unwrap(), 1.0); From 2bce1c31a6de18831d74d48bb90fc7f0acbf480e Mon Sep 17 00:00:00 2001 From: Remco Bloemen Date: Thu, 19 Nov 2020 12:07:28 -0800 Subject: [PATCH 136/183] Bench iter() and iter().rev() --- benches/core/matrix.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/benches/core/matrix.rs b/benches/core/matrix.rs index 7b4f85bd..7f4432e3 100644 --- a/benches/core/matrix.rs +++ b/benches/core/matrix.rs @@ -136,6 +136,30 @@ fn mat500_mul_mat500(bench: &mut criterion::Criterion) { bench.bench_function("mat500_mul_mat500", move |bh| bh.iter(|| &a * &b)); } +fn iter(bench: &mut criterion::Criterion) { + let a = DMatrix::::new_random(1000, 1000); + + bench.bench_function("iter", move |bh| { + bh.iter(|| { + for value in a.iter() { + criterion::black_box(value); + } + }) + }); +} + +fn iter_rev(bench: &mut criterion::Criterion) { + let a = DMatrix::::new_random(1000, 1000); + + bench.bench_function("iter_rev", move |bh| { + bh.iter(|| { + for value in a.iter().rev() { + criterion::black_box(value); + } + }) + }); +} + fn copy_from(bench: &mut criterion::Criterion) { let a = DMatrix::::new_random(1000, 1000); let mut b = DMatrix::::new_random(1000, 1000); @@ -235,6 +259,8 @@ criterion_group!( mat10_mul_mat10_static, mat100_mul_mat100, mat500_mul_mat500, + iter, + iter_rev, copy_from, axpy, tr_mul_to, From eb3d787ed637e792e4aaae6d38bed01ad86158f6 Mon Sep 17 00:00:00 2001 From: Remco Bloemen Date: Thu, 19 Nov 2020 12:43:29 -0800 Subject: [PATCH 137/183] Fix inner_size computation --- src/base/iter.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/base/iter.rs b/src/base/iter.rs index 501ca459..bd6e8fc2 100644 --- a/src/base/iter.rs +++ b/src/base/iter.rs @@ -133,15 +133,16 @@ macro_rules! iterator { // element we want to return. self.size -= 1; + // Compute number of rows + let inner_size = self.inner_end.offset_from(self.inner_ptr) as usize; + + // Compute rows and cols remaining + let outer_remaining = self.size / inner_size; + let inner_remaining = self.size % inner_size; + // Fetch strides let inner_stride = self.strides.0.value(); let outer_stride = self.strides.1.value(); - debug_assert_eq!(outer_stride % inner_stride, 0); - let num_rows = outer_stride / inner_stride; - - // Compute rows and cols remaining - let outer_remaining = self.size / num_rows; - let inner_remaining = self.size % num_rows; // Compute pointer to last element let last = self.ptr.offset( From d5ca2019a3f1b88d1ec7039fe4a8c21aa0456715 Mon Sep 17 00:00:00 2001 From: Remco Bloemen Date: Thu, 19 Nov 2020 13:00:28 -0800 Subject: [PATCH 138/183] impl FusedIterator --- src/base/iter.rs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/base/iter.rs b/src/base/iter.rs index bd6e8fc2..75de156e 100644 --- a/src/base/iter.rs +++ b/src/base/iter.rs @@ -1,5 +1,6 @@ //! Matrix iterators. +use std::iter::FusedIterator; use std::marker::PhantomData; use std::mem; @@ -111,15 +112,6 @@ macro_rules! iterator { } } - impl<'a, N: Scalar, R: Dim, C: Dim, S: 'a + $Storage> ExactSizeIterator - for $Name<'a, N, R, C, S> - { - #[inline] - fn len(&self) -> usize { - self.size - } - } - impl<'a, N: Scalar, R: Dim, C: Dim, S: 'a + $Storage> DoubleEndedIterator for $Name<'a, N, R, C, S> { @@ -157,6 +149,20 @@ macro_rules! iterator { } } } + + impl<'a, N: Scalar, R: Dim, C: Dim, S: 'a + $Storage> ExactSizeIterator + for $Name<'a, N, R, C, S> + { + #[inline] + fn len(&self) -> usize { + self.size + } + } + + impl<'a, N: Scalar, R: Dim, C: Dim, S: 'a + $Storage> FusedIterator + for $Name<'a, N, R, C, S> + { + } }; } From b25c2aa78cb9f8ba3a7b5ce0f8faed06e344851c Mon Sep 17 00:00:00 2001 From: Remco Bloemen Date: Thu, 19 Nov 2020 16:30:59 -0800 Subject: [PATCH 139/183] Fix inner size --- src/base/iter.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/base/iter.rs b/src/base/iter.rs index 75de156e..f744cf02 100644 --- a/src/base/iter.rs +++ b/src/base/iter.rs @@ -125,17 +125,19 @@ macro_rules! iterator { // element we want to return. self.size -= 1; + // Fetch strides + let inner_stride = self.strides.0.value(); + let outer_stride = self.strides.1.value(); + // Compute number of rows - let inner_size = self.inner_end.offset_from(self.inner_ptr) as usize; + // Division should be exact + let inner_raw_size = self.inner_end.offset_from(self.inner_ptr) as usize; + let inner_size = inner_raw_size / inner_stride; // Compute rows and cols remaining let outer_remaining = self.size / inner_size; let inner_remaining = self.size % inner_size; - // Fetch strides - let inner_stride = self.strides.0.value(); - let outer_stride = self.strides.1.value(); - // Compute pointer to last element let last = self.ptr.offset( (outer_remaining * outer_stride + inner_remaining * inner_stride) From 36a3ac814fbb90cb28a932456b49e3beb0ba9de7 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Thu, 26 Nov 2020 23:25:36 -0500 Subject: [PATCH 140/183] Propagate `mem::MaybeUninit` through the return types of `Allocator::allocate_uninitialized` and `Matrix::new_uninitialized_generic`. Most call sites still invoke UB through `assume_init`. Said call sites instead invoke `unimplemented!()` if the `no_unsound_assume_init` feature is enabled, to make it easier to gradually fix them. Progress towards #556. --- Cargo.toml | 1 + src/base/allocator.rs | 3 +- src/base/blas.rs | 10 ++++- src/base/construction.rs | 32 ++++++++++++--- src/base/conversion.rs | 22 ++++++---- src/base/default_allocator.rs | 33 ++++++++++----- src/base/edition.rs | 22 ++++++---- src/base/matrix.rs | 64 +++++++++++++++++++++++------ src/base/mod.rs | 2 +- src/base/ops.rs | 18 ++++---- src/base/statistics.rs | 10 ++++- src/geometry/point.rs | 5 ++- src/geometry/point_construction.rs | 5 ++- src/lib.rs | 1 + src/linalg/bidiagonal.rs | 12 +++--- src/linalg/cholesky.rs | 8 ++-- src/linalg/hessenberg.rs | 6 +-- src/linalg/permutation_sequence.rs | 5 ++- src/linalg/qr.rs | 2 +- src/linalg/schur.rs | 12 +++--- src/linalg/symmetric_tridiagonal.rs | 6 +-- 21 files changed, 192 insertions(+), 87 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 314f5174..ca331014 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ compare = [ "matrixcompare-core" ] libm = [ "simba/libm" ] libm-force = [ "simba/libm_force" ] proptest-support = [ "proptest" ] +no_unsound_assume_init = [ ] # This feature is only used for tests, and enables tests that require more time to run slow-tests = [] diff --git a/src/base/allocator.rs b/src/base/allocator.rs index ebd55553..3632cf5d 100644 --- a/src/base/allocator.rs +++ b/src/base/allocator.rs @@ -1,6 +1,7 @@ //! Abstract definition of a matrix data storage allocator. use std::any::Any; +use std::mem; use crate::base::constraint::{SameNumberOfColumns, SameNumberOfRows, ShapeConstraint}; use crate::base::dimension::{Dim, U1}; @@ -21,7 +22,7 @@ pub trait Allocator: Any + Sized { type Buffer: ContiguousStorageMut + Clone; /// Allocates a buffer with the given number of rows and columns without initializing its content. - unsafe fn allocate_uninitialized(nrows: R, ncols: C) -> Self::Buffer; + unsafe fn allocate_uninitialized(nrows: R, ncols: C) -> mem::MaybeUninit; /// Allocates a buffer initialized with the content of the given iterator. fn allocate_from_iterator>( diff --git a/src/base/blas.rs b/src/base/blas.rs index 761077e5..ea56c620 100644 --- a/src/base/blas.rs +++ b/src/base/blas.rs @@ -1328,7 +1328,10 @@ where ShapeConstraint: DimEq + DimEq + DimEq, DefaultAllocator: Allocator, { - let mut work = unsafe { Vector::new_uninitialized_generic(self.data.shape().0, U1) }; + #[cfg(feature="no_unsound_assume_init")] + let mut work = Vector::zeros_generic(self.data.shape().0, U1); + #[cfg(not(feature="no_unsound_assume_init"))] + let mut work = unsafe { Vector::new_uninitialized_generic(self.data.shape().0, U1).assume_init() }; self.quadform_tr_with_workspace(&mut work, alpha, lhs, mid, beta) } @@ -1421,7 +1424,10 @@ where ShapeConstraint: DimEq + DimEq + AreMultipliable, DefaultAllocator: Allocator, { - let mut work = unsafe { Vector::new_uninitialized_generic(mid.data.shape().0, U1) }; + #[cfg(feature="no_unsound_assume_init")] + let mut work = Vector::zeros_generic(mid.data.shape().0, U1); + #[cfg(not(feature="no_unsound_assume_init"))] + let mut work = unsafe { Vector::new_uninitialized_generic(mid.data.shape().0, U1).assume_init() }; self.quadform_with_workspace(&mut work, alpha, mid, rhs, beta) } } diff --git a/src/base/construction.rs b/src/base/construction.rs index 8c34bf3c..722bef75 100644 --- a/src/base/construction.rs +++ b/src/base/construction.rs @@ -14,6 +14,7 @@ use rand::Rng; #[cfg(feature = "std")] use rand_distr::StandardNormal; use std::iter; +use std::mem; use typenum::{self, Cmp, Greater}; #[cfg(feature = "std")] @@ -25,6 +26,16 @@ use crate::base::dimension::{Dim, DimName, Dynamic, U1, U2, U3, U4, U5, U6}; use crate::base::storage::Storage; use crate::base::{DefaultAllocator, Matrix, MatrixMN, MatrixN, Scalar, Unit, Vector, VectorN}; +/// When "no_unsound_assume_init" is enabled, expands to `zeros_generic()` instead of `new_uninitialized_generic().assume_init()`. +/// Intended for use in contexts where the `Scalar` type implements `num_traits::Zero`, to check whether uninitialized memory is actually performance-critical. +#[macro_export] +macro_rules! zero_or_uninitialized_generic { + ($nrows:expr, $ncols:expr) => {{ + #[cfg(feature="no_unsound_assume_init")] { crate::base::Matrix::zeros_generic($nrows, $ncols) } + #[cfg(not(feature="no_unsound_assume_init"))] { crate::base::Matrix::new_uninitialized_generic($nrows, $ncols).assume_init() } + }} +} + /// # Generic constructors /// This set of matrix and vector construction functions are all generic /// with-regard to the matrix dimensions. They all expect to be given @@ -38,8 +49,8 @@ where /// Creates a new uninitialized matrix. If the matrix has a compile-time dimension, this panics /// if `nrows != R::to_usize()` or `ncols != C::to_usize()`. #[inline] - pub unsafe fn new_uninitialized_generic(nrows: R, ncols: C) -> Self { - Self::from_data(DefaultAllocator::allocate_uninitialized(nrows, ncols)) + pub unsafe fn new_uninitialized_generic(nrows: R, ncols: C) -> mem::MaybeUninit { + Self::from_uninitialized_data(DefaultAllocator::allocate_uninitialized(nrows, ncols)) } /// Creates a matrix with all its elements set to `elem`. @@ -88,7 +99,10 @@ where "Matrix init. error: the slice did not contain the right number of elements." ); - let mut res = unsafe { Self::new_uninitialized_generic(nrows, ncols) }; + #[cfg(feature="no_unsound_assume_init")] + let mut res: Self = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let mut res = unsafe { Self::new_uninitialized_generic(nrows, ncols).assume_init() }; let mut iter = slice.iter(); for i in 0..nrows.value() { @@ -114,7 +128,10 @@ where where F: FnMut(usize, usize) -> N, { - let mut res = unsafe { Self::new_uninitialized_generic(nrows, ncols) }; + #[cfg(feature="no_unsound_assume_init")] + let mut res: Self = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let mut res = unsafe { Self::new_uninitialized_generic(nrows, ncols).assume_init() }; for j in 0..ncols.value() { for i in 0..nrows.value() { @@ -356,7 +373,7 @@ macro_rules! impl_constructors( ($($Dims: ty),*; $(=> $DimIdent: ident: $DimBound: ident),*; $($gargs: expr),*; $($args: ident),*) => { /// Creates a new uninitialized matrix or vector. #[inline] - pub unsafe fn new_uninitialized($($args: usize),*) -> Self { + pub unsafe fn new_uninitialized($($args: usize),*) -> mem::MaybeUninit { Self::new_uninitialized_generic($($gargs),*) } @@ -865,7 +882,10 @@ macro_rules! componentwise_constructors_impl( #[inline] pub fn new($($args: N),*) -> Self { unsafe { - let mut res = Self::new_uninitialized(); + #[cfg(feature="no_unsound_assume_init")] + let mut res: Self = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let mut res = Self::new_uninitialized().assume_init(); $( *res.get_unchecked_mut(($irow, $icol)) = $args; )* res diff --git a/src/base/conversion.rs b/src/base/conversion.rs index 77bf4005..97f8ff27 100644 --- a/src/base/conversion.rs +++ b/src/base/conversion.rs @@ -50,7 +50,10 @@ where let nrows2 = R2::from_usize(nrows); let ncols2 = C2::from_usize(ncols); - let mut res = unsafe { MatrixMN::::new_uninitialized_generic(nrows2, ncols2) }; + #[cfg(feature="no_unsound_assume_init")] + let mut res: MatrixMN = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let mut res = unsafe { MatrixMN::::new_uninitialized_generic(nrows2, ncols2).assume_init() }; for i in 0..nrows { for j in 0..ncols { unsafe { @@ -73,7 +76,10 @@ where let nrows = R1::from_usize(nrows2); let ncols = C1::from_usize(ncols2); - let mut res = unsafe { Self::new_uninitialized_generic(nrows, ncols) }; + #[cfg(feature="no_unsound_assume_init")] + let mut res: Self = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let mut res = unsafe { Self::new_uninitialized_generic(nrows, ncols).assume_init() }; for i in 0..nrows2 { for j in 0..ncols2 { unsafe { @@ -117,9 +123,9 @@ macro_rules! impl_from_into_asref_1D( fn from(arr: [N; $SZ]) -> Self { unsafe { let mut res = Self::new_uninitialized(); - ptr::copy_nonoverlapping(&arr[0], res.data.ptr_mut(), $SZ); + ptr::copy_nonoverlapping(&arr[0], (*res.as_mut_ptr()).data.ptr_mut(), $SZ); - res + res.assume_init() } } } @@ -184,9 +190,9 @@ macro_rules! impl_from_into_asref_2D( fn from(arr: [[N; $SZRows]; $SZCols]) -> Self { unsafe { let mut res = Self::new_uninitialized(); - ptr::copy_nonoverlapping(&arr[0][0], res.data.ptr_mut(), $SZRows * $SZCols); + ptr::copy_nonoverlapping(&arr[0][0], (*res.as_mut_ptr()).data.ptr_mut(), $SZRows * $SZCols); - res + res.assume_init() } } } @@ -306,13 +312,13 @@ macro_rules! impl_from_into_mint_2D( fn from(m: mint::$MV) -> Self { unsafe { let mut res = Self::new_uninitialized(); - let mut ptr = res.data.ptr_mut(); + let mut ptr = (*res).data.ptr_mut(); $( ptr::copy_nonoverlapping(&m.$component.x, ptr, $SZRows); ptr = ptr.offset($SZRows); )* let _ = ptr; - res + res.assume_init() } } } diff --git a/src/base/default_allocator.rs b/src/base/default_allocator.rs index bedca471..64b60a66 100644 --- a/src/base/default_allocator.rs +++ b/src/base/default_allocator.rs @@ -45,9 +45,8 @@ where type Buffer = ArrayStorage; #[inline] - unsafe fn allocate_uninitialized(_: R, _: C) -> Self::Buffer { - // TODO: Undefined behavior, see #556 - mem::MaybeUninit::::uninit().assume_init() + unsafe fn allocate_uninitialized(_: R, _: C) -> mem::MaybeUninit { + mem::MaybeUninit::::uninit() } #[inline] @@ -56,7 +55,10 @@ where ncols: C, iter: I, ) -> Self::Buffer { - let mut res = unsafe { Self::allocate_uninitialized(nrows, ncols) }; + #[cfg(feature="no_unsound_assume_init")] + let mut res: Self::Buffer = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let mut res = unsafe { Self::allocate_uninitialized(nrows, ncols).assume_init() }; let mut count = 0; for (res, e) in res.iter_mut().zip(iter.into_iter()) { @@ -80,13 +82,13 @@ impl Allocator for DefaultAllocator { type Buffer = VecStorage; #[inline] - unsafe fn allocate_uninitialized(nrows: Dynamic, ncols: C) -> Self::Buffer { + unsafe fn allocate_uninitialized(nrows: Dynamic, ncols: C) -> mem::MaybeUninit { let mut res = Vec::new(); let length = nrows.value() * ncols.value(); res.reserve_exact(length); res.set_len(length); - VecStorage::new(nrows, ncols, res) + mem::MaybeUninit::new(VecStorage::new(nrows, ncols, res)) } #[inline] @@ -110,13 +112,13 @@ impl Allocator for DefaultAllocator { type Buffer = VecStorage; #[inline] - unsafe fn allocate_uninitialized(nrows: R, ncols: Dynamic) -> Self::Buffer { + unsafe fn allocate_uninitialized(nrows: R, ncols: Dynamic) -> mem::MaybeUninit { let mut res = Vec::new(); let length = nrows.value() * ncols.value(); res.reserve_exact(length); res.set_len(length); - VecStorage::new(nrows, ncols, res) + mem::MaybeUninit::new(VecStorage::new(nrows, ncols, res)) } #[inline] @@ -156,7 +158,10 @@ where cto: CTo, buf: >::Buffer, ) -> ArrayStorage { - let mut res = >::allocate_uninitialized(rto, cto); + #[cfg(feature="no_unsound_assume_init")] + let mut res: ArrayStorage = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let mut res = >::allocate_uninitialized(rto, cto).assume_init(); let (rfrom, cfrom) = buf.shape(); @@ -184,7 +189,10 @@ where cto: CTo, buf: ArrayStorage, ) -> VecStorage { - let mut res = >::allocate_uninitialized(rto, cto); + #[cfg(feature="no_unsound_assume_init")] + let mut res: VecStorage = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let mut res = >::allocate_uninitialized(rto, cto).assume_init(); let (rfrom, cfrom) = buf.shape(); @@ -212,7 +220,10 @@ where cto: Dynamic, buf: ArrayStorage, ) -> VecStorage { - let mut res = >::allocate_uninitialized(rto, cto); + #[cfg(feature="no_unsound_assume_init")] + let mut res: VecStorage = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let mut res = >::allocate_uninitialized(rto, cto).assume_init(); let (rfrom, cfrom) = buf.shape(); diff --git a/src/base/edition.rs b/src/base/edition.rs index 983bde43..40ac5e0e 100644 --- a/src/base/edition.rs +++ b/src/base/edition.rs @@ -54,8 +54,7 @@ impl> Matrix { { let irows = irows.into_iter(); let ncols = self.data.shape().1; - let mut res = - unsafe { MatrixMN::new_uninitialized_generic(Dynamic::new(irows.len()), ncols) }; + let mut res = unsafe { crate::zero_or_uninitialized_generic!(Dynamic::new(irows.len()), ncols) }; // First, check that all the indices from irows are valid. // This will allow us to use unchecked access in the inner loop. @@ -90,7 +89,7 @@ impl> Matrix { let icols = icols.into_iter(); let nrows = self.data.shape().0; let mut res = - unsafe { MatrixMN::new_uninitialized_generic(nrows, Dynamic::new(icols.len())) }; + unsafe { crate::zero_or_uninitialized_generic!(nrows, Dynamic::new(icols.len())) }; for (destination, source) in icols.enumerate() { res.column_mut(destination).copy_from(&self.column(*source)) @@ -896,7 +895,10 @@ impl DMatrix { where DefaultAllocator: Reallocator, { - let placeholder = unsafe { Self::new_uninitialized(0, 0) }; + #[cfg(feature="no_unsound_assume_init")] + let placeholder = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let placeholder = unsafe { Self::new_uninitialized(0, 0).assume_init() }; let old = mem::replace(self, placeholder); let new = old.resize(new_nrows, new_ncols, val); let _ = mem::replace(self, new); @@ -919,8 +921,10 @@ where where DefaultAllocator: Reallocator, { - let placeholder = - unsafe { Self::new_uninitialized_generic(Dynamic::new(0), self.data.shape().1) }; + #[cfg(feature="no_unsound_assume_init")] + let placeholder = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let placeholder = unsafe { Self::new_uninitialized_generic(Dynamic::new(0), self.data.shape().1).assume_init() }; let old = mem::replace(self, placeholder); let new = old.resize_vertically(new_nrows, val); let _ = mem::replace(self, new); @@ -943,8 +947,10 @@ where where DefaultAllocator: Reallocator, { - let placeholder = - unsafe { Self::new_uninitialized_generic(self.data.shape().0, Dynamic::new(0)) }; + #[cfg(feature="no_unsound_assume_init")] + let placeholder = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let placeholder = unsafe { Self::new_uninitialized_generic(self.data.shape().0, Dynamic::new(0)).assume_init() }; let old = mem::replace(self, placeholder); let new = old.resize_horizontally(new_ncols, val); let _ = mem::replace(self, new); diff --git a/src/base/matrix.rs b/src/base/matrix.rs index 8035d2f8..eb5aa316 100644 --- a/src/base/matrix.rs +++ b/src/base/matrix.rs @@ -298,6 +298,17 @@ impl> Matrix { unsafe { Self::from_data_statically_unchecked(data) } } + /// Creates a new uninitialized matrix with the given uninitialized data + pub unsafe fn from_uninitialized_data(data: mem::MaybeUninit) -> mem::MaybeUninit { + let res: Matrix> = Matrix { data, _phantoms: PhantomData }; + let res: mem::MaybeUninit>> = mem::MaybeUninit::new(res); + // safety: since we wrap the inner MaybeUninit in an outer MaybeUninit above, the fact that the `data` field is partially-uninitialized is still opaque. + // with s/transmute_copy/transmute/, rustc claims that `MaybeUninit>>` may be of a different size from `MaybeUninit>` + // but MaybeUninit's documentation says "MaybeUninit is guaranteed to have the same size, alignment, and ABI as T", which implies those types should be the same size + let res: mem::MaybeUninit> = mem::transmute_copy(&res); + res + } + /// The shape of this matrix returned as the tuple (number of rows, number of columns). /// /// # Examples: @@ -496,8 +507,10 @@ impl> Matrix { let nrows: SameShapeR = Dim::from_usize(nrows); let ncols: SameShapeC = Dim::from_usize(ncols); - let mut res: MatrixSum = - unsafe { Matrix::new_uninitialized_generic(nrows, ncols) }; + #[cfg(feature="no_unsound_assume_init")] + let mut res: MatrixSum = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let mut res: MatrixSum = unsafe { Matrix::new_uninitialized_generic(nrows, ncols).assume_init() }; // TODO: use copy_from for j in 0..res.ncols() { @@ -546,7 +559,10 @@ impl> Matrix { let (nrows, ncols) = self.data.shape(); unsafe { - let mut res = Matrix::new_uninitialized_generic(ncols, nrows); + #[cfg(feature="no_unsound_assume_init")] + let mut res = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let mut res = Matrix::new_uninitialized_generic(ncols, nrows).assume_init(); self.transpose_to(&mut res); res @@ -564,7 +580,10 @@ impl> Matrix { { let (nrows, ncols) = self.data.shape(); - let mut res = unsafe { MatrixMN::new_uninitialized_generic(nrows, ncols) }; + #[cfg(feature="no_unsound_assume_init")] + let mut res: MatrixMN = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let mut res = unsafe { MatrixMN::new_uninitialized_generic(nrows, ncols).assume_init() }; for j in 0..ncols.value() { for i in 0..nrows.value() { @@ -608,7 +627,10 @@ impl> Matrix { { let (nrows, ncols) = self.data.shape(); - let mut res = unsafe { MatrixMN::new_uninitialized_generic(nrows, ncols) }; + #[cfg(feature="no_unsound_assume_init")] + let mut res: MatrixMN = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let mut res = unsafe { MatrixMN::new_uninitialized_generic(nrows, ncols).assume_init() }; for j in 0..ncols.value() { for i in 0..nrows.value() { @@ -635,7 +657,10 @@ impl> Matrix { { let (nrows, ncols) = self.data.shape(); - let mut res = unsafe { MatrixMN::new_uninitialized_generic(nrows, ncols) }; + #[cfg(feature="no_unsound_assume_init")] + let mut res: MatrixMN = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let mut res = unsafe { MatrixMN::new_uninitialized_generic(nrows, ncols).assume_init() }; assert_eq!( (nrows.value(), ncols.value()), @@ -676,7 +701,10 @@ impl> Matrix { { let (nrows, ncols) = self.data.shape(); - let mut res = unsafe { MatrixMN::new_uninitialized_generic(nrows, ncols) }; + #[cfg(feature="no_unsound_assume_init")] + let mut res: MatrixMN = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let mut res = unsafe { MatrixMN::new_uninitialized_generic(nrows, ncols).assume_init() }; assert_eq!( (nrows.value(), ncols.value()), @@ -1170,7 +1198,10 @@ impl> Matrix = Matrix::new_uninitialized_generic(ncols, nrows); + #[cfg(feature="no_unsound_assume_init")] + let mut res = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let mut res: MatrixMN<_, C, R> = Matrix::new_uninitialized_generic(ncols, nrows).assume_init(); self.adjoint_to(&mut res); res @@ -1311,7 +1342,10 @@ impl> SquareMatrix { ); let dim = self.data.shape().0; - let mut res = unsafe { VectorN::new_uninitialized_generic(dim, U1) }; + #[cfg(feature="no_unsound_assume_init")] + let mut res: VectorN = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let mut res = unsafe { VectorN::new_uninitialized_generic(dim, U1).assume_init() }; for i in 0..dim.value() { unsafe { @@ -1438,7 +1472,7 @@ impl, S: Storage> Vector { { let len = self.len(); let hnrows = DimSum::::from_usize(len + 1); - let mut res = unsafe { VectorN::::new_uninitialized_generic(hnrows, U1) }; + let mut res: VectorN:: = unsafe { crate::zero_or_uninitialized_generic!(hnrows, U1) }; res.generic_slice_mut((0, 0), self.data.shape()) .copy_from(self); res[(len, 0)] = element; @@ -1783,7 +1817,10 @@ impl::from_usize(3); let ncols = SameShapeC::::from_usize(1); - let mut res = Matrix::new_uninitialized_generic(nrows, ncols); + #[cfg(feature="no_unsound_assume_init")] + let mut res: MatrixCross = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let mut res = Matrix::new_uninitialized_generic(nrows, ncols).assume_init(); let ax = self.get_unchecked((0, 0)); let ay = self.get_unchecked((1, 0)); @@ -1807,7 +1844,10 @@ impl::from_usize(1); let ncols = SameShapeC::::from_usize(3); - let mut res = Matrix::new_uninitialized_generic(nrows, ncols); + #[cfg(feature="no_unsound_assume_init")] + let mut res: MatrixCross = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let mut res = Matrix::new_uninitialized_generic(nrows, ncols).assume_init(); let ax = self.get_unchecked((0, 0)); let ay = self.get_unchecked((0, 1)); diff --git a/src/base/mod.rs b/src/base/mod.rs index edea4a2d..abe2fd48 100644 --- a/src/base/mod.rs +++ b/src/base/mod.rs @@ -15,7 +15,7 @@ mod alias_slice; mod array_storage; mod cg; mod componentwise; -mod construction; +#[macro_use] mod construction; mod construction_slice; mod conversion; mod edition; diff --git a/src/base/ops.rs b/src/base/ops.rs index 01968b47..37d18827 100644 --- a/src/base/ops.rs +++ b/src/base/ops.rs @@ -331,7 +331,10 @@ macro_rules! componentwise_binop_impl( let (nrows, ncols) = self.shape(); let nrows: SameShapeR = Dim::from_usize(nrows); let ncols: SameShapeC = Dim::from_usize(ncols); - Matrix::new_uninitialized_generic(nrows, ncols) + #[cfg(feature="no_unsound_assume_init")] + { unimplemented!() } + #[cfg(not(feature="no_unsound_assume_init"))] + { Matrix::new_uninitialized_generic(nrows, ncols).assume_init() } }; self.$method_to_statically_unchecked(rhs, &mut res); @@ -573,9 +576,7 @@ where #[inline] fn mul(self, rhs: &'b Matrix) -> Self::Output { - let mut res = - unsafe { Matrix::new_uninitialized_generic(self.data.shape().0, rhs.data.shape().1) }; - + let mut res = unsafe { crate::zero_or_uninitialized_generic!(self.data.shape().0, rhs.data.shape().1) }; self.mul_to(rhs, &mut res); res } @@ -684,8 +685,7 @@ where DefaultAllocator: Allocator, ShapeConstraint: SameNumberOfRows, { - let mut res = - unsafe { Matrix::new_uninitialized_generic(self.data.shape().1, rhs.data.shape().1) }; + let mut res = unsafe { crate::zero_or_uninitialized_generic!(self.data.shape().1, rhs.data.shape().1) }; self.tr_mul_to(rhs, &mut res); res @@ -700,8 +700,7 @@ where DefaultAllocator: Allocator, ShapeConstraint: SameNumberOfRows, { - let mut res = - unsafe { Matrix::new_uninitialized_generic(self.data.shape().1, rhs.data.shape().1) }; + let mut res = unsafe { crate::zero_or_uninitialized_generic!(self.data.shape().1, rhs.data.shape().1) }; self.ad_mul_to(rhs, &mut res); res @@ -815,8 +814,7 @@ where let (nrows1, ncols1) = self.data.shape(); let (nrows2, ncols2) = rhs.data.shape(); - let mut res = - unsafe { Matrix::new_uninitialized_generic(nrows1.mul(nrows2), ncols1.mul(ncols2)) }; + let mut res = unsafe { crate::zero_or_uninitialized_generic!(nrows1.mul(nrows2), ncols1.mul(ncols2)) }; { let mut data_res = res.data.ptr_mut(); diff --git a/src/base/statistics.rs b/src/base/statistics.rs index 231f654b..6ac826bf 100644 --- a/src/base/statistics.rs +++ b/src/base/statistics.rs @@ -17,7 +17,10 @@ impl> Matrix { DefaultAllocator: Allocator, { let ncols = self.data.shape().1; - let mut res = unsafe { RowVectorN::new_uninitialized_generic(U1, ncols) }; + #[cfg(feature="no_unsound_assume_init")] + let mut res: RowVectorN = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let mut res = unsafe { RowVectorN::new_uninitialized_generic(U1, ncols).assume_init() }; for i in 0..ncols.value() { // TODO: avoid bound checking of column. @@ -42,7 +45,10 @@ impl> Matrix { DefaultAllocator: Allocator, { let ncols = self.data.shape().1; - let mut res = unsafe { VectorN::new_uninitialized_generic(ncols, U1) }; + #[cfg(feature="no_unsound_assume_init")] + let mut res: VectorN = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let mut res = unsafe { VectorN::new_uninitialized_generic(ncols, U1).assume_init() }; for i in 0..ncols.value() { // TODO: avoid bound checking of column. diff --git a/src/geometry/point.rs b/src/geometry/point.rs index 75410ccd..f5151d9a 100644 --- a/src/geometry/point.rs +++ b/src/geometry/point.rs @@ -181,7 +181,10 @@ where D: DimNameAdd, DefaultAllocator: Allocator>, { - let mut res = unsafe { VectorN::<_, DimNameSum>::new_uninitialized() }; + #[cfg(feature="no_unsound_assume_init")] + let mut res: VectorN> = unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] + let mut res = unsafe { VectorN::<_, DimNameSum>::new_uninitialized().assume_init() }; res.fixed_slice_mut::(0, 0).copy_from(&self.coords); res[(D::dim(), 0)] = N::one(); diff --git a/src/geometry/point_construction.rs b/src/geometry/point_construction.rs index f567cfac..ddf453dd 100644 --- a/src/geometry/point_construction.rs +++ b/src/geometry/point_construction.rs @@ -24,7 +24,10 @@ where /// Creates a new point with uninitialized coordinates. #[inline] pub unsafe fn new_uninitialized() -> Self { - Self::from(VectorN::new_uninitialized()) + #[cfg(feature="no_unsound_assume_init")] + { unimplemented!() } + #[cfg(not(feature="no_unsound_assume_init"))] + { Self::from(VectorN::new_uninitialized().assume_init()) } } /// Creates a new point with all coordinates equal to zero. diff --git a/src/lib.rs b/src/lib.rs index 41620a53..6fb45ef6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -87,6 +87,7 @@ an optimized set of tools for computer graphics and physics. Those features incl )] #![cfg_attr(not(feature = "std"), no_std)] #![cfg_attr(all(feature = "alloc", not(feature = "std")), feature(alloc))] +#![cfg_attr(feature = "no_unsound_assume_init", allow(unreachable_code))] #[cfg(feature = "arbitrary")] extern crate quickcheck; diff --git a/src/linalg/bidiagonal.rs b/src/linalg/bidiagonal.rs index 3ae38432..66957251 100644 --- a/src/linalg/bidiagonal.rs +++ b/src/linalg/bidiagonal.rs @@ -81,11 +81,11 @@ where "Cannot compute the bidiagonalization of an empty matrix." ); - let mut diagonal = unsafe { MatrixMN::new_uninitialized_generic(min_nrows_ncols, U1) }; + let mut diagonal = unsafe { crate::zero_or_uninitialized_generic!(min_nrows_ncols, U1) }; let mut off_diagonal = - unsafe { MatrixMN::new_uninitialized_generic(min_nrows_ncols.sub(U1), U1) }; - let mut axis_packed = unsafe { MatrixMN::new_uninitialized_generic(ncols, U1) }; - let mut work = unsafe { MatrixMN::new_uninitialized_generic(nrows, U1) }; + unsafe { crate::zero_or_uninitialized_generic!(min_nrows_ncols.sub(U1), U1) }; + let mut axis_packed = unsafe { crate::zero_or_uninitialized_generic!(ncols, U1) }; + let mut work = unsafe { crate::zero_or_uninitialized_generic!(nrows, U1) }; let upper_diagonal = nrows.value() >= ncols.value(); if upper_diagonal { @@ -239,8 +239,8 @@ where let min_nrows_ncols = nrows.min(ncols); let mut res = Matrix::identity_generic(min_nrows_ncols, ncols); - let mut work = unsafe { MatrixMN::new_uninitialized_generic(min_nrows_ncols, U1) }; - let mut axis_packed = unsafe { MatrixMN::new_uninitialized_generic(ncols, U1) }; + let mut work = unsafe { crate::zero_or_uninitialized_generic!(min_nrows_ncols, U1) }; + let mut axis_packed = unsafe { crate::zero_or_uninitialized_generic!(ncols, U1) }; let shift = self.axis_shift().1; diff --git a/src/linalg/cholesky.rs b/src/linalg/cholesky.rs index bd2f9281..134c9aa3 100644 --- a/src/linalg/cholesky.rs +++ b/src/linalg/cholesky.rs @@ -223,9 +223,9 @@ where // loads the data into a new matrix with an additional jth row/column let mut chol = unsafe { - Matrix::new_uninitialized_generic( + crate::zero_or_uninitialized_generic!( self.chol.data.shape().0.add(U1), - self.chol.data.shape().1.add(U1), + self.chol.data.shape().1.add(U1) ) }; chol.slice_range_mut(..j, ..j) @@ -288,9 +288,9 @@ where // loads the data into a new matrix except for the jth row/column let mut chol = unsafe { - Matrix::new_uninitialized_generic( + crate::zero_or_uninitialized_generic!( self.chol.data.shape().0.sub(U1), - self.chol.data.shape().1.sub(U1), + self.chol.data.shape().1.sub(U1) ) }; chol.slice_range_mut(..j, ..j) diff --git a/src/linalg/hessenberg.rs b/src/linalg/hessenberg.rs index beff5420..979ddfd8 100644 --- a/src/linalg/hessenberg.rs +++ b/src/linalg/hessenberg.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use crate::allocator::Allocator; -use crate::base::{DefaultAllocator, MatrixMN, MatrixN, VectorN}; +use crate::base::{DefaultAllocator, MatrixN, VectorN}; use crate::dimension::{DimDiff, DimSub, U1}; use crate::storage::Storage; use simba::scalar::ComplexField; @@ -48,7 +48,7 @@ where { /// Computes the Hessenberg decomposition using householder reflections. pub fn new(hess: MatrixN) -> Self { - let mut work = unsafe { MatrixMN::new_uninitialized_generic(hess.data.shape().0, U1) }; + let mut work = unsafe { crate::zero_or_uninitialized_generic!(hess.data.shape().0, U1) }; Self::new_with_workspace(hess, &mut work) } @@ -74,7 +74,7 @@ where "Hessenberg: invalid workspace size." ); - let mut subdiag = unsafe { MatrixMN::new_uninitialized_generic(dim.sub(U1), U1) }; + let mut subdiag = unsafe { crate::zero_or_uninitialized_generic!(dim.sub(U1), U1) }; if dim.value() == 0 { return Hessenberg { hess, subdiag }; diff --git a/src/linalg/permutation_sequence.rs b/src/linalg/permutation_sequence.rs index 47255832..75dae37d 100644 --- a/src/linalg/permutation_sequence.rs +++ b/src/linalg/permutation_sequence.rs @@ -70,9 +70,12 @@ where #[inline] pub fn identity_generic(dim: D) -> Self { unsafe { + #[cfg(feature="no_unsound_assume_init")] + unimplemented!(); + #[cfg(not(feature="no_unsound_assume_init"))] Self { len: 0, - ipiv: VectorN::new_uninitialized_generic(dim, U1), + ipiv: VectorN::new_uninitialized_generic(dim, U1).assume_init(), } } } diff --git a/src/linalg/qr.rs b/src/linalg/qr.rs index f404aa5a..191ccfbe 100644 --- a/src/linalg/qr.rs +++ b/src/linalg/qr.rs @@ -54,7 +54,7 @@ where let (nrows, ncols) = matrix.data.shape(); let min_nrows_ncols = nrows.min(ncols); - let mut diag = unsafe { MatrixMN::new_uninitialized_generic(min_nrows_ncols, U1) }; + let mut diag = unsafe { crate::zero_or_uninitialized_generic!(min_nrows_ncols, U1) }; if min_nrows_ncols.value() == 0 { return QR { qr: matrix, diag }; diff --git a/src/linalg/schur.rs b/src/linalg/schur.rs index 72c9b5ac..b9229047 100644 --- a/src/linalg/schur.rs +++ b/src/linalg/schur.rs @@ -71,7 +71,7 @@ where /// number of iteration is exceeded, `None` is returned. If `niter == 0`, then the algorithm /// continues indefinitely until convergence. pub fn try_new(m: MatrixN, eps: N::RealField, max_niter: usize) -> Option { - let mut work = unsafe { VectorN::new_uninitialized_generic(m.data.shape().0, U1) }; + let mut work = unsafe { crate::zero_or_uninitialized_generic!(m.data.shape().0, U1) }; Self::do_decompose(m, &mut work, eps, max_niter, true) .map(|(q, t)| Schur { q: q.unwrap(), t }) @@ -378,7 +378,7 @@ where /// /// Return `None` if some eigenvalues are complex. pub fn eigenvalues(&self) -> Option> { - let mut out = unsafe { VectorN::new_uninitialized_generic(self.t.data.shape().0, U1) }; + let mut out = unsafe { crate::zero_or_uninitialized_generic!(self.t.data.shape().0, U1) }; if Self::do_eigenvalues(&self.t, &mut out) { Some(out) } else { @@ -392,7 +392,7 @@ where N: RealField, DefaultAllocator: Allocator, D>, { - let mut out = unsafe { VectorN::new_uninitialized_generic(self.t.data.shape().0, U1) }; + let mut out = unsafe { crate::zero_or_uninitialized_generic!(self.t.data.shape().0, U1) }; Self::do_complex_eigenvalues(&self.t, &mut out); out } @@ -503,7 +503,7 @@ where "Unable to compute eigenvalues of a non-square matrix." ); - let mut work = unsafe { VectorN::new_uninitialized_generic(self.data.shape().0, U1) }; + let mut work = unsafe { crate::zero_or_uninitialized_generic!(self.data.shape().0, U1) }; // Special case for 2x2 matrices. if self.nrows() == 2 { @@ -544,7 +544,7 @@ where DefaultAllocator: Allocator, D>, { let dim = self.data.shape().0; - let mut work = unsafe { VectorN::new_uninitialized_generic(dim, U1) }; + let mut work = unsafe { crate::zero_or_uninitialized_generic!(dim, U1) }; let schur = Schur::do_decompose( self.clone_owned(), @@ -554,7 +554,7 @@ where false, ) .unwrap(); - let mut eig = unsafe { VectorN::new_uninitialized_generic(dim, U1) }; + let mut eig = unsafe { crate::zero_or_uninitialized_generic!(dim, U1) }; Schur::do_complex_eigenvalues(&schur.1, &mut eig); eig } diff --git a/src/linalg/symmetric_tridiagonal.rs b/src/linalg/symmetric_tridiagonal.rs index e8d9fb5d..c34a60da 100644 --- a/src/linalg/symmetric_tridiagonal.rs +++ b/src/linalg/symmetric_tridiagonal.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use crate::allocator::Allocator; -use crate::base::{DefaultAllocator, MatrixMN, MatrixN, VectorN}; +use crate::base::{DefaultAllocator, MatrixN, VectorN}; use crate::dimension::{DimDiff, DimSub, U1}; use crate::storage::Storage; use simba::scalar::ComplexField; @@ -61,8 +61,8 @@ where "Unable to compute the symmetric tridiagonal decomposition of an empty matrix." ); - let mut off_diagonal = unsafe { MatrixMN::new_uninitialized_generic(dim.sub(U1), U1) }; - let mut p = unsafe { MatrixMN::new_uninitialized_generic(dim.sub(U1), U1) }; + let mut off_diagonal = unsafe { crate::zero_or_uninitialized_generic!(dim.sub(U1), U1) }; + let mut p = unsafe { crate::zero_or_uninitialized_generic!(dim.sub(U1), U1) }; for i in 0..dim.value() - 1 { let mut m = m.rows_range_mut(i + 1..); From cd12422d6ffc95010bd0fe433f9e2cd6053e07c2 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Fri, 27 Nov 2020 15:58:48 -0500 Subject: [PATCH 141/183] Change `zero_or_uninitialized_generic` to `unimplemented_or_uninitialized_generic`, and use it instead of manually dispatching on `feature=no_unsound_assume_init` in functions without `N: Zero`. --- src/base/blas.rs | 10 ++---- src/base/construction.rs | 25 +++++++------- src/base/conversion.rs | 10 ++---- src/base/edition.rs | 19 +++-------- src/base/matrix.rs | 52 ++++++----------------------- src/base/ops.rs | 13 +++----- src/base/statistics.rs | 10 ++---- src/geometry/point.rs | 5 +-- src/geometry/point_construction.rs | 5 +-- src/linalg/bidiagonal.rs | 12 +++---- src/linalg/cholesky.rs | 4 +-- src/linalg/hessenberg.rs | 4 +-- src/linalg/permutation_sequence.rs | 5 +-- src/linalg/qr.rs | 2 +- src/linalg/schur.rs | 12 +++---- src/linalg/symmetric_tridiagonal.rs | 4 +-- 16 files changed, 62 insertions(+), 130 deletions(-) diff --git a/src/base/blas.rs b/src/base/blas.rs index ea56c620..111c6dd1 100644 --- a/src/base/blas.rs +++ b/src/base/blas.rs @@ -1328,10 +1328,7 @@ where ShapeConstraint: DimEq + DimEq + DimEq, DefaultAllocator: Allocator, { - #[cfg(feature="no_unsound_assume_init")] - let mut work = Vector::zeros_generic(self.data.shape().0, U1); - #[cfg(not(feature="no_unsound_assume_init"))] - let mut work = unsafe { Vector::new_uninitialized_generic(self.data.shape().0, U1).assume_init() }; + let mut work = unsafe { crate::unimplemented_or_uninitialized_generic!(self.data.shape().0, U1) }; self.quadform_tr_with_workspace(&mut work, alpha, lhs, mid, beta) } @@ -1424,10 +1421,7 @@ where ShapeConstraint: DimEq + DimEq + AreMultipliable, DefaultAllocator: Allocator, { - #[cfg(feature="no_unsound_assume_init")] - let mut work = Vector::zeros_generic(mid.data.shape().0, U1); - #[cfg(not(feature="no_unsound_assume_init"))] - let mut work = unsafe { Vector::new_uninitialized_generic(mid.data.shape().0, U1).assume_init() }; + let mut work = unsafe { crate::unimplemented_or_uninitialized_generic!(mid.data.shape().0, U1) }; self.quadform_with_workspace(&mut work, alpha, mid, rhs, beta) } } diff --git a/src/base/construction.rs b/src/base/construction.rs index 722bef75..e0464a02 100644 --- a/src/base/construction.rs +++ b/src/base/construction.rs @@ -26,12 +26,19 @@ use crate::base::dimension::{Dim, DimName, Dynamic, U1, U2, U3, U4, U5, U6}; use crate::base::storage::Storage; use crate::base::{DefaultAllocator, Matrix, MatrixMN, MatrixN, Scalar, Unit, Vector, VectorN}; -/// When "no_unsound_assume_init" is enabled, expands to `zeros_generic()` instead of `new_uninitialized_generic().assume_init()`. -/// Intended for use in contexts where the `Scalar` type implements `num_traits::Zero`, to check whether uninitialized memory is actually performance-critical. +/// When "no_unsound_assume_init" is enabled, expands to `unimplemented!()` instead of `new_uninitialized_generic().assume_init()`. +/// Intended as a placeholder, each callsite should be refactored to use uninitialized memory soundly #[macro_export] -macro_rules! zero_or_uninitialized_generic { +macro_rules! unimplemented_or_uninitialized_generic { ($nrows:expr, $ncols:expr) => {{ - #[cfg(feature="no_unsound_assume_init")] { crate::base::Matrix::zeros_generic($nrows, $ncols) } + #[cfg(feature="no_unsound_assume_init")] { + // Some of the call sites need the number of rows and columns from this to infer a type, so + // uninitialized memory is used to infer the type, as `N: Zero` isn't available at all callsites. + // This may technically still be UB even though the assume_init is dead code, but all callsites should be fixed before #556 is closed. + let typeinference_helper = crate::base::Matrix::new_uninitialized_generic($nrows, $ncols); + unimplemented!(); + typeinference_helper.assume_init() + } #[cfg(not(feature="no_unsound_assume_init"))] { crate::base::Matrix::new_uninitialized_generic($nrows, $ncols).assume_init() } }} } @@ -99,10 +106,7 @@ where "Matrix init. error: the slice did not contain the right number of elements." ); - #[cfg(feature="no_unsound_assume_init")] - let mut res: Self = unimplemented!(); - #[cfg(not(feature="no_unsound_assume_init"))] - let mut res = unsafe { Self::new_uninitialized_generic(nrows, ncols).assume_init() }; + let mut res = unsafe { crate::unimplemented_or_uninitialized_generic!(nrows, ncols) }; let mut iter = slice.iter(); for i in 0..nrows.value() { @@ -128,10 +132,7 @@ where where F: FnMut(usize, usize) -> N, { - #[cfg(feature="no_unsound_assume_init")] - let mut res: Self = unimplemented!(); - #[cfg(not(feature="no_unsound_assume_init"))] - let mut res = unsafe { Self::new_uninitialized_generic(nrows, ncols).assume_init() }; + let mut res: Self = unsafe { crate::unimplemented_or_uninitialized_generic!(nrows, ncols) }; for j in 0..ncols.value() { for i in 0..nrows.value() { diff --git a/src/base/conversion.rs b/src/base/conversion.rs index 97f8ff27..e7a52d1f 100644 --- a/src/base/conversion.rs +++ b/src/base/conversion.rs @@ -50,10 +50,7 @@ where let nrows2 = R2::from_usize(nrows); let ncols2 = C2::from_usize(ncols); - #[cfg(feature="no_unsound_assume_init")] - let mut res: MatrixMN = unimplemented!(); - #[cfg(not(feature="no_unsound_assume_init"))] - let mut res = unsafe { MatrixMN::::new_uninitialized_generic(nrows2, ncols2).assume_init() }; + let mut res: MatrixMN = unsafe { crate::unimplemented_or_uninitialized_generic!(nrows2, ncols2) }; for i in 0..nrows { for j in 0..ncols { unsafe { @@ -76,10 +73,7 @@ where let nrows = R1::from_usize(nrows2); let ncols = C1::from_usize(ncols2); - #[cfg(feature="no_unsound_assume_init")] - let mut res: Self = unimplemented!(); - #[cfg(not(feature="no_unsound_assume_init"))] - let mut res = unsafe { Self::new_uninitialized_generic(nrows, ncols).assume_init() }; + let mut res: Self = unsafe { crate::unimplemented_or_uninitialized_generic!(nrows, ncols) }; for i in 0..nrows2 { for j in 0..ncols2 { unsafe { diff --git a/src/base/edition.rs b/src/base/edition.rs index 40ac5e0e..c2a0595e 100644 --- a/src/base/edition.rs +++ b/src/base/edition.rs @@ -54,7 +54,7 @@ impl> Matrix { { let irows = irows.into_iter(); let ncols = self.data.shape().1; - let mut res = unsafe { crate::zero_or_uninitialized_generic!(Dynamic::new(irows.len()), ncols) }; + let mut res = unsafe { crate::unimplemented_or_uninitialized_generic!(Dynamic::new(irows.len()), ncols) }; // First, check that all the indices from irows are valid. // This will allow us to use unchecked access in the inner loop. @@ -89,7 +89,7 @@ impl> Matrix { let icols = icols.into_iter(); let nrows = self.data.shape().0; let mut res = - unsafe { crate::zero_or_uninitialized_generic!(nrows, Dynamic::new(icols.len())) }; + unsafe { crate::unimplemented_or_uninitialized_generic!(nrows, Dynamic::new(icols.len())) }; for (destination, source) in icols.enumerate() { res.column_mut(destination).copy_from(&self.column(*source)) @@ -895,10 +895,7 @@ impl DMatrix { where DefaultAllocator: Reallocator, { - #[cfg(feature="no_unsound_assume_init")] - let placeholder = unimplemented!(); - #[cfg(not(feature="no_unsound_assume_init"))] - let placeholder = unsafe { Self::new_uninitialized(0, 0).assume_init() }; + let placeholder = unsafe { crate::unimplemented_or_uninitialized_generic!(Dynamic::new(0), Dynamic::new(0)) }; let old = mem::replace(self, placeholder); let new = old.resize(new_nrows, new_ncols, val); let _ = mem::replace(self, new); @@ -921,10 +918,7 @@ where where DefaultAllocator: Reallocator, { - #[cfg(feature="no_unsound_assume_init")] - let placeholder = unimplemented!(); - #[cfg(not(feature="no_unsound_assume_init"))] - let placeholder = unsafe { Self::new_uninitialized_generic(Dynamic::new(0), self.data.shape().1).assume_init() }; + let placeholder = unsafe { crate::unimplemented_or_uninitialized_generic!(Dynamic::new(0), self.data.shape().1) }; let old = mem::replace(self, placeholder); let new = old.resize_vertically(new_nrows, val); let _ = mem::replace(self, new); @@ -947,10 +941,7 @@ where where DefaultAllocator: Reallocator, { - #[cfg(feature="no_unsound_assume_init")] - let placeholder = unimplemented!(); - #[cfg(not(feature="no_unsound_assume_init"))] - let placeholder = unsafe { Self::new_uninitialized_generic(self.data.shape().0, Dynamic::new(0)).assume_init() }; + let placeholder = unsafe { crate::unimplemented_or_uninitialized_generic!(self.data.shape().0, Dynamic::new(0)) }; let old = mem::replace(self, placeholder); let new = old.resize_horizontally(new_ncols, val); let _ = mem::replace(self, new); diff --git a/src/base/matrix.rs b/src/base/matrix.rs index eb5aa316..b5fc169e 100644 --- a/src/base/matrix.rs +++ b/src/base/matrix.rs @@ -507,10 +507,7 @@ impl> Matrix { let nrows: SameShapeR = Dim::from_usize(nrows); let ncols: SameShapeC = Dim::from_usize(ncols); - #[cfg(feature="no_unsound_assume_init")] - let mut res: MatrixSum = unimplemented!(); - #[cfg(not(feature="no_unsound_assume_init"))] - let mut res: MatrixSum = unsafe { Matrix::new_uninitialized_generic(nrows, ncols).assume_init() }; + let mut res: MatrixSum = unsafe { crate::unimplemented_or_uninitialized_generic!(nrows, ncols) }; // TODO: use copy_from for j in 0..res.ncols() { @@ -559,10 +556,7 @@ impl> Matrix { let (nrows, ncols) = self.data.shape(); unsafe { - #[cfg(feature="no_unsound_assume_init")] - let mut res = unimplemented!(); - #[cfg(not(feature="no_unsound_assume_init"))] - let mut res = Matrix::new_uninitialized_generic(ncols, nrows).assume_init(); + let mut res = crate::unimplemented_or_uninitialized_generic!(ncols, nrows); self.transpose_to(&mut res); res @@ -580,10 +574,7 @@ impl> Matrix { { let (nrows, ncols) = self.data.shape(); - #[cfg(feature="no_unsound_assume_init")] - let mut res: MatrixMN = unimplemented!(); - #[cfg(not(feature="no_unsound_assume_init"))] - let mut res = unsafe { MatrixMN::new_uninitialized_generic(nrows, ncols).assume_init() }; + let mut res: MatrixMN = unsafe { crate::unimplemented_or_uninitialized_generic!(nrows, ncols) }; for j in 0..ncols.value() { for i in 0..nrows.value() { @@ -627,10 +618,7 @@ impl> Matrix { { let (nrows, ncols) = self.data.shape(); - #[cfg(feature="no_unsound_assume_init")] - let mut res: MatrixMN = unimplemented!(); - #[cfg(not(feature="no_unsound_assume_init"))] - let mut res = unsafe { MatrixMN::new_uninitialized_generic(nrows, ncols).assume_init() }; + let mut res: MatrixMN = unsafe { crate::unimplemented_or_uninitialized_generic!(nrows, ncols) }; for j in 0..ncols.value() { for i in 0..nrows.value() { @@ -657,10 +645,7 @@ impl> Matrix { { let (nrows, ncols) = self.data.shape(); - #[cfg(feature="no_unsound_assume_init")] - let mut res: MatrixMN = unimplemented!(); - #[cfg(not(feature="no_unsound_assume_init"))] - let mut res = unsafe { MatrixMN::new_uninitialized_generic(nrows, ncols).assume_init() }; + let mut res: MatrixMN = unsafe { crate::unimplemented_or_uninitialized_generic!(nrows, ncols) }; assert_eq!( (nrows.value(), ncols.value()), @@ -701,10 +686,7 @@ impl> Matrix { { let (nrows, ncols) = self.data.shape(); - #[cfg(feature="no_unsound_assume_init")] - let mut res: MatrixMN = unimplemented!(); - #[cfg(not(feature="no_unsound_assume_init"))] - let mut res = unsafe { MatrixMN::new_uninitialized_generic(nrows, ncols).assume_init() }; + let mut res: MatrixMN = unsafe { crate::unimplemented_or_uninitialized_generic!(nrows, ncols) }; assert_eq!( (nrows.value(), ncols.value()), @@ -1198,10 +1180,7 @@ impl> Matrix = Matrix::new_uninitialized_generic(ncols, nrows).assume_init(); + let mut res: MatrixMN<_, C, R> = crate::unimplemented_or_uninitialized_generic!(ncols, nrows); self.adjoint_to(&mut res); res @@ -1342,10 +1321,7 @@ impl> SquareMatrix { ); let dim = self.data.shape().0; - #[cfg(feature="no_unsound_assume_init")] - let mut res: VectorN = unimplemented!(); - #[cfg(not(feature="no_unsound_assume_init"))] - let mut res = unsafe { VectorN::new_uninitialized_generic(dim, U1).assume_init() }; + let mut res: VectorN = unsafe { crate::unimplemented_or_uninitialized_generic!(dim, U1) }; for i in 0..dim.value() { unsafe { @@ -1472,7 +1448,7 @@ impl, S: Storage> Vector { { let len = self.len(); let hnrows = DimSum::::from_usize(len + 1); - let mut res: VectorN:: = unsafe { crate::zero_or_uninitialized_generic!(hnrows, U1) }; + let mut res: VectorN:: = unsafe { crate::unimplemented_or_uninitialized_generic!(hnrows, U1) }; res.generic_slice_mut((0, 0), self.data.shape()) .copy_from(self); res[(len, 0)] = element; @@ -1817,10 +1793,7 @@ impl::from_usize(3); let ncols = SameShapeC::::from_usize(1); - #[cfg(feature="no_unsound_assume_init")] - let mut res: MatrixCross = unimplemented!(); - #[cfg(not(feature="no_unsound_assume_init"))] - let mut res = Matrix::new_uninitialized_generic(nrows, ncols).assume_init(); + let mut res: MatrixCross = crate::unimplemented_or_uninitialized_generic!(nrows, ncols); let ax = self.get_unchecked((0, 0)); let ay = self.get_unchecked((1, 0)); @@ -1844,10 +1817,7 @@ impl::from_usize(1); let ncols = SameShapeC::::from_usize(3); - #[cfg(feature="no_unsound_assume_init")] - let mut res: MatrixCross = unimplemented!(); - #[cfg(not(feature="no_unsound_assume_init"))] - let mut res = Matrix::new_uninitialized_generic(nrows, ncols).assume_init(); + let mut res: MatrixCross = crate::unimplemented_or_uninitialized_generic!(nrows, ncols); let ax = self.get_unchecked((0, 0)); let ay = self.get_unchecked((0, 1)); diff --git a/src/base/ops.rs b/src/base/ops.rs index 37d18827..93ca3b8e 100644 --- a/src/base/ops.rs +++ b/src/base/ops.rs @@ -331,10 +331,7 @@ macro_rules! componentwise_binop_impl( let (nrows, ncols) = self.shape(); let nrows: SameShapeR = Dim::from_usize(nrows); let ncols: SameShapeC = Dim::from_usize(ncols); - #[cfg(feature="no_unsound_assume_init")] - { unimplemented!() } - #[cfg(not(feature="no_unsound_assume_init"))] - { Matrix::new_uninitialized_generic(nrows, ncols).assume_init() } + crate::unimplemented_or_uninitialized_generic!(nrows, ncols) }; self.$method_to_statically_unchecked(rhs, &mut res); @@ -576,7 +573,7 @@ where #[inline] fn mul(self, rhs: &'b Matrix) -> Self::Output { - let mut res = unsafe { crate::zero_or_uninitialized_generic!(self.data.shape().0, rhs.data.shape().1) }; + let mut res = unsafe { crate::unimplemented_or_uninitialized_generic!(self.data.shape().0, rhs.data.shape().1) }; self.mul_to(rhs, &mut res); res } @@ -685,7 +682,7 @@ where DefaultAllocator: Allocator, ShapeConstraint: SameNumberOfRows, { - let mut res = unsafe { crate::zero_or_uninitialized_generic!(self.data.shape().1, rhs.data.shape().1) }; + let mut res = unsafe { crate::unimplemented_or_uninitialized_generic!(self.data.shape().1, rhs.data.shape().1) }; self.tr_mul_to(rhs, &mut res); res @@ -700,7 +697,7 @@ where DefaultAllocator: Allocator, ShapeConstraint: SameNumberOfRows, { - let mut res = unsafe { crate::zero_or_uninitialized_generic!(self.data.shape().1, rhs.data.shape().1) }; + let mut res = unsafe { crate::unimplemented_or_uninitialized_generic!(self.data.shape().1, rhs.data.shape().1) }; self.ad_mul_to(rhs, &mut res); res @@ -814,7 +811,7 @@ where let (nrows1, ncols1) = self.data.shape(); let (nrows2, ncols2) = rhs.data.shape(); - let mut res = unsafe { crate::zero_or_uninitialized_generic!(nrows1.mul(nrows2), ncols1.mul(ncols2)) }; + let mut res = unsafe { crate::unimplemented_or_uninitialized_generic!(nrows1.mul(nrows2), ncols1.mul(ncols2)) }; { let mut data_res = res.data.ptr_mut(); diff --git a/src/base/statistics.rs b/src/base/statistics.rs index 6ac826bf..7ee5a9c9 100644 --- a/src/base/statistics.rs +++ b/src/base/statistics.rs @@ -17,10 +17,7 @@ impl> Matrix { DefaultAllocator: Allocator, { let ncols = self.data.shape().1; - #[cfg(feature="no_unsound_assume_init")] - let mut res: RowVectorN = unimplemented!(); - #[cfg(not(feature="no_unsound_assume_init"))] - let mut res = unsafe { RowVectorN::new_uninitialized_generic(U1, ncols).assume_init() }; + let mut res: RowVectorN = unsafe { crate::unimplemented_or_uninitialized_generic!(U1, ncols) }; for i in 0..ncols.value() { // TODO: avoid bound checking of column. @@ -45,10 +42,7 @@ impl> Matrix { DefaultAllocator: Allocator, { let ncols = self.data.shape().1; - #[cfg(feature="no_unsound_assume_init")] - let mut res: VectorN = unimplemented!(); - #[cfg(not(feature="no_unsound_assume_init"))] - let mut res = unsafe { VectorN::new_uninitialized_generic(ncols, U1).assume_init() }; + let mut res: VectorN = unsafe { crate::unimplemented_or_uninitialized_generic!(ncols, U1) }; for i in 0..ncols.value() { // TODO: avoid bound checking of column. diff --git a/src/geometry/point.rs b/src/geometry/point.rs index f5151d9a..d668e804 100644 --- a/src/geometry/point.rs +++ b/src/geometry/point.rs @@ -181,10 +181,7 @@ where D: DimNameAdd, DefaultAllocator: Allocator>, { - #[cfg(feature="no_unsound_assume_init")] - let mut res: VectorN> = unimplemented!(); - #[cfg(not(feature="no_unsound_assume_init"))] - let mut res = unsafe { VectorN::<_, DimNameSum>::new_uninitialized().assume_init() }; + let mut res = unsafe { crate::unimplemented_or_uninitialized_generic!( as DimName>::name(), U1) }; res.fixed_slice_mut::(0, 0).copy_from(&self.coords); res[(D::dim(), 0)] = N::one(); diff --git a/src/geometry/point_construction.rs b/src/geometry/point_construction.rs index ddf453dd..48c4a71c 100644 --- a/src/geometry/point_construction.rs +++ b/src/geometry/point_construction.rs @@ -24,10 +24,7 @@ where /// Creates a new point with uninitialized coordinates. #[inline] pub unsafe fn new_uninitialized() -> Self { - #[cfg(feature="no_unsound_assume_init")] - { unimplemented!() } - #[cfg(not(feature="no_unsound_assume_init"))] - { Self::from(VectorN::new_uninitialized().assume_init()) } + Self::from(crate::unimplemented_or_uninitialized_generic!(D::name(), U1)) } /// Creates a new point with all coordinates equal to zero. diff --git a/src/linalg/bidiagonal.rs b/src/linalg/bidiagonal.rs index 66957251..0202eb68 100644 --- a/src/linalg/bidiagonal.rs +++ b/src/linalg/bidiagonal.rs @@ -81,11 +81,11 @@ where "Cannot compute the bidiagonalization of an empty matrix." ); - let mut diagonal = unsafe { crate::zero_or_uninitialized_generic!(min_nrows_ncols, U1) }; + let mut diagonal = unsafe { crate::unimplemented_or_uninitialized_generic!(min_nrows_ncols, U1) }; let mut off_diagonal = - unsafe { crate::zero_or_uninitialized_generic!(min_nrows_ncols.sub(U1), U1) }; - let mut axis_packed = unsafe { crate::zero_or_uninitialized_generic!(ncols, U1) }; - let mut work = unsafe { crate::zero_or_uninitialized_generic!(nrows, U1) }; + unsafe { crate::unimplemented_or_uninitialized_generic!(min_nrows_ncols.sub(U1), U1) }; + let mut axis_packed = unsafe { crate::unimplemented_or_uninitialized_generic!(ncols, U1) }; + let mut work = unsafe { crate::unimplemented_or_uninitialized_generic!(nrows, U1) }; let upper_diagonal = nrows.value() >= ncols.value(); if upper_diagonal { @@ -239,8 +239,8 @@ where let min_nrows_ncols = nrows.min(ncols); let mut res = Matrix::identity_generic(min_nrows_ncols, ncols); - let mut work = unsafe { crate::zero_or_uninitialized_generic!(min_nrows_ncols, U1) }; - let mut axis_packed = unsafe { crate::zero_or_uninitialized_generic!(ncols, U1) }; + let mut work = unsafe { crate::unimplemented_or_uninitialized_generic!(min_nrows_ncols, U1) }; + let mut axis_packed = unsafe { crate::unimplemented_or_uninitialized_generic!(ncols, U1) }; let shift = self.axis_shift().1; diff --git a/src/linalg/cholesky.rs b/src/linalg/cholesky.rs index 134c9aa3..a6757b08 100644 --- a/src/linalg/cholesky.rs +++ b/src/linalg/cholesky.rs @@ -223,7 +223,7 @@ where // loads the data into a new matrix with an additional jth row/column let mut chol = unsafe { - crate::zero_or_uninitialized_generic!( + crate::unimplemented_or_uninitialized_generic!( self.chol.data.shape().0.add(U1), self.chol.data.shape().1.add(U1) ) @@ -288,7 +288,7 @@ where // loads the data into a new matrix except for the jth row/column let mut chol = unsafe { - crate::zero_or_uninitialized_generic!( + crate::unimplemented_or_uninitialized_generic!( self.chol.data.shape().0.sub(U1), self.chol.data.shape().1.sub(U1) ) diff --git a/src/linalg/hessenberg.rs b/src/linalg/hessenberg.rs index 979ddfd8..c6103ffd 100644 --- a/src/linalg/hessenberg.rs +++ b/src/linalg/hessenberg.rs @@ -48,7 +48,7 @@ where { /// Computes the Hessenberg decomposition using householder reflections. pub fn new(hess: MatrixN) -> Self { - let mut work = unsafe { crate::zero_or_uninitialized_generic!(hess.data.shape().0, U1) }; + let mut work = unsafe { crate::unimplemented_or_uninitialized_generic!(hess.data.shape().0, U1) }; Self::new_with_workspace(hess, &mut work) } @@ -74,7 +74,7 @@ where "Hessenberg: invalid workspace size." ); - let mut subdiag = unsafe { crate::zero_or_uninitialized_generic!(dim.sub(U1), U1) }; + let mut subdiag = unsafe { crate::unimplemented_or_uninitialized_generic!(dim.sub(U1), U1) }; if dim.value() == 0 { return Hessenberg { hess, subdiag }; diff --git a/src/linalg/permutation_sequence.rs b/src/linalg/permutation_sequence.rs index 75dae37d..dd389188 100644 --- a/src/linalg/permutation_sequence.rs +++ b/src/linalg/permutation_sequence.rs @@ -70,12 +70,9 @@ where #[inline] pub fn identity_generic(dim: D) -> Self { unsafe { - #[cfg(feature="no_unsound_assume_init")] - unimplemented!(); - #[cfg(not(feature="no_unsound_assume_init"))] Self { len: 0, - ipiv: VectorN::new_uninitialized_generic(dim, U1).assume_init(), + ipiv: crate::unimplemented_or_uninitialized_generic!(dim, U1), } } } diff --git a/src/linalg/qr.rs b/src/linalg/qr.rs index 191ccfbe..063cad0d 100644 --- a/src/linalg/qr.rs +++ b/src/linalg/qr.rs @@ -54,7 +54,7 @@ where let (nrows, ncols) = matrix.data.shape(); let min_nrows_ncols = nrows.min(ncols); - let mut diag = unsafe { crate::zero_or_uninitialized_generic!(min_nrows_ncols, U1) }; + let mut diag = unsafe { crate::unimplemented_or_uninitialized_generic!(min_nrows_ncols, U1) }; if min_nrows_ncols.value() == 0 { return QR { qr: matrix, diag }; diff --git a/src/linalg/schur.rs b/src/linalg/schur.rs index b9229047..820c0e21 100644 --- a/src/linalg/schur.rs +++ b/src/linalg/schur.rs @@ -71,7 +71,7 @@ where /// number of iteration is exceeded, `None` is returned. If `niter == 0`, then the algorithm /// continues indefinitely until convergence. pub fn try_new(m: MatrixN, eps: N::RealField, max_niter: usize) -> Option { - let mut work = unsafe { crate::zero_or_uninitialized_generic!(m.data.shape().0, U1) }; + let mut work = unsafe { crate::unimplemented_or_uninitialized_generic!(m.data.shape().0, U1) }; Self::do_decompose(m, &mut work, eps, max_niter, true) .map(|(q, t)| Schur { q: q.unwrap(), t }) @@ -378,7 +378,7 @@ where /// /// Return `None` if some eigenvalues are complex. pub fn eigenvalues(&self) -> Option> { - let mut out = unsafe { crate::zero_or_uninitialized_generic!(self.t.data.shape().0, U1) }; + let mut out = unsafe { crate::unimplemented_or_uninitialized_generic!(self.t.data.shape().0, U1) }; if Self::do_eigenvalues(&self.t, &mut out) { Some(out) } else { @@ -392,7 +392,7 @@ where N: RealField, DefaultAllocator: Allocator, D>, { - let mut out = unsafe { crate::zero_or_uninitialized_generic!(self.t.data.shape().0, U1) }; + let mut out = unsafe { crate::unimplemented_or_uninitialized_generic!(self.t.data.shape().0, U1) }; Self::do_complex_eigenvalues(&self.t, &mut out); out } @@ -503,7 +503,7 @@ where "Unable to compute eigenvalues of a non-square matrix." ); - let mut work = unsafe { crate::zero_or_uninitialized_generic!(self.data.shape().0, U1) }; + let mut work = unsafe { crate::unimplemented_or_uninitialized_generic!(self.data.shape().0, U1) }; // Special case for 2x2 matrices. if self.nrows() == 2 { @@ -544,7 +544,7 @@ where DefaultAllocator: Allocator, D>, { let dim = self.data.shape().0; - let mut work = unsafe { crate::zero_or_uninitialized_generic!(dim, U1) }; + let mut work = unsafe { crate::unimplemented_or_uninitialized_generic!(dim, U1) }; let schur = Schur::do_decompose( self.clone_owned(), @@ -554,7 +554,7 @@ where false, ) .unwrap(); - let mut eig = unsafe { crate::zero_or_uninitialized_generic!(dim, U1) }; + let mut eig = unsafe { crate::unimplemented_or_uninitialized_generic!(dim, U1) }; Schur::do_complex_eigenvalues(&schur.1, &mut eig); eig } diff --git a/src/linalg/symmetric_tridiagonal.rs b/src/linalg/symmetric_tridiagonal.rs index c34a60da..ba47724e 100644 --- a/src/linalg/symmetric_tridiagonal.rs +++ b/src/linalg/symmetric_tridiagonal.rs @@ -61,8 +61,8 @@ where "Unable to compute the symmetric tridiagonal decomposition of an empty matrix." ); - let mut off_diagonal = unsafe { crate::zero_or_uninitialized_generic!(dim.sub(U1), U1) }; - let mut p = unsafe { crate::zero_or_uninitialized_generic!(dim.sub(U1), U1) }; + let mut off_diagonal = unsafe { crate::unimplemented_or_uninitialized_generic!(dim.sub(U1), U1) }; + let mut p = unsafe { crate::unimplemented_or_uninitialized_generic!(dim.sub(U1), U1) }; for i in 0..dim.value() - 1 { let mut m = m.rows_range_mut(i + 1..); From ee32f7d4cfa841601c615a71ed6fbd201badcd11 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Fri, 27 Nov 2020 16:00:48 -0500 Subject: [PATCH 142/183] Run `cargo fmt`. --- src/base/blas.rs | 6 +++-- src/base/conversion.rs | 3 ++- src/base/default_allocator.rs | 25 ++++++++++--------- src/base/edition.rs | 21 +++++++++++----- src/base/matrix.rs | 38 ++++++++++++++++++++--------- src/base/mod.rs | 3 ++- src/base/ops.rs | 16 +++++++++--- src/base/statistics.rs | 6 +++-- src/geometry/point.rs | 7 +++++- src/geometry/point_construction.rs | 5 +++- src/linalg/bidiagonal.rs | 6 +++-- src/linalg/hessenberg.rs | 6 +++-- src/linalg/qr.rs | 3 ++- src/linalg/schur.rs | 12 ++++++--- src/linalg/symmetric_tridiagonal.rs | 3 ++- 15 files changed, 109 insertions(+), 51 deletions(-) diff --git a/src/base/blas.rs b/src/base/blas.rs index 111c6dd1..92a43a38 100644 --- a/src/base/blas.rs +++ b/src/base/blas.rs @@ -1328,7 +1328,8 @@ where ShapeConstraint: DimEq + DimEq + DimEq, DefaultAllocator: Allocator, { - let mut work = unsafe { crate::unimplemented_or_uninitialized_generic!(self.data.shape().0, U1) }; + let mut work = + unsafe { crate::unimplemented_or_uninitialized_generic!(self.data.shape().0, U1) }; self.quadform_tr_with_workspace(&mut work, alpha, lhs, mid, beta) } @@ -1421,7 +1422,8 @@ where ShapeConstraint: DimEq + DimEq + AreMultipliable, DefaultAllocator: Allocator, { - let mut work = unsafe { crate::unimplemented_or_uninitialized_generic!(mid.data.shape().0, U1) }; + let mut work = + unsafe { crate::unimplemented_or_uninitialized_generic!(mid.data.shape().0, U1) }; self.quadform_with_workspace(&mut work, alpha, mid, rhs, beta) } } diff --git a/src/base/conversion.rs b/src/base/conversion.rs index e7a52d1f..b852ff59 100644 --- a/src/base/conversion.rs +++ b/src/base/conversion.rs @@ -50,7 +50,8 @@ where let nrows2 = R2::from_usize(nrows); let ncols2 = C2::from_usize(ncols); - let mut res: MatrixMN = unsafe { crate::unimplemented_or_uninitialized_generic!(nrows2, ncols2) }; + let mut res: MatrixMN = + unsafe { crate::unimplemented_or_uninitialized_generic!(nrows2, ncols2) }; for i in 0..nrows { for j in 0..ncols { unsafe { diff --git a/src/base/default_allocator.rs b/src/base/default_allocator.rs index 64b60a66..81ed1f53 100644 --- a/src/base/default_allocator.rs +++ b/src/base/default_allocator.rs @@ -55,9 +55,9 @@ where ncols: C, iter: I, ) -> Self::Buffer { - #[cfg(feature="no_unsound_assume_init")] + #[cfg(feature = "no_unsound_assume_init")] let mut res: Self::Buffer = unimplemented!(); - #[cfg(not(feature="no_unsound_assume_init"))] + #[cfg(not(feature = "no_unsound_assume_init"))] let mut res = unsafe { Self::allocate_uninitialized(nrows, ncols).assume_init() }; let mut count = 0; @@ -158,10 +158,11 @@ where cto: CTo, buf: >::Buffer, ) -> ArrayStorage { - #[cfg(feature="no_unsound_assume_init")] + #[cfg(feature = "no_unsound_assume_init")] let mut res: ArrayStorage = unimplemented!(); - #[cfg(not(feature="no_unsound_assume_init"))] - let mut res = >::allocate_uninitialized(rto, cto).assume_init(); + #[cfg(not(feature = "no_unsound_assume_init"))] + let mut res = + >::allocate_uninitialized(rto, cto).assume_init(); let (rfrom, cfrom) = buf.shape(); @@ -189,10 +190,11 @@ where cto: CTo, buf: ArrayStorage, ) -> VecStorage { - #[cfg(feature="no_unsound_assume_init")] + #[cfg(feature = "no_unsound_assume_init")] let mut res: VecStorage = unimplemented!(); - #[cfg(not(feature="no_unsound_assume_init"))] - let mut res = >::allocate_uninitialized(rto, cto).assume_init(); + #[cfg(not(feature = "no_unsound_assume_init"))] + let mut res = + >::allocate_uninitialized(rto, cto).assume_init(); let (rfrom, cfrom) = buf.shape(); @@ -220,10 +222,11 @@ where cto: Dynamic, buf: ArrayStorage, ) -> VecStorage { - #[cfg(feature="no_unsound_assume_init")] + #[cfg(feature = "no_unsound_assume_init")] let mut res: VecStorage = unimplemented!(); - #[cfg(not(feature="no_unsound_assume_init"))] - let mut res = >::allocate_uninitialized(rto, cto).assume_init(); + #[cfg(not(feature = "no_unsound_assume_init"))] + let mut res = + >::allocate_uninitialized(rto, cto).assume_init(); let (rfrom, cfrom) = buf.shape(); diff --git a/src/base/edition.rs b/src/base/edition.rs index c2a0595e..9d8606af 100644 --- a/src/base/edition.rs +++ b/src/base/edition.rs @@ -54,7 +54,9 @@ impl> Matrix { { let irows = irows.into_iter(); let ncols = self.data.shape().1; - let mut res = unsafe { crate::unimplemented_or_uninitialized_generic!(Dynamic::new(irows.len()), ncols) }; + let mut res = unsafe { + crate::unimplemented_or_uninitialized_generic!(Dynamic::new(irows.len()), ncols) + }; // First, check that all the indices from irows are valid. // This will allow us to use unchecked access in the inner loop. @@ -88,8 +90,9 @@ impl> Matrix { { let icols = icols.into_iter(); let nrows = self.data.shape().0; - let mut res = - unsafe { crate::unimplemented_or_uninitialized_generic!(nrows, Dynamic::new(icols.len())) }; + let mut res = unsafe { + crate::unimplemented_or_uninitialized_generic!(nrows, Dynamic::new(icols.len())) + }; for (destination, source) in icols.enumerate() { res.column_mut(destination).copy_from(&self.column(*source)) @@ -895,7 +898,9 @@ impl DMatrix { where DefaultAllocator: Reallocator, { - let placeholder = unsafe { crate::unimplemented_or_uninitialized_generic!(Dynamic::new(0), Dynamic::new(0)) }; + let placeholder = unsafe { + crate::unimplemented_or_uninitialized_generic!(Dynamic::new(0), Dynamic::new(0)) + }; let old = mem::replace(self, placeholder); let new = old.resize(new_nrows, new_ncols, val); let _ = mem::replace(self, new); @@ -918,7 +923,9 @@ where where DefaultAllocator: Reallocator, { - let placeholder = unsafe { crate::unimplemented_or_uninitialized_generic!(Dynamic::new(0), self.data.shape().1) }; + let placeholder = unsafe { + crate::unimplemented_or_uninitialized_generic!(Dynamic::new(0), self.data.shape().1) + }; let old = mem::replace(self, placeholder); let new = old.resize_vertically(new_nrows, val); let _ = mem::replace(self, new); @@ -941,7 +948,9 @@ where where DefaultAllocator: Reallocator, { - let placeholder = unsafe { crate::unimplemented_or_uninitialized_generic!(self.data.shape().0, Dynamic::new(0)) }; + let placeholder = unsafe { + crate::unimplemented_or_uninitialized_generic!(self.data.shape().0, Dynamic::new(0)) + }; let old = mem::replace(self, placeholder); let new = old.resize_horizontally(new_ncols, val); let _ = mem::replace(self, new); diff --git a/src/base/matrix.rs b/src/base/matrix.rs index b5fc169e..889c1ecd 100644 --- a/src/base/matrix.rs +++ b/src/base/matrix.rs @@ -300,8 +300,12 @@ impl> Matrix { /// Creates a new uninitialized matrix with the given uninitialized data pub unsafe fn from_uninitialized_data(data: mem::MaybeUninit) -> mem::MaybeUninit { - let res: Matrix> = Matrix { data, _phantoms: PhantomData }; - let res: mem::MaybeUninit>> = mem::MaybeUninit::new(res); + let res: Matrix> = Matrix { + data, + _phantoms: PhantomData, + }; + let res: mem::MaybeUninit>> = + mem::MaybeUninit::new(res); // safety: since we wrap the inner MaybeUninit in an outer MaybeUninit above, the fact that the `data` field is partially-uninitialized is still opaque. // with s/transmute_copy/transmute/, rustc claims that `MaybeUninit>>` may be of a different size from `MaybeUninit>` // but MaybeUninit's documentation says "MaybeUninit is guaranteed to have the same size, alignment, and ABI as T", which implies those types should be the same size @@ -507,7 +511,8 @@ impl> Matrix { let nrows: SameShapeR = Dim::from_usize(nrows); let ncols: SameShapeC = Dim::from_usize(ncols); - let mut res: MatrixSum = unsafe { crate::unimplemented_or_uninitialized_generic!(nrows, ncols) }; + let mut res: MatrixSum = + unsafe { crate::unimplemented_or_uninitialized_generic!(nrows, ncols) }; // TODO: use copy_from for j in 0..res.ncols() { @@ -574,7 +579,8 @@ impl> Matrix { { let (nrows, ncols) = self.data.shape(); - let mut res: MatrixMN = unsafe { crate::unimplemented_or_uninitialized_generic!(nrows, ncols) }; + let mut res: MatrixMN = + unsafe { crate::unimplemented_or_uninitialized_generic!(nrows, ncols) }; for j in 0..ncols.value() { for i in 0..nrows.value() { @@ -618,7 +624,8 @@ impl> Matrix { { let (nrows, ncols) = self.data.shape(); - let mut res: MatrixMN = unsafe { crate::unimplemented_or_uninitialized_generic!(nrows, ncols) }; + let mut res: MatrixMN = + unsafe { crate::unimplemented_or_uninitialized_generic!(nrows, ncols) }; for j in 0..ncols.value() { for i in 0..nrows.value() { @@ -645,7 +652,8 @@ impl> Matrix { { let (nrows, ncols) = self.data.shape(); - let mut res: MatrixMN = unsafe { crate::unimplemented_or_uninitialized_generic!(nrows, ncols) }; + let mut res: MatrixMN = + unsafe { crate::unimplemented_or_uninitialized_generic!(nrows, ncols) }; assert_eq!( (nrows.value(), ncols.value()), @@ -686,7 +694,8 @@ impl> Matrix { { let (nrows, ncols) = self.data.shape(); - let mut res: MatrixMN = unsafe { crate::unimplemented_or_uninitialized_generic!(nrows, ncols) }; + let mut res: MatrixMN = + unsafe { crate::unimplemented_or_uninitialized_generic!(nrows, ncols) }; assert_eq!( (nrows.value(), ncols.value()), @@ -1180,7 +1189,8 @@ impl> Matrix = crate::unimplemented_or_uninitialized_generic!(ncols, nrows); + let mut res: MatrixMN<_, C, R> = + crate::unimplemented_or_uninitialized_generic!(ncols, nrows); self.adjoint_to(&mut res); res @@ -1321,7 +1331,8 @@ impl> SquareMatrix { ); let dim = self.data.shape().0; - let mut res: VectorN = unsafe { crate::unimplemented_or_uninitialized_generic!(dim, U1) }; + let mut res: VectorN = + unsafe { crate::unimplemented_or_uninitialized_generic!(dim, U1) }; for i in 0..dim.value() { unsafe { @@ -1448,7 +1459,8 @@ impl, S: Storage> Vector { { let len = self.len(); let hnrows = DimSum::::from_usize(len + 1); - let mut res: VectorN:: = unsafe { crate::unimplemented_or_uninitialized_generic!(hnrows, U1) }; + let mut res: VectorN = + unsafe { crate::unimplemented_or_uninitialized_generic!(hnrows, U1) }; res.generic_slice_mut((0, 0), self.data.shape()) .copy_from(self); res[(len, 0)] = element; @@ -1793,7 +1805,8 @@ impl::from_usize(3); let ncols = SameShapeC::::from_usize(1); - let mut res: MatrixCross = crate::unimplemented_or_uninitialized_generic!(nrows, ncols); + let mut res: MatrixCross = + crate::unimplemented_or_uninitialized_generic!(nrows, ncols); let ax = self.get_unchecked((0, 0)); let ay = self.get_unchecked((1, 0)); @@ -1817,7 +1830,8 @@ impl::from_usize(1); let ncols = SameShapeC::::from_usize(3); - let mut res: MatrixCross = crate::unimplemented_or_uninitialized_generic!(nrows, ncols); + let mut res: MatrixCross = + crate::unimplemented_or_uninitialized_generic!(nrows, ncols); let ax = self.get_unchecked((0, 0)); let ay = self.get_unchecked((0, 1)); diff --git a/src/base/mod.rs b/src/base/mod.rs index abe2fd48..9f08572f 100644 --- a/src/base/mod.rs +++ b/src/base/mod.rs @@ -15,7 +15,8 @@ mod alias_slice; mod array_storage; mod cg; mod componentwise; -#[macro_use] mod construction; +#[macro_use] +mod construction; mod construction_slice; mod conversion; mod edition; diff --git a/src/base/ops.rs b/src/base/ops.rs index 93ca3b8e..73f18a8c 100644 --- a/src/base/ops.rs +++ b/src/base/ops.rs @@ -573,7 +573,9 @@ where #[inline] fn mul(self, rhs: &'b Matrix) -> Self::Output { - let mut res = unsafe { crate::unimplemented_or_uninitialized_generic!(self.data.shape().0, rhs.data.shape().1) }; + let mut res = unsafe { + crate::unimplemented_or_uninitialized_generic!(self.data.shape().0, rhs.data.shape().1) + }; self.mul_to(rhs, &mut res); res } @@ -682,7 +684,9 @@ where DefaultAllocator: Allocator, ShapeConstraint: SameNumberOfRows, { - let mut res = unsafe { crate::unimplemented_or_uninitialized_generic!(self.data.shape().1, rhs.data.shape().1) }; + let mut res = unsafe { + crate::unimplemented_or_uninitialized_generic!(self.data.shape().1, rhs.data.shape().1) + }; self.tr_mul_to(rhs, &mut res); res @@ -697,7 +701,9 @@ where DefaultAllocator: Allocator, ShapeConstraint: SameNumberOfRows, { - let mut res = unsafe { crate::unimplemented_or_uninitialized_generic!(self.data.shape().1, rhs.data.shape().1) }; + let mut res = unsafe { + crate::unimplemented_or_uninitialized_generic!(self.data.shape().1, rhs.data.shape().1) + }; self.ad_mul_to(rhs, &mut res); res @@ -811,7 +817,9 @@ where let (nrows1, ncols1) = self.data.shape(); let (nrows2, ncols2) = rhs.data.shape(); - let mut res = unsafe { crate::unimplemented_or_uninitialized_generic!(nrows1.mul(nrows2), ncols1.mul(ncols2)) }; + let mut res = unsafe { + crate::unimplemented_or_uninitialized_generic!(nrows1.mul(nrows2), ncols1.mul(ncols2)) + }; { let mut data_res = res.data.ptr_mut(); diff --git a/src/base/statistics.rs b/src/base/statistics.rs index 7ee5a9c9..811b508f 100644 --- a/src/base/statistics.rs +++ b/src/base/statistics.rs @@ -17,7 +17,8 @@ impl> Matrix { DefaultAllocator: Allocator, { let ncols = self.data.shape().1; - let mut res: RowVectorN = unsafe { crate::unimplemented_or_uninitialized_generic!(U1, ncols) }; + let mut res: RowVectorN = + unsafe { crate::unimplemented_or_uninitialized_generic!(U1, ncols) }; for i in 0..ncols.value() { // TODO: avoid bound checking of column. @@ -42,7 +43,8 @@ impl> Matrix { DefaultAllocator: Allocator, { let ncols = self.data.shape().1; - let mut res: VectorN = unsafe { crate::unimplemented_or_uninitialized_generic!(ncols, U1) }; + let mut res: VectorN = + unsafe { crate::unimplemented_or_uninitialized_generic!(ncols, U1) }; for i in 0..ncols.value() { // TODO: avoid bound checking of column. diff --git a/src/geometry/point.rs b/src/geometry/point.rs index d668e804..f5c8d31f 100644 --- a/src/geometry/point.rs +++ b/src/geometry/point.rs @@ -181,7 +181,12 @@ where D: DimNameAdd, DefaultAllocator: Allocator>, { - let mut res = unsafe { crate::unimplemented_or_uninitialized_generic!( as DimName>::name(), U1) }; + let mut res = unsafe { + crate::unimplemented_or_uninitialized_generic!( + as DimName>::name(), + U1 + ) + }; res.fixed_slice_mut::(0, 0).copy_from(&self.coords); res[(D::dim(), 0)] = N::one(); diff --git a/src/geometry/point_construction.rs b/src/geometry/point_construction.rs index 48c4a71c..c21680a9 100644 --- a/src/geometry/point_construction.rs +++ b/src/geometry/point_construction.rs @@ -24,7 +24,10 @@ where /// Creates a new point with uninitialized coordinates. #[inline] pub unsafe fn new_uninitialized() -> Self { - Self::from(crate::unimplemented_or_uninitialized_generic!(D::name(), U1)) + Self::from(crate::unimplemented_or_uninitialized_generic!( + D::name(), + U1 + )) } /// Creates a new point with all coordinates equal to zero. diff --git a/src/linalg/bidiagonal.rs b/src/linalg/bidiagonal.rs index 0202eb68..33fc81e6 100644 --- a/src/linalg/bidiagonal.rs +++ b/src/linalg/bidiagonal.rs @@ -81,7 +81,8 @@ where "Cannot compute the bidiagonalization of an empty matrix." ); - let mut diagonal = unsafe { crate::unimplemented_or_uninitialized_generic!(min_nrows_ncols, U1) }; + let mut diagonal = + unsafe { crate::unimplemented_or_uninitialized_generic!(min_nrows_ncols, U1) }; let mut off_diagonal = unsafe { crate::unimplemented_or_uninitialized_generic!(min_nrows_ncols.sub(U1), U1) }; let mut axis_packed = unsafe { crate::unimplemented_or_uninitialized_generic!(ncols, U1) }; @@ -239,7 +240,8 @@ where let min_nrows_ncols = nrows.min(ncols); let mut res = Matrix::identity_generic(min_nrows_ncols, ncols); - let mut work = unsafe { crate::unimplemented_or_uninitialized_generic!(min_nrows_ncols, U1) }; + let mut work = + unsafe { crate::unimplemented_or_uninitialized_generic!(min_nrows_ncols, U1) }; let mut axis_packed = unsafe { crate::unimplemented_or_uninitialized_generic!(ncols, U1) }; let shift = self.axis_shift().1; diff --git a/src/linalg/hessenberg.rs b/src/linalg/hessenberg.rs index c6103ffd..ac3e82b8 100644 --- a/src/linalg/hessenberg.rs +++ b/src/linalg/hessenberg.rs @@ -48,7 +48,8 @@ where { /// Computes the Hessenberg decomposition using householder reflections. pub fn new(hess: MatrixN) -> Self { - let mut work = unsafe { crate::unimplemented_or_uninitialized_generic!(hess.data.shape().0, U1) }; + let mut work = + unsafe { crate::unimplemented_or_uninitialized_generic!(hess.data.shape().0, U1) }; Self::new_with_workspace(hess, &mut work) } @@ -74,7 +75,8 @@ where "Hessenberg: invalid workspace size." ); - let mut subdiag = unsafe { crate::unimplemented_or_uninitialized_generic!(dim.sub(U1), U1) }; + let mut subdiag = + unsafe { crate::unimplemented_or_uninitialized_generic!(dim.sub(U1), U1) }; if dim.value() == 0 { return Hessenberg { hess, subdiag }; diff --git a/src/linalg/qr.rs b/src/linalg/qr.rs index 063cad0d..b9cec96a 100644 --- a/src/linalg/qr.rs +++ b/src/linalg/qr.rs @@ -54,7 +54,8 @@ where let (nrows, ncols) = matrix.data.shape(); let min_nrows_ncols = nrows.min(ncols); - let mut diag = unsafe { crate::unimplemented_or_uninitialized_generic!(min_nrows_ncols, U1) }; + let mut diag = + unsafe { crate::unimplemented_or_uninitialized_generic!(min_nrows_ncols, U1) }; if min_nrows_ncols.value() == 0 { return QR { qr: matrix, diag }; diff --git a/src/linalg/schur.rs b/src/linalg/schur.rs index 820c0e21..4b89567b 100644 --- a/src/linalg/schur.rs +++ b/src/linalg/schur.rs @@ -71,7 +71,8 @@ where /// number of iteration is exceeded, `None` is returned. If `niter == 0`, then the algorithm /// continues indefinitely until convergence. pub fn try_new(m: MatrixN, eps: N::RealField, max_niter: usize) -> Option { - let mut work = unsafe { crate::unimplemented_or_uninitialized_generic!(m.data.shape().0, U1) }; + let mut work = + unsafe { crate::unimplemented_or_uninitialized_generic!(m.data.shape().0, U1) }; Self::do_decompose(m, &mut work, eps, max_niter, true) .map(|(q, t)| Schur { q: q.unwrap(), t }) @@ -378,7 +379,8 @@ where /// /// Return `None` if some eigenvalues are complex. pub fn eigenvalues(&self) -> Option> { - let mut out = unsafe { crate::unimplemented_or_uninitialized_generic!(self.t.data.shape().0, U1) }; + let mut out = + unsafe { crate::unimplemented_or_uninitialized_generic!(self.t.data.shape().0, U1) }; if Self::do_eigenvalues(&self.t, &mut out) { Some(out) } else { @@ -392,7 +394,8 @@ where N: RealField, DefaultAllocator: Allocator, D>, { - let mut out = unsafe { crate::unimplemented_or_uninitialized_generic!(self.t.data.shape().0, U1) }; + let mut out = + unsafe { crate::unimplemented_or_uninitialized_generic!(self.t.data.shape().0, U1) }; Self::do_complex_eigenvalues(&self.t, &mut out); out } @@ -503,7 +506,8 @@ where "Unable to compute eigenvalues of a non-square matrix." ); - let mut work = unsafe { crate::unimplemented_or_uninitialized_generic!(self.data.shape().0, U1) }; + let mut work = + unsafe { crate::unimplemented_or_uninitialized_generic!(self.data.shape().0, U1) }; // Special case for 2x2 matrices. if self.nrows() == 2 { diff --git a/src/linalg/symmetric_tridiagonal.rs b/src/linalg/symmetric_tridiagonal.rs index ba47724e..c05b5558 100644 --- a/src/linalg/symmetric_tridiagonal.rs +++ b/src/linalg/symmetric_tridiagonal.rs @@ -61,7 +61,8 @@ where "Unable to compute the symmetric tridiagonal decomposition of an empty matrix." ); - let mut off_diagonal = unsafe { crate::unimplemented_or_uninitialized_generic!(dim.sub(U1), U1) }; + let mut off_diagonal = + unsafe { crate::unimplemented_or_uninitialized_generic!(dim.sub(U1), U1) }; let mut p = unsafe { crate::unimplemented_or_uninitialized_generic!(dim.sub(U1), U1) }; for i in 0..dim.value() - 1 { From 234e103e4be23dc0a76ceda87e6f420680ec6c98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Thu, 25 Feb 2021 13:52:58 +0100 Subject: [PATCH 143/183] Fix compilation of the sparse module. --- src/sparse/cs_matrix.rs | 2 +- src/sparse/cs_matrix_cholesky.rs | 9 ++++++--- src/sparse/cs_matrix_ops.rs | 2 +- src/sparse/cs_matrix_solve.rs | 3 ++- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/sparse/cs_matrix.rs b/src/sparse/cs_matrix.rs index 45a2bbf7..3b056ab7 100644 --- a/src/sparse/cs_matrix.rs +++ b/src/sparse/cs_matrix.rs @@ -460,7 +460,7 @@ where { // Size = R let nrows = self.data.shape().0; - let mut workspace = unsafe { VectorN::new_uninitialized_generic(nrows, U1) }; + let mut workspace = unsafe { crate::unimplemented_or_uninitialized_generic!(nrows, U1) }; self.sort_with_workspace(workspace.as_mut_slice()); } diff --git a/src/sparse/cs_matrix_cholesky.rs b/src/sparse/cs_matrix_cholesky.rs index 277f9316..1a0c15dc 100644 --- a/src/sparse/cs_matrix_cholesky.rs +++ b/src/sparse/cs_matrix_cholesky.rs @@ -48,8 +48,10 @@ where let (l, u) = Self::nonzero_pattern(m); // Workspaces. - let work_x = unsafe { VectorN::new_uninitialized_generic(m.data.shape().0, U1) }; - let work_c = unsafe { VectorN::new_uninitialized_generic(m.data.shape().1, U1) }; + let work_x = + unsafe { crate::unimplemented_or_uninitialized_generic!(m.data.shape().0, U1) }; + let work_c = + unsafe { crate::unimplemented_or_uninitialized_generic!(m.data.shape().1, U1) }; let mut original_p = m.data.p.as_slice().to_vec(); original_p.push(m.data.i.len()); @@ -291,7 +293,8 @@ where let etree = Self::elimination_tree(m); let (nrows, ncols) = m.data.shape(); let mut rows = Vec::with_capacity(m.len()); - let mut cols = unsafe { VectorN::new_uninitialized_generic(m.data.shape().0, U1) }; + let mut cols = + unsafe { crate::unimplemented_or_uninitialized_generic!(m.data.shape().0, U1) }; let mut marks = Vec::new(); // NOTE: the following will actually compute the non-zero pattern of diff --git a/src/sparse/cs_matrix_ops.rs b/src/sparse/cs_matrix_ops.rs index 803bc61f..a440882c 100644 --- a/src/sparse/cs_matrix_ops.rs +++ b/src/sparse/cs_matrix_ops.rs @@ -242,7 +242,7 @@ where let mut res = CsMatrix::new_uninitialized_generic(nrows1, ncols2, self.len() + rhs.len()); let mut timestamps = VectorN::zeros_generic(nrows1, U1); - let mut workspace = unsafe { VectorN::new_uninitialized_generic(nrows1, U1) }; + let mut workspace = unsafe { crate::unimplemented_or_uninitialized_generic!(nrows1, U1) }; let mut nz = 0; for j in 0..ncols2.value() { diff --git a/src/sparse/cs_matrix_solve.rs b/src/sparse/cs_matrix_solve.rs index 73b50db3..bc7c5bc7 100644 --- a/src/sparse/cs_matrix_solve.rs +++ b/src/sparse/cs_matrix_solve.rs @@ -149,7 +149,8 @@ impl> CsMatrix { self.lower_triangular_reach(b, &mut reach); // We sort the reach so the result matrix has sorted indices. reach.sort(); - let mut workspace = unsafe { VectorN::new_uninitialized_generic(b.data.shape().0, U1) }; + let mut workspace = + unsafe { crate::unimplemented_or_uninitialized_generic!(b.data.shape().0, U1) }; for i in reach.iter().cloned() { workspace[i] = N::zero(); From dc15261ec15da0797dad8d3687b247db2c26ca4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Thu, 25 Feb 2021 14:10:34 +0100 Subject: [PATCH 144/183] Move the bytemuck impls to GenericArray and add a transitive impl for matrices. --- src/base/array_storage.rs | 20 ++++++++++++++++++++ src/base/matrix.rs | 15 ++++++--------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/base/array_storage.rs b/src/base/array_storage.rs index 4fdd3e73..e067cb49 100644 --- a/src/base/array_storage.rs +++ b/src/base/array_storage.rs @@ -394,6 +394,26 @@ where } } +#[cfg(feature = "bytemuck")] +unsafe impl bytemuck::Zeroable + for ArrayStorage +where + R::Value: Mul, + Prod: ArrayLength, + Self: Copy, +{ +} + +#[cfg(feature = "bytemuck")] +unsafe impl bytemuck::Pod + for ArrayStorage +where + R::Value: Mul, + Prod: ArrayLength, + Self: Copy, +{ +} + #[cfg(feature = "abomonation-serialize")] impl Abomonation for ArrayStorage where diff --git a/src/base/matrix.rs b/src/base/matrix.rs index 73001536..eb525b14 100644 --- a/src/base/matrix.rs +++ b/src/base/matrix.rs @@ -280,21 +280,18 @@ impl> matrixcompare_core::DenseAc } #[cfg(feature = "bytemuck")] -unsafe impl - bytemuck::Zeroable for Matrix> +unsafe impl> bytemuck::Zeroable + for Matrix where - R::Value: core::ops::Mul, - >::Output: generic_array::ArrayLength, + S: bytemuck::Zeroable, { } #[cfg(feature = "bytemuck")] -unsafe impl - bytemuck::Pod for Matrix> +unsafe impl> bytemuck::Pod for Matrix where - R::Value: core::ops::Mul, - >::Output: generic_array::ArrayLength, - <::Value>>::Output as generic_array::ArrayLength>::ArrayType: Copy + S: bytemuck::Pod, + Self: Copy, { } From 0b1e6f0b05f862efb225ad4ab317348ecedbdf09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Thu, 25 Feb 2021 14:16:57 +0100 Subject: [PATCH 145/183] Run cargo fmt. --- src/geometry/quaternion.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/geometry/quaternion.rs b/src/geometry/quaternion.rs index 78b95332..ebed6779 100755 --- a/src/geometry/quaternion.rs +++ b/src/geometry/quaternion.rs @@ -44,7 +44,12 @@ impl Default for Quaternion { unsafe impl bytemuck::Zeroable for Quaternion where Vector4: bytemuck::Zeroable {} #[cfg(feature = "bytemuck")] -unsafe impl bytemuck::Pod for Quaternion where Vector4: bytemuck::Pod, N: Copy {} +unsafe impl bytemuck::Pod for Quaternion +where + Vector4: bytemuck::Pod, + N: Copy, +{ +} #[cfg(feature = "abomonation-serialize")] impl Abomonation for Quaternion From af448d2c707dd9b6d63a157131725e135ff19cf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Thu, 25 Feb 2021 14:19:20 +0100 Subject: [PATCH 146/183] Add bytemuck impls to points. --- src/geometry/point.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/geometry/point.rs b/src/geometry/point.rs index 75410ccd..390db80b 100644 --- a/src/geometry/point.rs +++ b/src/geometry/point.rs @@ -65,6 +65,24 @@ where { } +#[cfg(feature = "bytemuck")] +unsafe impl bytemuck::Zeroable for Point +where + VectorN: bytemuck::Zeroable, + DefaultAllocator: Allocator, +{ +} + +#[cfg(feature = "bytemuck")] +unsafe impl bytemuck::Pod for Point +where + N: Copy, + VectorN: bytemuck::Pod, + DefaultAllocator: Allocator, + >::Buffer: Copy, +{ +} + #[cfg(feature = "serde-serialize")] impl Serialize for Point where From 162a7ef09c87441bcd152383aa36c72fcda2034a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Thu, 25 Feb 2021 14:30:04 +0100 Subject: [PATCH 147/183] Fix compilation when the mint or alga features are enabled. --- src/base/conversion.rs | 6 +++--- src/base/matrix_alga.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/base/conversion.rs b/src/base/conversion.rs index b852ff59..8ef1a967 100644 --- a/src/base/conversion.rs +++ b/src/base/conversion.rs @@ -245,9 +245,9 @@ macro_rules! impl_from_into_mint_1D( fn from(v: mint::$VT) -> Self { unsafe { let mut res = Self::new_uninitialized(); - ptr::copy_nonoverlapping(&v.x, res.data.ptr_mut(), $SZ); + ptr::copy_nonoverlapping(&v.x, (*res.as_mut_ptr()).data.ptr_mut(), $SZ); - res + res.assume_init() } } } @@ -307,7 +307,7 @@ macro_rules! impl_from_into_mint_2D( fn from(m: mint::$MV) -> Self { unsafe { let mut res = Self::new_uninitialized(); - let mut ptr = (*res).data.ptr_mut(); + let mut ptr = (*res.as_mut_ptr()).data.ptr_mut(); $( ptr::copy_nonoverlapping(&m.$component.x, ptr, $SZRows); ptr = ptr.offset($SZRows); diff --git a/src/base/matrix_alga.rs b/src/base/matrix_alga.rs index c8c08e64..6e97aedb 100644 --- a/src/base/matrix_alga.rs +++ b/src/base/matrix_alga.rs @@ -433,8 +433,8 @@ where "Matrix meet/join error: mismatched dimensions." ); - let mut mres = unsafe { Self::new_uninitialized_generic(shape.0, shape.1) }; - let mut jres = unsafe { Self::new_uninitialized_generic(shape.0, shape.1) }; + let mut mres = unsafe { crate::unimplemented_or_uninitialized_generic!(shape.0, shape.1) }; + let mut jres = unsafe { crate::unimplemented_or_uninitialized_generic!(shape.0, shape.1) }; for i in 0..shape.0.value() * shape.1.value() { unsafe { From bf0f3163cecad4befcce08ea5e6e2e97e940111d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Thu, 25 Feb 2021 14:45:26 +0100 Subject: [PATCH 148/183] Rename some of the variables in dual-quaternion doc-tests. --- src/geometry/dual_quaternion.rs | 26 +++++++++++++------------- src/geometry/dual_quaternion_ops.rs | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/geometry/dual_quaternion.rs b/src/geometry/dual_quaternion.rs index dac11725..cce0da2a 100644 --- a/src/geometry/dual_quaternion.rs +++ b/src/geometry/dual_quaternion.rs @@ -316,7 +316,7 @@ impl> UlpsEq for DualQuaternion { } } -/// A unit quaternions. May be used to represent a rotation. +/// A unit quaternions. May be used to represent a rotation followed by a translation. pub type UnitDualQuaternion = Unit>; impl PartialEq for UnitDualQuaternion { @@ -471,10 +471,10 @@ where /// # use nalgebra::{UnitDualQuaternion, DualQuaternion, Quaternion}; /// let qr = Quaternion::new(1.0, 2.0, 3.0, 4.0); /// let qd = Quaternion::new(5.0, 6.0, 7.0, 8.0); - /// let iso1 = UnitDualQuaternion::new_normalize(DualQuaternion::from_real_and_dual(qr, qd)); - /// let iso2 = UnitDualQuaternion::new_normalize(DualQuaternion::from_real_and_dual(qd, qr)); - /// let iso_to = iso1.isometry_to(&iso2); - /// assert_relative_eq!(iso_to * iso1, iso2, epsilon = 1.0e-6); + /// let dq1 = UnitDualQuaternion::new_normalize(DualQuaternion::from_real_and_dual(qr, qd)); + /// let dq2 = UnitDualQuaternion::new_normalize(DualQuaternion::from_real_and_dual(qd, qr)); + /// let dq_to = dq1.isometry_to(&dq2); + /// assert_relative_eq!(dq_to * dq1, dq2, epsilon = 1.0e-6); /// ``` #[inline] pub fn isometry_to(&self, other: &Self) -> Self { @@ -545,7 +545,7 @@ where } /// Screw linear interpolation between two unit quaternions. This creates a - /// smooth arc from one isometry to another. + /// smooth arc from one dual-quaternion to another. /// /// Panics if the angle between both quaternion is 180 degrees (in which case the interpolation /// is not well-defined). Use `.try_sclerp` instead to avoid the panic. @@ -754,9 +754,9 @@ where } /// Rotate and translate a point by the inverse of this unit quaternion. - /// This may be - /// cheaper than inverting the unit dual quaternion and transforming the - /// point. + /// + /// This may be cheaper than inverting the unit dual quaternion and + /// transforming the point. /// /// ``` /// # #[macro_use] extern crate approx; @@ -777,10 +777,10 @@ where } /// Rotate a vector by the inverse of this unit quaternion, ignoring the - /// translational component - /// This may be - /// cheaper than inverting the unit dual quaternion and transforming the - /// vector. + /// translational component. + /// + /// This may be cheaper than inverting the unit dual quaternion and + /// transforming the vector. /// /// ``` /// # #[macro_use] extern crate approx; diff --git a/src/geometry/dual_quaternion_ops.rs b/src/geometry/dual_quaternion_ops.rs index a9e83764..44d36b97 100644 --- a/src/geometry/dual_quaternion_ops.rs +++ b/src/geometry/dual_quaternion_ops.rs @@ -796,7 +796,7 @@ dual_quaternion_op_impl!( (U3, U1), (U4, U1); self: &'a Isometry3, rhs: &'b UnitDualQuaternion, Output = UnitDualQuaternion => U3, U1; - // TODO: can we avoid the conversion from a rotation matrix? + // TODO: can we avoid the conversion from a rotation matrix? UnitDualQuaternion::::from_isometry(self) / rhs; 'a, 'b); From a32f41bd410a214a022d76adc60d09f67e52ad75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Thu, 25 Feb 2021 15:01:53 +0100 Subject: [PATCH 149/183] Fix compilation when targetting no-std. --- src/geometry/dual_quaternion.rs | 15 ++++++++++++--- src/geometry/dual_quaternion_construction.rs | 9 ++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/geometry/dual_quaternion.rs b/src/geometry/dual_quaternion.rs index cce0da2a..b11e7364 100644 --- a/src/geometry/dual_quaternion.rs +++ b/src/geometry/dual_quaternion.rs @@ -1,6 +1,6 @@ use crate::{ Isometry3, Matrix4, Normed, Point3, Quaternion, Scalar, SimdRealField, Translation3, Unit, - UnitQuaternion, Vector3, VectorN, U8, + UnitQuaternion, Vector3, VectorN, Zero, U8, }; use approx::{AbsDiffEq, RelativeEq, UlpsEq}; #[cfg(feature = "serde-serialize")] @@ -35,14 +35,23 @@ use simba::scalar::{ClosedNeg, RealField}; /// If a feature that you need is missing, feel free to open an issue or a PR. /// See https://github.com/dimforge/nalgebra/issues/487 #[repr(C)] -#[derive(Debug, Default, Eq, PartialEq, Copy, Clone)] -pub struct DualQuaternion { +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub struct DualQuaternion { /// The real component of the quaternion pub real: Quaternion, /// The dual component of the quaternion pub dual: Quaternion, } +impl Default for DualQuaternion { + fn default() -> Self { + Self { + real: Quaternion::default(), + dual: Quaternion::default(), + } + } +} + impl DualQuaternion where N::Element: SimdRealField, diff --git a/src/geometry/dual_quaternion_construction.rs b/src/geometry/dual_quaternion_construction.rs index 95c208ce..b0ae8036 100644 --- a/src/geometry/dual_quaternion_construction.rs +++ b/src/geometry/dual_quaternion_construction.rs @@ -1,12 +1,12 @@ use crate::{ - DualQuaternion, Isometry3, Quaternion, SimdRealField, Translation3, UnitDualQuaternion, + DualQuaternion, Isometry3, Quaternion, Scalar, SimdRealField, Translation3, UnitDualQuaternion, UnitQuaternion, }; use num::{One, Zero}; #[cfg(feature = "arbitrary")] use quickcheck::{Arbitrary, Gen}; -impl DualQuaternion { +impl DualQuaternion { /// Creates a dual quaternion from its rotation and translation components. /// /// # Example @@ -40,7 +40,10 @@ impl DualQuaternion { /// assert_eq!(dq2 * dq1, dq2); /// ``` #[inline] - pub fn identity() -> Self { + pub fn identity() -> Self + where + N: SimdRealField, + { Self::from_real_and_dual( Quaternion::from_real(N::one()), Quaternion::from_real(N::zero()), From 52254568836cdcef0eba072c60f8ac2be11f145b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Thu, 25 Feb 2021 15:07:15 +0100 Subject: [PATCH 150/183] Fix nalgebra-lapack. Since nalgebra-lapack can only be used with f32 and f64, it is OK to just call `.assume_init()`. --- nalgebra-lapack/src/eigen.rs | 22 +++++++++++++--------- nalgebra-lapack/src/hessenberg.rs | 2 +- nalgebra-lapack/src/qr.rs | 3 ++- nalgebra-lapack/src/schur.rs | 9 +++++---- nalgebra-lapack/src/svd.rs | 6 +++--- nalgebra-lapack/src/symmetric_eigen.rs | 2 +- 6 files changed, 25 insertions(+), 19 deletions(-) diff --git a/nalgebra-lapack/src/eigen.rs b/nalgebra-lapack/src/eigen.rs index 1ccd6e3f..1aae842c 100644 --- a/nalgebra-lapack/src/eigen.rs +++ b/nalgebra-lapack/src/eigen.rs @@ -78,9 +78,9 @@ where let lda = n as i32; - let mut wr = unsafe { Matrix::new_uninitialized_generic(nrows, U1) }; + let mut wr = unsafe { Matrix::new_uninitialized_generic(nrows, U1).assume_init() }; // TODO: Tap into the workspace. - let mut wi = unsafe { Matrix::new_uninitialized_generic(nrows, U1) }; + let mut wi = unsafe { Matrix::new_uninitialized_generic(nrows, U1).assume_init() }; let mut info = 0; let mut placeholder1 = [N::zero()]; @@ -107,8 +107,10 @@ where match (left_eigenvectors, eigenvectors) { (true, true) => { - let mut vl = unsafe { Matrix::new_uninitialized_generic(nrows, ncols) }; - let mut vr = unsafe { Matrix::new_uninitialized_generic(nrows, ncols) }; + let mut vl = + unsafe { Matrix::new_uninitialized_generic(nrows, ncols).assume_init() }; + let mut vr = + unsafe { Matrix::new_uninitialized_generic(nrows, ncols).assume_init() }; N::xgeev( ljob, @@ -137,7 +139,8 @@ where } } (true, false) => { - let mut vl = unsafe { Matrix::new_uninitialized_generic(nrows, ncols) }; + let mut vl = + unsafe { Matrix::new_uninitialized_generic(nrows, ncols).assume_init() }; N::xgeev( ljob, @@ -166,7 +169,8 @@ where } } (false, true) => { - let mut vr = unsafe { Matrix::new_uninitialized_generic(nrows, ncols) }; + let mut vr = + unsafe { Matrix::new_uninitialized_generic(nrows, ncols).assume_init() }; N::xgeev( ljob, @@ -243,8 +247,8 @@ where let lda = n as i32; - let mut wr = unsafe { Matrix::new_uninitialized_generic(nrows, U1) }; - let mut wi = unsafe { Matrix::new_uninitialized_generic(nrows, U1) }; + let mut wr = unsafe { Matrix::new_uninitialized_generic(nrows, U1).assume_init() }; + let mut wi = unsafe { Matrix::new_uninitialized_generic(nrows, U1).assume_init() }; let mut info = 0; let mut placeholder1 = [N::zero()]; @@ -287,7 +291,7 @@ where ); lapack_panic!(info); - let mut res = unsafe { Matrix::new_uninitialized_generic(nrows, U1) }; + let mut res = unsafe { Matrix::new_uninitialized_generic(nrows, U1).assume_init() }; for i in 0..res.len() { res[i] = Complex::new(wr[i], wi[i]); diff --git a/nalgebra-lapack/src/hessenberg.rs b/nalgebra-lapack/src/hessenberg.rs index b20df55e..ed456ecb 100644 --- a/nalgebra-lapack/src/hessenberg.rs +++ b/nalgebra-lapack/src/hessenberg.rs @@ -60,7 +60,7 @@ where "Unable to compute the hessenberg decomposition of an empty matrix." ); - let mut tau = unsafe { Matrix::new_uninitialized_generic(nrows.sub(U1), U1) }; + let mut tau = unsafe { Matrix::new_uninitialized_generic(nrows.sub(U1), U1).assume_init() }; let mut info = 0; let lwork = diff --git a/nalgebra-lapack/src/qr.rs b/nalgebra-lapack/src/qr.rs index ac8ad672..c9216135 100644 --- a/nalgebra-lapack/src/qr.rs +++ b/nalgebra-lapack/src/qr.rs @@ -57,7 +57,8 @@ where let (nrows, ncols) = m.data.shape(); let mut info = 0; - let mut tau = unsafe { Matrix::new_uninitialized_generic(nrows.min(ncols), U1) }; + let mut tau = + unsafe { Matrix::new_uninitialized_generic(nrows.min(ncols), U1).assume_init() }; if nrows.value() == 0 || ncols.value() == 0 { return Self { qr: m, tau: tau }; diff --git a/nalgebra-lapack/src/schur.rs b/nalgebra-lapack/src/schur.rs index 5079efbf..0480f73f 100644 --- a/nalgebra-lapack/src/schur.rs +++ b/nalgebra-lapack/src/schur.rs @@ -78,9 +78,9 @@ where let mut info = 0; - let mut wr = unsafe { Matrix::new_uninitialized_generic(nrows, U1) }; - let mut wi = unsafe { Matrix::new_uninitialized_generic(nrows, U1) }; - let mut q = unsafe { Matrix::new_uninitialized_generic(nrows, ncols) }; + let mut wr = unsafe { Matrix::new_uninitialized_generic(nrows, U1).assume_init() }; + let mut wi = unsafe { Matrix::new_uninitialized_generic(nrows, U1).assume_init() }; + let mut q = unsafe { Matrix::new_uninitialized_generic(nrows, ncols).assume_init() }; // Placeholders: let mut bwork = [0i32]; let mut unused = 0; @@ -151,7 +151,8 @@ where where DefaultAllocator: Allocator, D>, { - let mut out = unsafe { VectorN::new_uninitialized_generic(self.t.data.shape().0, U1) }; + let mut out = + unsafe { VectorN::new_uninitialized_generic(self.t.data.shape().0, U1).assume_init() }; for i in 0..out.len() { out[i] = Complex::new(self.re[i], self.im[i]) diff --git a/nalgebra-lapack/src/svd.rs b/nalgebra-lapack/src/svd.rs index 18b4957f..70c7fd18 100644 --- a/nalgebra-lapack/src/svd.rs +++ b/nalgebra-lapack/src/svd.rs @@ -99,9 +99,9 @@ macro_rules! svd_impl( let lda = nrows.value() as i32; - let mut u = unsafe { Matrix::new_uninitialized_generic(nrows, nrows) }; - let mut s = unsafe { Matrix::new_uninitialized_generic(nrows.min(ncols), U1) }; - let mut vt = unsafe { Matrix::new_uninitialized_generic(ncols, ncols) }; + let mut u = unsafe { Matrix::new_uninitialized_generic(nrows, nrows).assume_init() }; + let mut s = unsafe { Matrix::new_uninitialized_generic(nrows.min(ncols), U1).assume_init() }; + let mut vt = unsafe { Matrix::new_uninitialized_generic(ncols, ncols).assume_init() }; let ldu = nrows.value(); let ldvt = ncols.value(); diff --git a/nalgebra-lapack/src/symmetric_eigen.rs b/nalgebra-lapack/src/symmetric_eigen.rs index 93961328..c255058d 100644 --- a/nalgebra-lapack/src/symmetric_eigen.rs +++ b/nalgebra-lapack/src/symmetric_eigen.rs @@ -94,7 +94,7 @@ where let lda = n as i32; - let mut values = unsafe { Matrix::new_uninitialized_generic(nrows, U1) }; + let mut values = unsafe { Matrix::new_uninitialized_generic(nrows, U1).assume_init() }; let mut info = 0; let lwork = N::xsyev_work_size(jobz, b'L', n as i32, m.as_mut_slice(), lda, &mut info); From 8dda6714b5b8c515bc25004115125d622abe6f32 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Thu, 24 Sep 2020 23:21:13 -0600 Subject: [PATCH 151/183] Untested UDU implementation Pushing to trigger build Signed-off-by: Christopher Rabotin --- src/linalg/mod.rs | 2 ++ src/linalg/udu.rs | 68 +++++++++++++++++++++++++++++++++++++++++++++ tests/linalg/mod.rs | 1 + tests/linalg/udu.rs | 68 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+) create mode 100644 src/linalg/udu.rs create mode 100644 tests/linalg/udu.rs diff --git a/src/linalg/mod.rs b/src/linalg/mod.rs index df9ae7de..075d91c2 100644 --- a/src/linalg/mod.rs +++ b/src/linalg/mod.rs @@ -25,6 +25,7 @@ mod solve; mod svd; mod symmetric_eigen; mod symmetric_tridiagonal; +mod udu; //// TODO: Not complete enough for publishing. //// This handles only cases where each eigenvalue has multiplicity one. @@ -45,3 +46,4 @@ pub use self::schur::*; pub use self::svd::*; pub use self::symmetric_eigen::*; pub use self::symmetric_tridiagonal::*; +pub use self::udu::*; diff --git a/src/linalg/udu.rs b/src/linalg/udu.rs new file mode 100644 index 00000000..50c2e20d --- /dev/null +++ b/src/linalg/udu.rs @@ -0,0 +1,68 @@ +#[cfg(feature = "serde-serialize")] +use serde::{Deserialize, Serialize}; + +use crate::allocator::Allocator; +use crate::base::{DefaultAllocator, MatrixN}; +use crate::dimension::DimName; +use simba::scalar::ComplexField; + +/// UDU factorization +#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] +#[derive(Clone, Debug)] +pub struct UDU +where + DefaultAllocator: Allocator, +{ + /// The upper triangular matrix resulting from the factorization + pub u: MatrixN, + /// The diagonal matrix resulting from the factorization + pub d: MatrixN, +} + +impl Copy for UDU +where + DefaultAllocator: Allocator, + MatrixN: Copy, +{ +} + +impl UDU +where + DefaultAllocator: Allocator, +{ + /// Computes the UDU^T factorization as per NASA's "Navigation Filter Best Practices", NTRS document ID 20180003657 + /// section 7.2 page 64. + /// NOTE: The provided matrix MUST be symmetric. + pub fn new(matrix: MatrixN) -> Self { + let mut d = MatrixN::::zeros(); + let mut u = MatrixN::::zeros(); + + let n = matrix.ncols(); + + d[(n, n)] = matrix[(n, n)]; + u[(n, n)] = N::one(); + + for j in (0..n - 1).rev() { + u[(j, n)] = matrix[(j, n)] / d[(n, n)]; + } + + for j in (1..n).rev() { + d[(j, j)] = matrix[(j, j)]; + for k in (j + 1..n).rev() { + d[(j, j)] = d[(j, j)] + d[(k, k)] * u[(j, k)].powi(2); + } + + u[(j, j)] = N::one(); + + for i in (0..j - 1).rev() { + u[(i, j)] = matrix[(i, j)]; + for k in j + 1..n { + u[(i, j)] = u[(i, j)] + d[(k, k)] * u[(i, k)] * u[(j, k)]; + } + u[(i, j)] /= d[(j, j)]; + } + } + + Self { u, d } + } +} diff --git a/tests/linalg/mod.rs b/tests/linalg/mod.rs index 66a0c06a..9c252bfd 100644 --- a/tests/linalg/mod.rs +++ b/tests/linalg/mod.rs @@ -14,3 +14,4 @@ mod schur; mod solve; mod svd; mod tridiagonal; +mod udu; diff --git a/tests/linalg/udu.rs b/tests/linalg/udu.rs new file mode 100644 index 00000000..443178d5 --- /dev/null +++ b/tests/linalg/udu.rs @@ -0,0 +1,68 @@ +use na::Matrix3; +use na::UDU; + +#[test] +#[rustfmt::skip] +fn udu_simple() { + let m = Matrix3::new( + 2.0, -1.0, 0.0, + -1.0, 2.0, -1.0, + 0.0, -1.0, 2.0); + + let udu = UDU::new(m); + + println!("u = {}", udu.u); + println!("d = {}", udu.d); + + // Rebuild + let p = &udu.u * &udu.d * &udu.u.transpose(); + + println!("{}", p); + + assert!(relative_eq!(m, p, epsilon = 1.0e-7)); +} + +#[cfg(feature = "arbitrary")] +mod quickcheck_tests { + #[allow(unused_imports)] + use crate::core::helper::{RandComplex, RandScalar}; + + macro_rules! gen_tests( + ($module: ident, $scalar: ty) => { + mod $module { + use std::cmp; + use na::{DMatrix, Matrix4}; + #[allow(unused_imports)] + use crate::core::helper::{RandScalar, RandComplex}; + + quickcheck! { + fn udu(m: DMatrix<$scalar>) -> bool { + let mut m = m; + if m.len() == 0 { + m = DMatrix::<$scalar>::new_random(1, 1); + } + + let m = m.map(|e| e.0); + + let udu = UDU::new(m.clone()); + let p = &udu.u * &udu.d * &udu.u.transpose(); + + relative_eq!(m, p, epsilon = 1.0e-7) + } + + fn udu_static(m: Matrix4<$scalar>) -> bool { + let m = m.map(|e| e.0); + + let udu = UDU::new(m.clone()); + let p = &udu.u * &udu.d * &udu.u.transpose(); + + relative_eq!(m, p, epsilon = 1.0e-7) + } + } + } + } + ); + + gen_tests!(complex, RandComplex); + gen_tests!(f64, RandScalar); +} From d534c3bf9d0d5ea4515bc06a9ea908336cdf117a Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Thu, 24 Sep 2020 23:46:24 -0600 Subject: [PATCH 152/183] Trying to break the test to make sure it works Signed-off-by: Christopher Rabotin --- tests/linalg/udu.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/linalg/udu.rs b/tests/linalg/udu.rs index 443178d5..d9d73e81 100644 --- a/tests/linalg/udu.rs +++ b/tests/linalg/udu.rs @@ -10,16 +10,10 @@ fn udu_simple() { 0.0, -1.0, 2.0); let udu = UDU::new(m); - - println!("u = {}", udu.u); - println!("d = {}", udu.d); - // Rebuild let p = &udu.u * &udu.d * &udu.u.transpose(); - println!("{}", p); - - assert!(relative_eq!(m, p, epsilon = 1.0e-7)); + assert!(relative_eq!(m, 2.0*p, epsilon = 1.0e-7)); } #[cfg(feature = "arbitrary")] From a8d40423ea248d318838cfa941085b0a7883e4f0 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Fri, 25 Sep 2020 19:21:14 -0600 Subject: [PATCH 153/183] Fixed UDU algorithm Signed-off-by: Christopher Rabotin --- src/linalg/udu.rs | 35 +++++++++++++++++++---------------- tests/linalg/udu.rs | 8 ++++---- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/linalg/udu.rs b/src/linalg/udu.rs index 50c2e20d..30143e44 100644 --- a/src/linalg/udu.rs +++ b/src/linalg/udu.rs @@ -30,37 +30,40 @@ impl UDU where DefaultAllocator: Allocator, { - /// Computes the UDU^T factorization as per NASA's "Navigation Filter Best Practices", NTRS document ID 20180003657 - /// section 7.2 page 64. - /// NOTE: The provided matrix MUST be symmetric. - pub fn new(matrix: MatrixN) -> Self { + /// Computes the UDU^T factorization + /// NOTE: The provided matrix MUST be symmetric, and no verification is done in this regard. + /// Ref.: "Optimal control and estimation-Dover Publications", Robert F. Stengel, (1994) page 360 + pub fn new(p: MatrixN) -> Self { let mut d = MatrixN::::zeros(); let mut u = MatrixN::::zeros(); - let n = matrix.ncols(); + let n = p.ncols() - 1; - d[(n, n)] = matrix[(n, n)]; + d[(n, n)] = p[(n, n)]; u[(n, n)] = N::one(); - for j in (0..n - 1).rev() { - u[(j, n)] = matrix[(j, n)] / d[(n, n)]; + for j in (0..n).rev() { + u[(j, n)] = p[(j, n)] / d[(n, n)]; } - for j in (1..n).rev() { - d[(j, j)] = matrix[(j, j)]; - for k in (j + 1..n).rev() { + for j in (0..n).rev() { + for k in j + 1..=n { d[(j, j)] = d[(j, j)] + d[(k, k)] * u[(j, k)].powi(2); } - u[(j, j)] = N::one(); + d[(j, j)] = p[(j, j)] - d[(j, j)]; - for i in (0..j - 1).rev() { - u[(i, j)] = matrix[(i, j)]; - for k in j + 1..n { - u[(i, j)] = u[(i, j)] + d[(k, k)] * u[(i, k)] * u[(j, k)]; + for i in (0..=j).rev() { + for k in j + 1..=n { + u[(i, j)] = u[(i, j)] + d[(k, k)] * u[(j, k)] * u[(i, k)]; } + + u[(i, j)] = p[(i, j)] - u[(i, j)]; + u[(i, j)] /= d[(j, j)]; } + + u[(j, j)] = N::one(); } Self { u, d } diff --git a/tests/linalg/udu.rs b/tests/linalg/udu.rs index d9d73e81..0d457db5 100644 --- a/tests/linalg/udu.rs +++ b/tests/linalg/udu.rs @@ -11,9 +11,9 @@ fn udu_simple() { let udu = UDU::new(m); // Rebuild - let p = &udu.u * &udu.d * &udu.u.transpose(); + let p = udu.u * udu.d * udu.u.transpose(); - assert!(relative_eq!(m, 2.0*p, epsilon = 1.0e-7)); + assert!(relative_eq!(m, p, epsilon = 3.0e-16)); } #[cfg(feature = "arbitrary")] @@ -48,9 +48,9 @@ mod quickcheck_tests { let m = m.map(|e| e.0); let udu = UDU::new(m.clone()); - let p = &udu.u * &udu.d * &udu.u.transpose(); + let p = udu.u * udu.d * udu.u.transpose(); - relative_eq!(m, p, epsilon = 1.0e-7) + relative_eq!(m, p, epsilon = 3.0e-16) } } } From 5a7ed61e9b82134251ae37013acab293c3633674 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Fri, 25 Sep 2020 21:29:46 -0600 Subject: [PATCH 154/183] UDU impl: using 0-index nomenclature Signed-off-by: Christopher Rabotin --- src/linalg/udu.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/linalg/udu.rs b/src/linalg/udu.rs index 30143e44..cd2db206 100644 --- a/src/linalg/udu.rs +++ b/src/linalg/udu.rs @@ -37,24 +37,24 @@ where let mut d = MatrixN::::zeros(); let mut u = MatrixN::::zeros(); - let n = p.ncols() - 1; + let n = p.ncols(); - d[(n, n)] = p[(n, n)]; - u[(n, n)] = N::one(); + d[(n - 1, n - 1)] = p[(n - 1, n - 1)]; + u[(n - 1, n - 1)] = N::one(); - for j in (0..n).rev() { - u[(j, n)] = p[(j, n)] / d[(n, n)]; + for j in (0..n - 1).rev() { + u[(j, n - 1)] = p[(j, n - 1)] / d[(n - 1, n - 1)]; } - for j in (0..n).rev() { - for k in j + 1..=n { + for j in (0..n - 1).rev() { + for k in j + 1..n { d[(j, j)] = d[(j, j)] + d[(k, k)] * u[(j, k)].powi(2); } d[(j, j)] = p[(j, j)] - d[(j, j)]; for i in (0..=j).rev() { - for k in j + 1..=n { + for k in j + 1..n { u[(i, j)] = u[(i, j)] + d[(k, k)] * u[(j, k)] * u[(i, k)]; } From e9933e5c91c4fcd9693d2afd80bb0f7c43e4abf8 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sat, 26 Sep 2020 18:34:35 -0600 Subject: [PATCH 155/183] UDU: Expand to Dim from DimName Signed-off-by: Christopher Rabotin --- src/linalg/udu.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/linalg/udu.rs b/src/linalg/udu.rs index cd2db206..eada8e99 100644 --- a/src/linalg/udu.rs +++ b/src/linalg/udu.rs @@ -3,13 +3,13 @@ use serde::{Deserialize, Serialize}; use crate::allocator::Allocator; use crate::base::{DefaultAllocator, MatrixN}; -use crate::dimension::DimName; +use crate::dimension::Dim; use simba::scalar::ComplexField; /// UDU factorization #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] #[derive(Clone, Debug)] -pub struct UDU +pub struct UDU where DefaultAllocator: Allocator, { @@ -19,14 +19,14 @@ where pub d: MatrixN, } -impl Copy for UDU +impl Copy for UDU where DefaultAllocator: Allocator, MatrixN: Copy, { } -impl UDU +impl UDU where DefaultAllocator: Allocator, { @@ -34,10 +34,11 @@ where /// NOTE: The provided matrix MUST be symmetric, and no verification is done in this regard. /// Ref.: "Optimal control and estimation-Dover Publications", Robert F. Stengel, (1994) page 360 pub fn new(p: MatrixN) -> Self { - let mut d = MatrixN::::zeros(); - let mut u = MatrixN::::zeros(); - let n = p.ncols(); + let n_as_dim = D::from_usize(n); + + let mut d = MatrixN::::zeros_generic(n_as_dim, n_as_dim); + let mut u = MatrixN::::zeros_generic(n_as_dim, n_as_dim); d[(n - 1, n - 1)] = p[(n - 1, n - 1)]; u[(n - 1, n - 1)] = N::one(); From 7a49b9eecaac3e2075b8a91b08ee70a9d4e667ce Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sun, 27 Sep 2020 15:28:50 -0600 Subject: [PATCH 156/183] UDU: d now stored in VectorN instead of MatrixN Signed-off-by: Christopher Rabotin --- src/linalg/udu.rs | 30 ++++++++++++++++++------------ tests/linalg/udu.rs | 6 +++--- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/linalg/udu.rs b/src/linalg/udu.rs index eada8e99..953359e8 100644 --- a/src/linalg/udu.rs +++ b/src/linalg/udu.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use crate::allocator::Allocator; -use crate::base::{DefaultAllocator, MatrixN}; +use crate::base::{DefaultAllocator, MatrixN, VectorN, U1}; use crate::dimension::Dim; use simba::scalar::ComplexField; @@ -11,24 +11,25 @@ use simba::scalar::ComplexField; #[derive(Clone, Debug)] pub struct UDU where - DefaultAllocator: Allocator, + DefaultAllocator: Allocator + Allocator, { /// The upper triangular matrix resulting from the factorization pub u: MatrixN, /// The diagonal matrix resulting from the factorization - pub d: MatrixN, + pub d: VectorN, } impl Copy for UDU where - DefaultAllocator: Allocator, + DefaultAllocator: Allocator + Allocator, + VectorN: Copy, MatrixN: Copy, { } impl UDU where - DefaultAllocator: Allocator, + DefaultAllocator: Allocator + Allocator, { /// Computes the UDU^T factorization /// NOTE: The provided matrix MUST be symmetric, and no verification is done in this regard. @@ -37,31 +38,31 @@ where let n = p.ncols(); let n_as_dim = D::from_usize(n); - let mut d = MatrixN::::zeros_generic(n_as_dim, n_as_dim); + let mut d = VectorN::::zeros_generic(n_as_dim, U1); let mut u = MatrixN::::zeros_generic(n_as_dim, n_as_dim); - d[(n - 1, n - 1)] = p[(n - 1, n - 1)]; + d[n - 1] = p[(n - 1, n - 1)]; u[(n - 1, n - 1)] = N::one(); for j in (0..n - 1).rev() { - u[(j, n - 1)] = p[(j, n - 1)] / d[(n - 1, n - 1)]; + u[(j, n - 1)] = p[(j, n - 1)] / d[n - 1]; } for j in (0..n - 1).rev() { for k in j + 1..n { - d[(j, j)] = d[(j, j)] + d[(k, k)] * u[(j, k)].powi(2); + d[j] = d[j] + d[k] * u[(j, k)].powi(2); } - d[(j, j)] = p[(j, j)] - d[(j, j)]; + d[j] = p[(j, j)] - d[j]; for i in (0..=j).rev() { for k in j + 1..n { - u[(i, j)] = u[(i, j)] + d[(k, k)] * u[(j, k)] * u[(i, k)]; + u[(i, j)] = u[(i, j)] + d[k] * u[(j, k)] * u[(i, k)]; } u[(i, j)] = p[(i, j)] - u[(i, j)]; - u[(i, j)] /= d[(j, j)]; + u[(i, j)] /= d[j]; } u[(j, j)] = N::one(); @@ -69,4 +70,9 @@ where Self { u, d } } + + /// Returns the diagonal elements as a matrix + pub fn d_matrix(&self) -> MatrixN { + MatrixN::from_diagonal(&self.d) + } } diff --git a/tests/linalg/udu.rs b/tests/linalg/udu.rs index 0d457db5..3304d73b 100644 --- a/tests/linalg/udu.rs +++ b/tests/linalg/udu.rs @@ -11,7 +11,7 @@ fn udu_simple() { let udu = UDU::new(m); // Rebuild - let p = udu.u * udu.d * udu.u.transpose(); + let p = udu.u * udu.d_matrix() * udu.u.transpose(); assert!(relative_eq!(m, p, epsilon = 3.0e-16)); } @@ -39,7 +39,7 @@ mod quickcheck_tests { let m = m.map(|e| e.0); let udu = UDU::new(m.clone()); - let p = &udu.u * &udu.d * &udu.u.transpose(); + let p = &udu.u * &udu.d_matrix() * &udu.u.transpose(); relative_eq!(m, p, epsilon = 1.0e-7) } @@ -48,7 +48,7 @@ mod quickcheck_tests { let m = m.map(|e| e.0); let udu = UDU::new(m.clone()); - let p = udu.u * udu.d * udu.u.transpose(); + let p = udu.u * udu.d_matrix() * udu.u.transpose(); relative_eq!(m, p, epsilon = 3.0e-16) } From f6c1aeb07f011a7c1cda295eaae09b54cf17657b Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sun, 27 Sep 2020 16:18:47 -0600 Subject: [PATCH 157/183] UDU: add panic test for non symmetric matrix Signed-off-by: Christopher Rabotin --- tests/linalg/udu.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/linalg/udu.rs b/tests/linalg/udu.rs index 3304d73b..c7f416d7 100644 --- a/tests/linalg/udu.rs +++ b/tests/linalg/udu.rs @@ -16,6 +16,22 @@ fn udu_simple() { assert!(relative_eq!(m, p, epsilon = 3.0e-16)); } +#[test] +#[should_panic] +#[rustfmt::skip] +fn udu_non_sym_panic() { + let m = Matrix3::new( + 2.0, -1.0, 0.0, + 1.0, -2.0, 3.0, + -2.0, 1.0, 0.0); + + let udu = UDU::new(m); + // Rebuild + let p = udu.u * udu.d_matrix() * udu.u.transpose(); + + assert!(relative_eq!(m, p, epsilon = 3.0e-16)); +} + #[cfg(feature = "arbitrary")] mod quickcheck_tests { #[allow(unused_imports)] From 06861a9755e2f629bec0b25371affd7d0f8a697d Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 26 Oct 2020 14:39:34 -0600 Subject: [PATCH 158/183] Update src/linalg/udu.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sébastien Crozet --- src/linalg/udu.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linalg/udu.rs b/src/linalg/udu.rs index 953359e8..182b8a6d 100644 --- a/src/linalg/udu.rs +++ b/src/linalg/udu.rs @@ -32,7 +32,7 @@ where DefaultAllocator: Allocator + Allocator, { /// Computes the UDU^T factorization - /// NOTE: The provided matrix MUST be symmetric, and no verification is done in this regard. + /// The input matrix `p` is assumed to be symmetric and this decomposition will only read the upper-triangular part of `p`. /// Ref.: "Optimal control and estimation-Dover Publications", Robert F. Stengel, (1994) page 360 pub fn new(p: MatrixN) -> Self { let n = p.ncols(); From 4ff4911ac3559bc7e825a8bd2b05d1e0e475e69d Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Mon, 26 Oct 2020 22:06:37 -0600 Subject: [PATCH 159/183] Implement requested changes Signed-off-by: Christopher Rabotin --- src/linalg/udu.rs | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/linalg/udu.rs b/src/linalg/udu.rs index 182b8a6d..bce2ea41 100644 --- a/src/linalg/udu.rs +++ b/src/linalg/udu.rs @@ -4,10 +4,21 @@ use serde::{Deserialize, Serialize}; use crate::allocator::Allocator; use crate::base::{DefaultAllocator, MatrixN, VectorN, U1}; use crate::dimension::Dim; +use crate::storage::Storage; use simba::scalar::ComplexField; /// UDU factorization #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde-serialize", + serde(bound(serialize = "VectorN: Serialize, MatrixN: Serialize")) +)] +#[cfg_attr( + feature = "serde-serialize", + serde(bound( + deserialize = "VectorN: Deserialize<'de>, MatrixN: Deserialize<'de>" + )) +)] #[derive(Clone, Debug)] pub struct UDU where @@ -36,33 +47,30 @@ where /// Ref.: "Optimal control and estimation-Dover Publications", Robert F. Stengel, (1994) page 360 pub fn new(p: MatrixN) -> Self { let n = p.ncols(); - let n_as_dim = D::from_usize(n); + let n_dim = p.data.shape().1; - let mut d = VectorN::::zeros_generic(n_as_dim, U1); - let mut u = MatrixN::::zeros_generic(n_as_dim, n_as_dim); + let mut d = VectorN::zeros_generic(n_dim, U1); + let mut u = MatrixN::zeros_generic(n_dim, n_dim); d[n - 1] = p[(n - 1, n - 1)]; - u[(n - 1, n - 1)] = N::one(); - - for j in (0..n - 1).rev() { - u[(j, n - 1)] = p[(j, n - 1)] / d[n - 1]; - } + u.column_mut(n - 1) + .axpy(N::one() / d[n - 1], &p.column(n - 1), N::zero()); for j in (0..n - 1).rev() { + let mut d_j = d[j]; for k in j + 1..n { - d[j] = d[j] + d[k] * u[(j, k)].powi(2); + d_j += d[k] * u[(j, k)].powi(2); } - d[j] = p[(j, j)] - d[j]; + d[j] = p[(j, j)] - d_j; for i in (0..=j).rev() { + let mut u_ij = u[(i, j)]; for k in j + 1..n { - u[(i, j)] = u[(i, j)] + d[k] * u[(j, k)] * u[(i, k)]; + u_ij += d[k] * u[(j, k)] * u[(i, k)]; } - u[(i, j)] = p[(i, j)] - u[(i, j)]; - - u[(i, j)] /= d[j]; + u[(i, j)] = (p[(i, j)] - u_ij) / d[j]; } u[(j, j)] = N::one(); From 89ca2fe5fbe7a8987096cb5d8ca0fe6da92189e8 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Wed, 28 Oct 2020 16:04:46 -0600 Subject: [PATCH 160/183] UDU only supported for Real matrices, not Complex Signed-off-by: Christopher Rabotin --- src/linalg/udu.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/linalg/udu.rs b/src/linalg/udu.rs index bce2ea41..46b1497c 100644 --- a/src/linalg/udu.rs +++ b/src/linalg/udu.rs @@ -5,7 +5,7 @@ use crate::allocator::Allocator; use crate::base::{DefaultAllocator, MatrixN, VectorN, U1}; use crate::dimension::Dim; use crate::storage::Storage; -use simba::scalar::ComplexField; +use simba::scalar::RealField; /// UDU factorization #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] @@ -20,7 +20,7 @@ use simba::scalar::ComplexField; )) )] #[derive(Clone, Debug)] -pub struct UDU +pub struct UDU where DefaultAllocator: Allocator + Allocator, { @@ -30,7 +30,7 @@ where pub d: VectorN, } -impl Copy for UDU +impl Copy for UDU where DefaultAllocator: Allocator + Allocator, VectorN: Copy, @@ -38,7 +38,7 @@ where { } -impl UDU +impl UDU where DefaultAllocator: Allocator + Allocator, { From ab0d335b61f9869d8a958c7ac84ea80ecba8d42e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Thu, 25 Feb 2021 13:16:04 +0100 Subject: [PATCH 161/183] Fix tests for the UDU decomposition. --- src/linalg/udu.rs | 9 ++++++--- tests/linalg/udu.rs | 21 +++++++-------------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/linalg/udu.rs b/src/linalg/udu.rs index 46b1497c..346753b2 100644 --- a/src/linalg/udu.rs +++ b/src/linalg/udu.rs @@ -7,7 +7,7 @@ use crate::dimension::Dim; use crate::storage::Storage; use simba::scalar::RealField; -/// UDU factorization +/// UDU factorization. #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] #[cfg_attr( feature = "serde-serialize", @@ -42,8 +42,11 @@ impl UDU where DefaultAllocator: Allocator + Allocator, { - /// Computes the UDU^T factorization - /// The input matrix `p` is assumed to be symmetric and this decomposition will only read the upper-triangular part of `p`. + /// Computes the UDU^T factorization. + /// + /// The input matrix `p` is assumed to be symmetric and this decomposition will only read + /// the upper-triangular part of `p`. + /// /// Ref.: "Optimal control and estimation-Dover Publications", Robert F. Stengel, (1994) page 360 pub fn new(p: MatrixN) -> Self { let n = p.ncols(); diff --git a/tests/linalg/udu.rs b/tests/linalg/udu.rs index c7f416d7..95cdc9c2 100644 --- a/tests/linalg/udu.rs +++ b/tests/linalg/udu.rs @@ -1,5 +1,4 @@ -use na::Matrix3; -use na::UDU; +use na::{Matrix3, UDU}; #[test] #[rustfmt::skip] @@ -40,19 +39,14 @@ mod quickcheck_tests { macro_rules! gen_tests( ($module: ident, $scalar: ty) => { mod $module { - use std::cmp; - use na::{DMatrix, Matrix4}; + use na::{UDU, DMatrix, Matrix4}; #[allow(unused_imports)] use crate::core::helper::{RandScalar, RandComplex}; quickcheck! { - fn udu(m: DMatrix<$scalar>) -> bool { - let mut m = m; - if m.len() == 0 { - m = DMatrix::<$scalar>::new_random(1, 1); - } - - let m = m.map(|e| e.0); + fn udu(n: usize) -> bool { + let n = std::cmp::max(1, std::cmp::min(n, 10)); + let m = DMatrix::<$scalar>::new_random(n, n).map(|e| e.0).hermitian_part(); let udu = UDU::new(m.clone()); let p = &udu.u * &udu.d_matrix() * &udu.u.transpose(); @@ -61,18 +55,17 @@ mod quickcheck_tests { } fn udu_static(m: Matrix4<$scalar>) -> bool { - let m = m.map(|e| e.0); + let m = m.map(|e| e.0).hermitian_part(); let udu = UDU::new(m.clone()); let p = udu.u * udu.d_matrix() * udu.u.transpose(); - relative_eq!(m, p, epsilon = 3.0e-16) + relative_eq!(m, p, epsilon = 1.0e-7) } } } } ); - gen_tests!(complex, RandComplex); gen_tests!(f64, RandScalar); } From aeb9f7ea3994499540e3a7eeb1b4eed46768a275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Thu, 25 Feb 2021 13:20:20 +0100 Subject: [PATCH 162/183] Add a matrix.udu() method to compute the UDU decomposition. --- src/linalg/decomposition.rs | 17 +++++++++++++++-- tests/linalg/udu.rs | 13 +++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/linalg/decomposition.rs b/src/linalg/decomposition.rs index 7890748c..99f2bcfe 100644 --- a/src/linalg/decomposition.rs +++ b/src/linalg/decomposition.rs @@ -1,8 +1,8 @@ use crate::storage::Storage; use crate::{ Allocator, Bidiagonal, Cholesky, ColPivQR, ComplexField, DefaultAllocator, Dim, DimDiff, - DimMin, DimMinimum, DimSub, FullPivLU, Hessenberg, Matrix, Schur, SymmetricEigen, - SymmetricTridiagonal, LU, QR, SVD, U1, + DimMin, DimMinimum, DimSub, FullPivLU, Hessenberg, Matrix, RealField, Schur, SymmetricEigen, + SymmetricTridiagonal, LU, QR, SVD, U1, UDU }; /// # Rectangular matrix decomposition @@ -134,6 +134,7 @@ impl> Matrix { /// | -------------------------|---------------------------|--------------| /// | Hessenberg | `Q * H * Qᵀ` | `Q` is a unitary matrix and `H` an upper-Hessenberg matrix. | /// | Cholesky | `L * Lᵀ` | `L` is a lower-triangular matrix. | +/// | UDU | `U * D * Uᵀ` | `U` is a upper-triangular matrix, and `D` a diagonal matrix. | /// | Schur decomposition | `Q * T * Qᵀ` | `Q` is an unitary matrix and `T` a quasi-upper-triangular matrix. | /// | Symmetric eigendecomposition | `Q ~ Λ ~ Qᵀ` | `Q` is an unitary matrix, and `Λ` is a real diagonal matrix. | /// | Symmetric tridiagonalization | `Q ~ T ~ Qᵀ` | `Q` is an unitary matrix, and `T` is a tridiagonal matrix. | @@ -149,6 +150,18 @@ impl> Matrix { Cholesky::new(self.into_owned()) } + /// Attempts to compute the UDU decomposition of this matrix. + /// + /// The input matrix `self` is assumed to be symmetric and this decomposition will only read + /// the upper-triangular part of `self`. + pub fn udu(self) -> UDU + where + N: RealField, + DefaultAllocator: Allocator + Allocator, + { + UDU::new(self.into_owned()) + } + /// Computes the Hessenberg decomposition of this matrix using householder reflections. pub fn hessenberg(self) -> Hessenberg where diff --git a/tests/linalg/udu.rs b/tests/linalg/udu.rs index 95cdc9c2..006cc95f 100644 --- a/tests/linalg/udu.rs +++ b/tests/linalg/udu.rs @@ -1,4 +1,4 @@ -use na::{Matrix3, UDU}; +use na::Matrix3; #[test] #[rustfmt::skip] @@ -8,7 +8,8 @@ fn udu_simple() { -1.0, 2.0, -1.0, 0.0, -1.0, 2.0); - let udu = UDU::new(m); + let udu = m.udu(); + // Rebuild let p = udu.u * udu.d_matrix() * udu.u.transpose(); @@ -24,7 +25,7 @@ fn udu_non_sym_panic() { 1.0, -2.0, 3.0, -2.0, 1.0, 0.0); - let udu = UDU::new(m); + let udu = m.udu(); // Rebuild let p = udu.u * udu.d_matrix() * udu.u.transpose(); @@ -39,7 +40,7 @@ mod quickcheck_tests { macro_rules! gen_tests( ($module: ident, $scalar: ty) => { mod $module { - use na::{UDU, DMatrix, Matrix4}; + use na::{DMatrix, Matrix4}; #[allow(unused_imports)] use crate::core::helper::{RandScalar, RandComplex}; @@ -48,7 +49,7 @@ mod quickcheck_tests { let n = std::cmp::max(1, std::cmp::min(n, 10)); let m = DMatrix::<$scalar>::new_random(n, n).map(|e| e.0).hermitian_part(); - let udu = UDU::new(m.clone()); + let udu = m.clone().udu(); let p = &udu.u * &udu.d_matrix() * &udu.u.transpose(); relative_eq!(m, p, epsilon = 1.0e-7) @@ -57,7 +58,7 @@ mod quickcheck_tests { fn udu_static(m: Matrix4<$scalar>) -> bool { let m = m.map(|e| e.0).hermitian_part(); - let udu = UDU::new(m.clone()); + let udu = m.udu(); let p = udu.u * udu.d_matrix() * udu.u.transpose(); relative_eq!(m, p, epsilon = 1.0e-7) From 6699039fecbb9271539432d0dfbeac822d50c9ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Thu, 25 Feb 2021 15:51:13 +0100 Subject: [PATCH 163/183] Fix rebase-induced compilation error. --- src/linalg/col_piv_qr.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/linalg/col_piv_qr.rs b/src/linalg/col_piv_qr.rs index 4fa97482..302dcd66 100644 --- a/src/linalg/col_piv_qr.rs +++ b/src/linalg/col_piv_qr.rs @@ -66,7 +66,8 @@ where let min_nrows_ncols = nrows.min(ncols); let mut p = PermutationSequence::identity_generic(min_nrows_ncols); - let mut diag = unsafe { MatrixMN::new_uninitialized_generic(min_nrows_ncols, U1) }; + let mut diag = + unsafe { crate::unimplemented_or_uninitialized_generic!(min_nrows_ncols, U1) }; if min_nrows_ncols.value() == 0 { return ColPivQR { From 7b6b3649f27770fe456add8debff443e87d4147f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Thu, 25 Feb 2021 16:20:11 +0100 Subject: [PATCH 164/183] Run cargo fmt. --- src/linalg/decomposition.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linalg/decomposition.rs b/src/linalg/decomposition.rs index 99f2bcfe..e167861a 100644 --- a/src/linalg/decomposition.rs +++ b/src/linalg/decomposition.rs @@ -2,7 +2,7 @@ use crate::storage::Storage; use crate::{ Allocator, Bidiagonal, Cholesky, ColPivQR, ComplexField, DefaultAllocator, Dim, DimDiff, DimMin, DimMinimum, DimSub, FullPivLU, Hessenberg, Matrix, RealField, Schur, SymmetricEigen, - SymmetricTridiagonal, LU, QR, SVD, U1, UDU + SymmetricTridiagonal, LU, QR, SVD, U1, UDU, }; /// # Rectangular matrix decomposition From 6cfd2bca143aa9a8c5c3ed2ea756e9e73b7cdc58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Sun, 28 Feb 2021 17:52:14 +0100 Subject: [PATCH 165/183] Use proptest for all nalgebra tests. --- .circleci/config.yml | 2 +- Cargo.toml | 21 +- nalgebra-glm/Cargo.toml | 3 +- nalgebra-lapack/Cargo.toml | 14 +- nalgebra-sparse/Cargo.toml | 4 +- src/base/construction.rs | 10 +- src/base/helper.rs | 2 +- src/debug/random_orthogonal.rs | 5 +- src/debug/random_sdp.rs | 5 +- src/geometry/dual_quaternion_construction.rs | 4 +- src/geometry/isometry_construction.rs | 2 +- src/geometry/orthographic.rs | 2 +- src/geometry/perspective.rs | 2 +- src/geometry/point_construction.rs | 2 +- src/geometry/quaternion_construction.rs | 4 +- src/geometry/rotation_specialization.rs | 4 +- src/geometry/similarity_construction.rs | 2 +- src/geometry/translation_construction.rs | 4 +- src/geometry/unit_complex_construction.rs | 2 +- src/linalg/decomposition.rs | 2 +- src/linalg/udu.rs | 13 +- src/proptest/mod.rs | 2 +- tests/core/blas.rs | 55 ++--- tests/core/conversion.rs | 172 ++++++------- tests/core/helper.rs | 4 +- tests/core/matrix.rs | 173 ++++++------- tests/core/matrixcompare.rs | 36 ++- tests/geometry/dual_quaternion.rs | 88 +++---- tests/geometry/isometry.rs | 172 +++++++------ tests/geometry/point.rs | 8 - tests/geometry/projection.rs | 21 +- tests/geometry/quaternion.rs | 87 ++++--- tests/geometry/rotation.rs | 138 ++++++----- tests/geometry/similarity.rs | 244 ++++++++++--------- tests/geometry/unit_complex.rs | 52 ++-- tests/lib.rs | 3 - tests/linalg/balancing.rs | 22 +- tests/linalg/bidiagonal.rs | 46 ++-- tests/linalg/cholesky.rs | 68 +++--- tests/linalg/col_piv_qr.rs | 109 ++++----- tests/linalg/eigen.rs | 52 ++-- tests/linalg/exp.rs | 1 - tests/linalg/full_piv_lu.rs | 96 ++++---- tests/linalg/hessenberg.rs | 33 ++- tests/linalg/lu.rs | 106 ++++---- tests/linalg/qr.rs | 114 ++++----- tests/linalg/schur.rs | 69 ++---- tests/linalg/solve.rs | 46 ++-- tests/linalg/svd.rs | 191 +++++++-------- tests/linalg/tridiagonal.rs | 46 ++-- tests/linalg/udu.rs | 46 ++-- tests/proptest/mod.rs | 157 +++++++++++- tests/sparse/cs_conversion.rs | 3 - tests/sparse/cs_matrix_market.rs | 1 - 54 files changed, 1337 insertions(+), 1233 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e5be1145..f0c28f6f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -60,7 +60,7 @@ jobs: command: cargo test --features arbitrary --features serde-serialize --features abomonation-serialize --features sparse --features debug --features io --features compare --features libm --features proptest-support --features slow-tests - run: name: test nalgebra-glm - command: cargo test -p nalgebra-glm --features arbitrary --features serde-serialize --features abomonation-serialize --features sparse --features debug --features io --features compare --features libm --features slow-tests + command: cargo test -p nalgebra-glm --features arbitrary --features serde-serialize --features abomonation-serialize --features sparse --features debug --features io --features compare --features libm --features proptest-support --features slow-tests - run: name: test nalgebra-sparse # Manifest-path is necessary because cargo otherwise won't correctly forward features diff --git a/Cargo.toml b/Cargo.toml index c753a350..67747f33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,8 +23,7 @@ path = "src/lib.rs" [features] default = [ "std" ] -std = [ "matrixmultiply", "rand/std", "rand_distr", "simba/std" ] -stdweb = [ "rand/stdweb" ] +std = [ "matrixmultiply", "rand/std", "rand/std_rng", "rand_distr", "simba/std" ] arbitrary = [ "quickcheck" ] serde-serialize = [ "serde", "num-complex/serde" ] abomonation-serialize = [ "abomonation" ] @@ -44,29 +43,29 @@ slow-tests = [] [dependencies] typenum = "1.12" generic-array = "0.14" -rand = { version = "0.7", default-features = false } +rand = { version = "0.8", default-features = false } num-traits = { version = "0.2", default-features = false } num-complex = { version = "0.3", default-features = false } num-rational = { version = "0.3", default-features = false } approx = { version = "0.4", default-features = false } -simba = { version = "0.3", default-features = false } +simba = { version = "0.4", default-features = false } alga = { version = "0.9", default-features = false, optional = true } -rand_distr = { version = "0.3", optional = true } -matrixmultiply = { version = "0.2", optional = true } +rand_distr = { version = "0.4", optional = true } +matrixmultiply = { version = "0.3", optional = true } serde = { version = "1.0", default-features = false, features = [ "derive" ], optional = true } abomonation = { version = "0.7", optional = true } mint = { version = "0.5", optional = true } -quickcheck = { version = "0.9", optional = true } +quickcheck = { version = "1", optional = true } pest = { version = "2", optional = true } pest_derive = { version = "2", optional = true } bytemuck = { version = "1.5", optional = true } matrixcompare-core = { version = "0.1", optional = true } -proptest = { version = "0.10", optional = true, default-features = false, features = ["std"] } +proptest = { version = "1", optional = true, default-features = false, features = ["std"] } [dev-dependencies] serde_json = "1.0" -rand_xorshift = "0.2" -rand_isaac = "0.2" +rand_xorshift = "0.3" +rand_isaac = "0.3" ### Uncomment this line before running benchmarks. ### We can't just let this uncommented because that would break ### compilation for #[no-std] because of the terrible Cargo bug @@ -75,7 +74,7 @@ rand_isaac = "0.2" # For matrix comparison macro matrixcompare = "0.2.0" -itertools = "0.9" +itertools = "0.10" [workspace] members = [ "nalgebra-lapack", "nalgebra-glm", "nalgebra-sparse" ] diff --git a/nalgebra-glm/Cargo.toml b/nalgebra-glm/Cargo.toml index 97e7f8b3..f3f1c886 100644 --- a/nalgebra-glm/Cargo.toml +++ b/nalgebra-glm/Cargo.toml @@ -19,7 +19,6 @@ maintenance = { status = "actively-developed" } [features] default = [ "std" ] std = [ "nalgebra/std", "simba/std" ] -stdweb = [ "nalgebra/stdweb" ] arbitrary = [ "nalgebra/arbitrary" ] serde-serialize = [ "nalgebra/serde-serialize" ] abomonation-serialize = [ "nalgebra/abomonation-serialize" ] @@ -27,5 +26,5 @@ abomonation-serialize = [ "nalgebra/abomonation-serialize" ] [dependencies] num-traits = { version = "0.2", default-features = false } approx = { version = "0.4", default-features = false } -simba = { version = "0.3", default-features = false } +simba = { version = "0.4", default-features = false } nalgebra = { path = "..", version = "0.24", default-features = false } diff --git a/nalgebra-lapack/Cargo.toml b/nalgebra-lapack/Cargo.toml index 58c92dd0..cefc6662 100644 --- a/nalgebra-lapack/Cargo.toml +++ b/nalgebra-lapack/Cargo.toml @@ -29,16 +29,16 @@ intel-mkl = ["lapack-src/intel-mkl"] [dependencies] nalgebra = { version = "0.24", path = ".." } num-traits = "0.2" -num-complex = { version = "0.2", default-features = false } -simba = "0.2" +num-complex = { version = "0.3", default-features = false } +simba = "0.4" serde = { version = "1.0", optional = true } serde_derive = { version = "1.0", optional = true } -lapack = { version = "0.16", default-features = false } -lapack-src = { version = "0.5", default-features = false } +lapack = { version = "0.17", default-features = false } +lapack-src = { version = "0.6", default-features = false } # clippy = "*" [dev-dependencies] nalgebra = { version = "0.24", features = [ "arbitrary" ], path = ".." } -quickcheck = "0.9" -approx = "0.3" -rand = "0.7" +quickcheck = "1" +approx = "0.4" +rand = "0.8" diff --git a/nalgebra-sparse/Cargo.toml b/nalgebra-sparse/Cargo.toml index bca07280..fed47f1b 100644 --- a/nalgebra-sparse/Cargo.toml +++ b/nalgebra-sparse/Cargo.toml @@ -14,11 +14,11 @@ slow-tests = [] [dependencies] nalgebra = { version="0.24", path = "../" } num-traits = { version = "0.2", default-features = false } -proptest = { version = "0.10", optional = true } +proptest = { version = "1.0", optional = true } matrixcompare-core = { version = "0.1.0", optional = true } [dev-dependencies] -itertools = "0.9" +itertools = "0.10" matrixcompare = { version = "0.2.0", features = [ "proptest-support" ] } nalgebra = { version="0.24", path = "../", features = ["compare"] } diff --git a/src/base/construction.rs b/src/base/construction.rs index e0464a02..ba15a0f0 100644 --- a/src/base/construction.rs +++ b/src/base/construction.rs @@ -824,8 +824,8 @@ where { #[inline] fn sample<'a, G: Rng + ?Sized>(&self, rng: &'a mut G) -> MatrixMN { - let nrows = R::try_to_usize().unwrap_or_else(|| rng.gen_range(0, 10)); - let ncols = C::try_to_usize().unwrap_or_else(|| rng.gen_range(0, 10)); + let nrows = R::try_to_usize().unwrap_or_else(|| rng.gen_range(0..10)); + let ncols = C::try_to_usize().unwrap_or_else(|| rng.gen_range(0..10)); MatrixMN::from_fn_generic(R::from_usize(nrows), C::from_usize(ncols), |_, _| rng.gen()) } @@ -841,9 +841,9 @@ where Owned: Clone + Send, { #[inline] - fn arbitrary(g: &mut G) -> Self { - let nrows = R::try_to_usize().unwrap_or(g.gen_range(0, 10)); - let ncols = C::try_to_usize().unwrap_or(g.gen_range(0, 10)); + fn arbitrary(g: &mut Gen) -> Self { + let nrows = R::try_to_usize().unwrap_or(usize::arbitrary(g) % 10); + let ncols = C::try_to_usize().unwrap_or(usize::arbitrary(g) % 10); Self::from_fn_generic(R::from_usize(nrows), C::from_usize(ncols), |_, _| { N::arbitrary(g) diff --git a/src/base/helper.rs b/src/base/helper.rs index de601fb6..fe5ffd02 100644 --- a/src/base/helper.rs +++ b/src/base/helper.rs @@ -7,7 +7,7 @@ use rand::Rng; #[cfg(feature = "arbitrary")] #[doc(hidden)] #[inline] -pub fn reject bool, T: Arbitrary>(g: &mut G, f: F) -> T { +pub fn reject bool, T: Arbitrary>(g: &mut Gen, f: F) -> T { use std::iter; iter::repeat(()) .map(|_| Arbitrary::arbitrary(g)) diff --git a/src/debug/random_orthogonal.rs b/src/debug/random_orthogonal.rs index de72bdb7..fc740dc2 100644 --- a/src/debug/random_orthogonal.rs +++ b/src/debug/random_orthogonal.rs @@ -48,9 +48,8 @@ where DefaultAllocator: Allocator, Owned: Clone + Send, { - fn arbitrary(g: &mut G) -> Self { - use rand::Rng; - let dim = D::try_to_usize().unwrap_or(g.gen_range(1, 50)); + fn arbitrary(g: &mut Gen) -> Self { + let dim = D::try_to_usize().unwrap_or(1 + usize::arbitrary(g) % 50); Self::new(D::from_usize(dim), || N::arbitrary(g)) } } diff --git a/src/debug/random_sdp.rs b/src/debug/random_sdp.rs index b9f81859..fa2ef118 100644 --- a/src/debug/random_sdp.rs +++ b/src/debug/random_sdp.rs @@ -51,9 +51,8 @@ where DefaultAllocator: Allocator, Owned: Clone + Send, { - fn arbitrary(g: &mut G) -> Self { - use rand::Rng; - let dim = D::try_to_usize().unwrap_or(g.gen_range(1, 50)); + fn arbitrary(g: &mut Gen) -> Self { + let dim = D::try_to_usize().unwrap_or(1 + usize::arbitrary(g) % 50); Self::new(D::from_usize(dim), || N::arbitrary(g)) } } diff --git a/src/geometry/dual_quaternion_construction.rs b/src/geometry/dual_quaternion_construction.rs index b0ae8036..739972a9 100644 --- a/src/geometry/dual_quaternion_construction.rs +++ b/src/geometry/dual_quaternion_construction.rs @@ -108,7 +108,7 @@ where N::Element: SimdRealField, { #[inline] - fn arbitrary(rng: &mut G) -> Self { + fn arbitrary(rng: &mut Gen) -> Self { Self::from_real_and_dual(Arbitrary::arbitrary(rng), Arbitrary::arbitrary(rng)) } } @@ -216,7 +216,7 @@ where N::Element: SimdRealField, { #[inline] - fn arbitrary(rng: &mut G) -> Self { + fn arbitrary(rng: &mut Gen) -> Self { Self::new_normalize(Arbitrary::arbitrary(rng)) } } diff --git a/src/geometry/isometry_construction.rs b/src/geometry/isometry_construction.rs index 0f487547..a64f8208 100644 --- a/src/geometry/isometry_construction.rs +++ b/src/geometry/isometry_construction.rs @@ -102,7 +102,7 @@ where DefaultAllocator: Allocator, { #[inline] - fn arbitrary(rng: &mut G) -> Self { + fn arbitrary(rng: &mut Gen) -> Self { Self::from_parts(Arbitrary::arbitrary(rng), Arbitrary::arbitrary(rng)) } } diff --git a/src/geometry/orthographic.rs b/src/geometry/orthographic.rs index bd1e73c7..bf85a198 100644 --- a/src/geometry/orthographic.rs +++ b/src/geometry/orthographic.rs @@ -705,7 +705,7 @@ impl Arbitrary for Orthographic3 where Matrix4: Send, { - fn arbitrary(g: &mut G) -> Self { + fn arbitrary(g: &mut Gen) -> Self { let left = Arbitrary::arbitrary(g); let right = helper::reject(g, |x: &N| *x > left); let bottom = Arbitrary::arbitrary(g); diff --git a/src/geometry/perspective.rs b/src/geometry/perspective.rs index bd8abac2..066ca57a 100644 --- a/src/geometry/perspective.rs +++ b/src/geometry/perspective.rs @@ -283,7 +283,7 @@ where #[cfg(feature = "arbitrary")] impl Arbitrary for Perspective3 { - fn arbitrary(g: &mut G) -> Self { + fn arbitrary(g: &mut Gen) -> Self { let znear = Arbitrary::arbitrary(g); let zfar = helper::reject(g, |&x: &N| !(x - znear).is_zero()); let aspect = helper::reject(g, |&x: &N| !x.is_zero()); diff --git a/src/geometry/point_construction.rs b/src/geometry/point_construction.rs index c21680a9..e132304b 100644 --- a/src/geometry/point_construction.rs +++ b/src/geometry/point_construction.rs @@ -156,7 +156,7 @@ where >::Buffer: Send, { #[inline] - fn arbitrary(g: &mut G) -> Self { + fn arbitrary(g: &mut Gen) -> Self { Self::from(VectorN::arbitrary(g)) } } diff --git a/src/geometry/quaternion_construction.rs b/src/geometry/quaternion_construction.rs index a26deb1a..ec46b68b 100644 --- a/src/geometry/quaternion_construction.rs +++ b/src/geometry/quaternion_construction.rs @@ -160,7 +160,7 @@ where Owned: Send, { #[inline] - fn arbitrary(g: &mut G) -> Self { + fn arbitrary(g: &mut Gen) -> Self { Self::new( N::arbitrary(g), N::arbitrary(g), @@ -845,7 +845,7 @@ where Owned: Send, { #[inline] - fn arbitrary(g: &mut G) -> Self { + fn arbitrary(g: &mut Gen) -> Self { let axisangle = Vector3::arbitrary(g); Self::from_scaled_axis(axisangle) } diff --git a/src/geometry/rotation_specialization.rs b/src/geometry/rotation_specialization.rs index fcaa8667..de87b40b 100644 --- a/src/geometry/rotation_specialization.rs +++ b/src/geometry/rotation_specialization.rs @@ -275,7 +275,7 @@ where Owned: Send, { #[inline] - fn arbitrary(g: &mut G) -> Self { + fn arbitrary(g: &mut Gen) -> Self { Self::new(N::arbitrary(g)) } } @@ -961,7 +961,7 @@ where Owned: Send, { #[inline] - fn arbitrary(g: &mut G) -> Self { + fn arbitrary(g: &mut Gen) -> Self { Self::new(VectorN::arbitrary(g)) } } diff --git a/src/geometry/similarity_construction.rs b/src/geometry/similarity_construction.rs index 510758cf..c228c5d0 100644 --- a/src/geometry/similarity_construction.rs +++ b/src/geometry/similarity_construction.rs @@ -114,7 +114,7 @@ where Owned: Send, { #[inline] - fn arbitrary(rng: &mut G) -> Self { + fn arbitrary(rng: &mut Gen) -> Self { let mut s: N = Arbitrary::arbitrary(rng); while s.is_zero() { s = Arbitrary::arbitrary(rng) diff --git a/src/geometry/translation_construction.rs b/src/geometry/translation_construction.rs index 9466816d..d9061ba0 100644 --- a/src/geometry/translation_construction.rs +++ b/src/geometry/translation_construction.rs @@ -61,13 +61,13 @@ where } #[cfg(feature = "arbitrary")] -impl Arbitrary for Translation +impl Arbitrary for Translation where DefaultAllocator: Allocator, Owned: Send, { #[inline] - fn arbitrary(rng: &mut G) -> Self { + fn arbitrary(rng: &mut Gen) -> Self { let v: VectorN = Arbitrary::arbitrary(rng); Self::from(v) } diff --git a/src/geometry/unit_complex_construction.rs b/src/geometry/unit_complex_construction.rs index acdbac8a..114fea6e 100644 --- a/src/geometry/unit_complex_construction.rs +++ b/src/geometry/unit_complex_construction.rs @@ -395,7 +395,7 @@ where N::Element: SimdRealField, { #[inline] - fn arbitrary(g: &mut G) -> Self { + fn arbitrary(g: &mut Gen) -> Self { UnitComplex::from_angle(N::arbitrary(g)) } } diff --git a/src/linalg/decomposition.rs b/src/linalg/decomposition.rs index e167861a..6428856b 100644 --- a/src/linalg/decomposition.rs +++ b/src/linalg/decomposition.rs @@ -154,7 +154,7 @@ impl> Matrix { /// /// The input matrix `self` is assumed to be symmetric and this decomposition will only read /// the upper-triangular part of `self`. - pub fn udu(self) -> UDU + pub fn udu(self) -> Option> where N: RealField, DefaultAllocator: Allocator + Allocator, diff --git a/src/linalg/udu.rs b/src/linalg/udu.rs index 346753b2..70ff84a7 100644 --- a/src/linalg/udu.rs +++ b/src/linalg/udu.rs @@ -48,7 +48,7 @@ where /// the upper-triangular part of `p`. /// /// Ref.: "Optimal control and estimation-Dover Publications", Robert F. Stengel, (1994) page 360 - pub fn new(p: MatrixN) -> Self { + pub fn new(p: MatrixN) -> Option { let n = p.ncols(); let n_dim = p.data.shape().1; @@ -56,6 +56,11 @@ where let mut u = MatrixN::zeros_generic(n_dim, n_dim); d[n - 1] = p[(n - 1, n - 1)]; + + if d[n - 1].is_zero() { + return None; + } + u.column_mut(n - 1) .axpy(N::one() / d[n - 1], &p.column(n - 1), N::zero()); @@ -67,6 +72,10 @@ where d[j] = p[(j, j)] - d_j; + if d[j].is_zero() { + return None; + } + for i in (0..=j).rev() { let mut u_ij = u[(i, j)]; for k in j + 1..n { @@ -79,7 +88,7 @@ where u[(j, j)] = N::one(); } - Self { u, d } + Some(Self { u, d }) } /// Returns the diagonal elements as a matrix diff --git a/src/proptest/mod.rs b/src/proptest/mod.rs index 0ac2a377..ae263956 100644 --- a/src/proptest/mod.rs +++ b/src/proptest/mod.rs @@ -408,7 +408,7 @@ where } /// A strategy for generating matrices. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct MatrixStrategy where NStrategy: Strategy, diff --git a/tests/core/blas.rs b/tests/core/blas.rs index 9b7be4af..ef5c1c2f 100644 --- a/tests/core/blas.rs +++ b/tests/core/blas.rs @@ -21,19 +21,20 @@ fn gemm_noncommutative() { assert_eq!(res, Matrix2::zero()); } -#[cfg(feature = "arbitrary")] -mod blas_quickcheck { +#[cfg(feature = "proptest-support")] +mod blas_proptest { + use crate::proptest::{PROPTEST_F64, PROPTEST_MATRIX_DIM}; use na::{DMatrix, DVector}; - use std::cmp; + use proptest::{prop_assert, proptest}; - quickcheck! { + proptest! { /* * * Symmetric operators. * */ - fn gemv_symm(n: usize, alpha: f64, beta: f64) -> bool { - let n = cmp::max(1, cmp::min(n, 50)); + #[test] + fn gemv_symm(n in PROPTEST_MATRIX_DIM, alpha in PROPTEST_F64, beta in PROPTEST_F64) { let a = DMatrix::::new_random(n, n); let a = &a * a.transpose(); @@ -44,18 +45,16 @@ mod blas_quickcheck { y1.gemv(alpha, &a, &x, beta); y2.sygemv(alpha, &a.lower_triangle(), &x, beta); - if !relative_eq!(y1, y2, epsilon = 1.0e-10) { - return false; - } + prop_assert!(relative_eq!(y1, y2, epsilon = 1.0e-10)); y1.gemv(alpha, &a, &x, 0.0); y2.sygemv(alpha, &a.lower_triangle(), &x, 0.0); - relative_eq!(y1, y2, epsilon = 1.0e-10) + prop_assert!(relative_eq!(y1, y2, epsilon = 1.0e-10)) } - fn gemv_tr(n: usize, alpha: f64, beta: f64) -> bool { - let n = cmp::max(1, cmp::min(n, 50)); + #[test] + fn gemv_tr(n in PROPTEST_MATRIX_DIM, alpha in PROPTEST_F64, beta in PROPTEST_F64) { let a = DMatrix::::new_random(n, n); let x = DVector::new_random(n); let mut y1 = DVector::new_random(n); @@ -64,18 +63,16 @@ mod blas_quickcheck { y1.gemv(alpha, &a, &x, beta); y2.gemv_tr(alpha, &a.transpose(), &x, beta); - if !relative_eq!(y1, y2, epsilon = 1.0e-10) { - return false; - } + prop_assert!(relative_eq!(y1, y2, epsilon = 1.0e-10)); y1.gemv(alpha, &a, &x, 0.0); y2.gemv_tr(alpha, &a.transpose(), &x, 0.0); - relative_eq!(y1, y2, epsilon = 1.0e-10) + prop_assert!(relative_eq!(y1, y2, epsilon = 1.0e-10)) } - fn ger_symm(n: usize, alpha: f64, beta: f64) -> bool { - let n = cmp::max(1, cmp::min(n, 50)); + #[test] + fn ger_symm(n in PROPTEST_MATRIX_DIM, alpha in PROPTEST_F64, beta in PROPTEST_F64) { let a = DMatrix::::new_random(n, n); let mut a1 = &a * a.transpose(); let mut a2 = a1.lower_triangle(); @@ -86,18 +83,16 @@ mod blas_quickcheck { a1.ger(alpha, &x, &y, beta); a2.syger(alpha, &x, &y, beta); - if !relative_eq!(a1.lower_triangle(), a2) { - return false; - } + prop_assert!(relative_eq!(a1.lower_triangle(), a2)); a1.ger(alpha, &x, &y, 0.0); a2.syger(alpha, &x, &y, 0.0); - relative_eq!(a1.lower_triangle(), a2) + prop_assert!(relative_eq!(a1.lower_triangle(), a2)) } - fn quadform(n: usize, alpha: f64, beta: f64) -> bool { - let n = cmp::max(1, cmp::min(n, 50)); + #[test] + fn quadform(n in PROPTEST_MATRIX_DIM, alpha in PROPTEST_F64, beta in PROPTEST_F64) { let rhs = DMatrix::::new_random(6, n); let mid = DMatrix::::new_random(6, 6); let mut res = DMatrix::new_random(n, n); @@ -106,13 +101,11 @@ mod blas_quickcheck { res.quadform(alpha, &mid, &rhs, beta); - println!("{}{}", res, expected); - - relative_eq!(res, expected, epsilon = 1.0e-7) + prop_assert!(relative_eq!(res, expected, epsilon = 1.0e-7)) } - fn quadform_tr(n: usize, alpha: f64, beta: f64) -> bool { - let n = cmp::max(1, cmp::min(n, 50)); + #[test] + fn quadform_tr(n in PROPTEST_MATRIX_DIM, alpha in PROPTEST_F64, beta in PROPTEST_F64) { let lhs = DMatrix::::new_random(6, n); let mid = DMatrix::::new_random(n, n); let mut res = DMatrix::new_random(6, 6); @@ -121,9 +114,7 @@ mod blas_quickcheck { res.quadform_tr(alpha, &lhs, &mid , beta); - println!("{}{}", res, expected); - - relative_eq!(res, expected, epsilon = 1.0e-7) + prop_assert!(relative_eq!(res, expected, epsilon = 1.0e-7)) } } } diff --git a/tests/core/conversion.rs b/tests/core/conversion.rs index b7a8c5f8..93545004 100644 --- a/tests/core/conversion.rs +++ b/tests/core/conversion.rs @@ -1,44 +1,49 @@ -#![cfg(all(feature = "arbitrary", feature = "alga"))] +#![cfg(all(feature = "proptest-support", feature = "alga"))] use alga::linear::Transformation; use na::{ self, Affine3, Isometry3, Matrix2, Matrix2x3, Matrix2x4, Matrix2x5, Matrix2x6, Matrix3, Matrix3x2, Matrix3x4, Matrix3x5, Matrix3x6, Matrix4, Matrix4x2, Matrix4x3, Matrix4x5, Matrix4x6, Matrix5, Matrix5x2, Matrix5x3, Matrix5x4, Matrix5x6, Matrix6, Matrix6x2, Matrix6x3, - Matrix6x4, Matrix6x5, Point3, Projective3, Rotation3, RowVector1, RowVector2, RowVector3, - RowVector4, RowVector5, RowVector6, Similarity3, Transform3, Translation3, UnitQuaternion, - Vector1, Vector2, Vector3, Vector4, Vector5, Vector6, + Matrix6x4, Matrix6x5, Projective3, Rotation3, RowVector1, RowVector2, RowVector3, RowVector4, + RowVector5, RowVector6, Similarity3, Transform3, UnitQuaternion, Vector1, Vector2, Vector3, + Vector4, Vector5, Vector6, }; use na::{DMatrix, DMatrixSlice, DMatrixSliceMut, MatrixSlice, MatrixSliceMut}; use na::{U1, U3, U4}; -quickcheck! { - fn translation_conversion(t: Translation3, v: Vector3, p: Point3) -> bool { +use crate::proptest::*; +use proptest::{prop_assert, prop_assert_eq, proptest}; + +proptest! { + #[test] + fn translation_conversion(t in translation3(), v in vector3(), p in point3()) { let iso: Isometry3 = na::convert(t); let sim: Similarity3 = na::convert(t); let aff: Affine3 = na::convert(t); let prj: Projective3 = na::convert(t); let tr: Transform3 = na::convert(t); - t == na::try_convert(iso).unwrap() && - t == na::try_convert(sim).unwrap() && - t == na::try_convert(aff).unwrap() && - t == na::try_convert(prj).unwrap() && - t == na::try_convert(tr).unwrap() && + prop_assert_eq!(t, na::try_convert(iso).unwrap()); + prop_assert_eq!(t, na::try_convert(sim).unwrap()); + prop_assert_eq!(t, na::try_convert(aff).unwrap()); + prop_assert_eq!(t, na::try_convert(prj).unwrap()); + prop_assert_eq!(t, na::try_convert(tr).unwrap() ); - t.transform_vector(&v) == iso * v && - t.transform_vector(&v) == sim * v && - t.transform_vector(&v) == aff * v && - t.transform_vector(&v) == prj * v && - t.transform_vector(&v) == tr * v && + prop_assert_eq!(t.transform_vector(&v), iso * v); + prop_assert_eq!(t.transform_vector(&v), sim * v); + prop_assert_eq!(t.transform_vector(&v), aff * v); + prop_assert_eq!(t.transform_vector(&v), prj * v); + prop_assert_eq!(t.transform_vector(&v), tr * v); - t * p == iso * p && - t * p == sim * p && - t * p == aff * p && - t * p == prj * p && - t * p == tr * p + prop_assert_eq!(t * p, iso * p); + prop_assert_eq!(t * p, sim * p); + prop_assert_eq!(t * p, aff * p); + prop_assert_eq!(t * p, prj * p); + prop_assert_eq!(t * p, tr * p); } - fn rotation_conversion(r: Rotation3, v: Vector3, p: Point3) -> bool { + #[test] + fn rotation_conversion(r in rotation3(), v in vector3(), p in point3()) { let uq: UnitQuaternion = na::convert(r); let iso: Isometry3 = na::convert(r); let sim: Similarity3 = na::convert(r); @@ -46,30 +51,31 @@ quickcheck! { let prj: Projective3 = na::convert(r); let tr: Transform3 = na::convert(r); - relative_eq!(r, na::try_convert(uq).unwrap(), epsilon = 1.0e-7) && - relative_eq!(r, na::try_convert(iso).unwrap(), epsilon = 1.0e-7) && - relative_eq!(r, na::try_convert(sim).unwrap(), epsilon = 1.0e-7) && - r == na::try_convert(aff).unwrap() && - r == na::try_convert(prj).unwrap() && - r == na::try_convert(tr).unwrap() && + prop_assert!(relative_eq!(r, na::try_convert(uq).unwrap(), epsilon = 1.0e-7)); + prop_assert!(relative_eq!(r, na::try_convert(iso).unwrap(), epsilon = 1.0e-7)); + prop_assert!(relative_eq!(r, na::try_convert(sim).unwrap(), epsilon = 1.0e-7)); + prop_assert_eq!(r, na::try_convert(aff).unwrap()); + prop_assert_eq!(r, na::try_convert(prj).unwrap()); + prop_assert_eq!(r, na::try_convert(tr).unwrap() ); // NOTE: we need relative_eq because Isometry and Similarity use quaternions. - relative_eq!(r * v, uq * v, epsilon = 1.0e-7) && - relative_eq!(r * v, iso * v, epsilon = 1.0e-7) && - relative_eq!(r * v, sim * v, epsilon = 1.0e-7) && - r * v == aff * v && - r * v == prj * v && - r * v == tr * v && + prop_assert!(relative_eq!(r * v, uq * v, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(r * v, iso * v, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(r * v, sim * v, epsilon = 1.0e-7)); + prop_assert_eq!(r * v, aff * v); + prop_assert_eq!(r * v, prj * v); + prop_assert_eq!(r * v, tr * v); - relative_eq!(r * p, uq * p, epsilon = 1.0e-7) && - relative_eq!(r * p, iso * p, epsilon = 1.0e-7) && - relative_eq!(r * p, sim * p, epsilon = 1.0e-7) && - r * p == aff * p && - r * p == prj * p && - r * p == tr * p + prop_assert!(relative_eq!(r * p, uq * p, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(r * p, iso * p, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(r * p, sim * p, epsilon = 1.0e-7)); + prop_assert_eq!(r * p, aff * p); + prop_assert_eq!(r * p, prj * p); + prop_assert_eq!(r * p, tr * p); } - fn unit_quaternion_conversion(uq: UnitQuaternion, v: Vector3, p: Point3) -> bool { + #[test] + fn unit_quaternion_conversion(uq in unit_quaternion(), v in vector3(), p in point3()) { let rot: Rotation3 = na::convert(uq); let iso: Isometry3 = na::convert(uq); let sim: Similarity3 = na::convert(uq); @@ -77,68 +83,70 @@ quickcheck! { let prj: Projective3 = na::convert(uq); let tr: Transform3 = na::convert(uq); - uq == na::try_convert(iso).unwrap() && - uq == na::try_convert(sim).unwrap() && - relative_eq!(uq, na::try_convert(rot).unwrap(), epsilon = 1.0e-7) && - relative_eq!(uq, na::try_convert(aff).unwrap(), epsilon = 1.0e-7) && - relative_eq!(uq, na::try_convert(prj).unwrap(), epsilon = 1.0e-7) && - relative_eq!(uq, na::try_convert(tr).unwrap(), epsilon = 1.0e-7) && + prop_assert_eq!(uq, na::try_convert(iso).unwrap()); + prop_assert_eq!(uq, na::try_convert(sim).unwrap()); + prop_assert!(relative_eq!(uq, na::try_convert(rot).unwrap(), epsilon = 1.0e-7)); + prop_assert!(relative_eq!(uq, na::try_convert(aff).unwrap(), epsilon = 1.0e-7)); + prop_assert!(relative_eq!(uq, na::try_convert(prj).unwrap(), epsilon = 1.0e-7)); + prop_assert!(relative_eq!(uq, na::try_convert(tr).unwrap(), epsilon = 1.0e-7) ); // NOTE: iso and sim use unit quaternions for the rotation so conversions to them are exact. - relative_eq!(uq * v, rot * v, epsilon = 1.0e-7) && - uq * v == iso * v && - uq * v == sim * v && - relative_eq!(uq * v, aff * v, epsilon = 1.0e-7) && - relative_eq!(uq * v, prj * v, epsilon = 1.0e-7) && - relative_eq!(uq * v, tr * v, epsilon = 1.0e-7) && + prop_assert!(relative_eq!(uq * v, rot * v, epsilon = 1.0e-7)); + prop_assert_eq!(uq * v, iso * v); + prop_assert_eq!(uq * v, sim * v); + prop_assert!(relative_eq!(uq * v, aff * v, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(uq * v, prj * v, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(uq * v, tr * v, epsilon = 1.0e-7)); - relative_eq!(uq * p, rot * p, epsilon = 1.0e-7) && - uq * p == iso * p && - uq * p == sim * p && - relative_eq!(uq * p, aff * p, epsilon = 1.0e-7) && - relative_eq!(uq * p, prj * p, epsilon = 1.0e-7) && - relative_eq!(uq * p, tr * p, epsilon = 1.0e-7) + prop_assert!(relative_eq!(uq * p, rot * p, epsilon = 1.0e-7)); + prop_assert_eq!(uq * p, iso * p); + prop_assert_eq!(uq * p, sim * p); + prop_assert!(relative_eq!(uq * p, aff * p, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(uq * p, prj * p, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(uq * p, tr * p, epsilon = 1.0e-7)); } - fn isometry_conversion(iso: Isometry3, v: Vector3, p: Point3) -> bool { + #[test] + fn isometry_conversion(iso in isometry3(), v in vector3(), p in point3()) { let sim: Similarity3 = na::convert(iso); let aff: Affine3 = na::convert(iso); let prj: Projective3 = na::convert(iso); let tr: Transform3 = na::convert(iso); - iso == na::try_convert(sim).unwrap() && - relative_eq!(iso, na::try_convert(aff).unwrap(), epsilon = 1.0e-7) && - relative_eq!(iso, na::try_convert(prj).unwrap(), epsilon = 1.0e-7) && - relative_eq!(iso, na::try_convert(tr).unwrap(), epsilon = 1.0e-7) && + prop_assert_eq!(iso, na::try_convert(sim).unwrap()); + prop_assert!(relative_eq!(iso, na::try_convert(aff).unwrap(), epsilon = 1.0e-7)); + prop_assert!(relative_eq!(iso, na::try_convert(prj).unwrap(), epsilon = 1.0e-7)); + prop_assert!(relative_eq!(iso, na::try_convert(tr).unwrap(), epsilon = 1.0e-7) ); - iso * v == sim * v && - relative_eq!(iso * v, aff * v, epsilon = 1.0e-7) && - relative_eq!(iso * v, prj * v, epsilon = 1.0e-7) && - relative_eq!(iso * v, tr * v, epsilon = 1.0e-7) && + prop_assert_eq!(iso * v, sim * v); + prop_assert!(relative_eq!(iso * v, aff * v, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(iso * v, prj * v, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(iso * v, tr * v, epsilon = 1.0e-7)); - iso * p == sim * p && - relative_eq!(iso * p, aff * p, epsilon = 1.0e-7) && - relative_eq!(iso * p, prj * p, epsilon = 1.0e-7) && - relative_eq!(iso * p, tr * p, epsilon = 1.0e-7) + prop_assert_eq!(iso * p, sim * p); + prop_assert!(relative_eq!(iso * p, aff * p, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(iso * p, prj * p, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(iso * p, tr * p, epsilon = 1.0e-7)); } - fn similarity_conversion(sim: Similarity3, v: Vector3, p: Point3) -> bool { + #[test] + fn similarity_conversion(sim in similarity3(), v in vector3(), p in point3()) { let aff: Affine3 = na::convert(sim); let prj: Projective3 = na::convert(sim); let tr: Transform3 = na::convert(sim); - relative_eq!(sim, na::try_convert(aff).unwrap(), epsilon = 1.0e-7) && - relative_eq!(sim, na::try_convert(prj).unwrap(), epsilon = 1.0e-7) && - relative_eq!(sim, na::try_convert(tr).unwrap(), epsilon = 1.0e-7) && + prop_assert!(relative_eq!(sim, na::try_convert(aff).unwrap(), epsilon = 1.0e-7)); + prop_assert!(relative_eq!(sim, na::try_convert(prj).unwrap(), epsilon = 1.0e-7)); + prop_assert!(relative_eq!(sim, na::try_convert(tr).unwrap(), epsilon = 1.0e-7)); - relative_eq!(sim * v, aff * v, epsilon = 1.0e-7) && - relative_eq!(sim * v, prj * v, epsilon = 1.0e-7) && - relative_eq!(sim * v, tr * v, epsilon = 1.0e-7) && + prop_assert!(relative_eq!(sim * v, aff * v, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(sim * v, prj * v, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(sim * v, tr * v, epsilon = 1.0e-7)); - relative_eq!(sim * p, aff * p, epsilon = 1.0e-7) && - relative_eq!(sim * p, prj * p, epsilon = 1.0e-7) && - relative_eq!(sim * p, tr * p, epsilon = 1.0e-7) + prop_assert!(relative_eq!(sim * p, aff * p, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(sim * p, prj * p, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(sim * p, tr * p, epsilon = 1.0e-7)); } // XXX test Transform diff --git a/tests/core/helper.rs b/tests/core/helper.rs index 42938580..ef749da6 100644 --- a/tests/core/helper.rs +++ b/tests/core/helper.rs @@ -12,7 +12,7 @@ pub struct RandComplex(pub Complex); impl Arbitrary for RandComplex { #[inline] - fn arbitrary(rng: &mut G) -> Self { + fn arbitrary(rng: &mut Gen) -> Self { let im = Arbitrary::arbitrary(rng); let re = Arbitrary::arbitrary(rng); RandComplex(Complex::new(re, im)) @@ -38,7 +38,7 @@ pub struct RandScalar(pub N); impl Arbitrary for RandScalar { #[inline] - fn arbitrary(rng: &mut G) -> Self { + fn arbitrary(rng: &mut Gen) -> Self { RandScalar(Arbitrary::arbitrary(rng)) } } diff --git a/tests/core/matrix.rs b/tests/core/matrix.rs index 14eaeacd..daa8b72f 100644 --- a/tests/core/matrix.rs +++ b/tests/core/matrix.rs @@ -829,151 +829,145 @@ fn swizzle() { assert_eq!(c.zyz(), Vector3::new(3.0, 2.0, 3.0)); } -#[cfg(feature = "arbitrary")] +#[cfg(feature = "proptest-support")] mod transposition_tests { use super::*; - use na::Matrix4x6; + use crate::proptest::{dmatrix, matrix, vector4, PROPTEST_F64}; + use na::{U2, U3, U4, U6}; + use proptest::{prop_assert, prop_assert_eq, proptest}; - quickcheck! { - fn transpose_transpose_is_self(m: Matrix2x3) -> bool { - m.transpose().transpose() == m + proptest! { + #[test] + fn transpose_transpose_is_self(m in matrix(PROPTEST_F64, U2, U3)) { + prop_assert_eq!(m.transpose().transpose(), m) } - fn transpose_mut_transpose_mut_is_self(m: Matrix3) -> bool { + #[test] + fn transpose_mut_transpose_mut_is_self(m in matrix(PROPTEST_F64, U3, U3)) { let mut mm = m; mm.transpose_mut(); mm.transpose_mut(); - m == mm + prop_assert_eq!(m, mm) } - fn transpose_transpose_is_id_dyn(m: DMatrix) -> bool { - m.transpose().transpose() == m + #[test] + fn transpose_transpose_is_id_dyn(m in dmatrix()) { + prop_assert_eq!(m.transpose().transpose(), m) } - fn check_transpose_components_dyn(m: DMatrix) -> bool { + #[test] + fn check_transpose_components_dyn(m in dmatrix()) { let tr = m.transpose(); let (nrows, ncols) = m.shape(); - if nrows != tr.shape().1 || ncols != tr.shape().0 { - return false - } + prop_assert!(nrows == tr.shape().1 && ncols == tr.shape().0); for i in 0 .. nrows { for j in 0 .. ncols { - if m[(i, j)] != tr[(j, i)] { - return false - } + prop_assert_eq!(m[(i, j)], tr[(j, i)]); } } - - true } - fn tr_mul_is_transpose_then_mul(m: Matrix4x6, v: Vector4) -> bool { - relative_eq!(m.transpose() * v, m.tr_mul(&v), epsilon = 1.0e-7) + #[test] + fn tr_mul_is_transpose_then_mul(m in matrix(PROPTEST_F64, U4, U6), v in vector4()) { + prop_assert!(relative_eq!(m.transpose() * v, m.tr_mul(&v), epsilon = 1.0e-7)) } } } -#[cfg(feature = "arbitrary")] +#[cfg(feature = "proptest-support")] mod inversion_tests { use super::*; + use crate::proptest::*; use na::Matrix1; + use proptest::{prop_assert, proptest}; - quickcheck! { - fn self_mul_inv_is_id_dim1(m: Matrix1) -> bool { + proptest! { + #[test] + fn self_mul_inv_is_id_dim1(m in matrix1()) { if let Some(im) = m.try_inverse() { let id = Matrix1::one(); - relative_eq!(im * m, id, epsilon = 1.0e-7) && - relative_eq!(m * im, id, epsilon = 1.0e-7) - } - else { - true + prop_assert!(relative_eq!(im * m, id, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(m * im, id, epsilon = 1.0e-7)); } } - fn self_mul_inv_is_id_dim2(m: Matrix2) -> bool { + #[test] + fn self_mul_inv_is_id_dim2(m in matrix2()) { if let Some(im) = m.try_inverse() { let id = Matrix2::one(); - relative_eq!(im * m, id, epsilon = 1.0e-7) && - relative_eq!(m * im, id, epsilon = 1.0e-7) - } - else { - true + prop_assert!(relative_eq!(im * m, id, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(m * im, id, epsilon = 1.0e-7)); } } - fn self_mul_inv_is_id_dim3(m: Matrix3) -> bool { + #[test] + fn self_mul_inv_is_id_dim3(m in matrix3()) { if let Some(im) = m.try_inverse() { let id = Matrix3::one(); - relative_eq!(im * m, id, epsilon = 1.0e-7) && - relative_eq!(m * im, id, epsilon = 1.0e-7) - } - else { - true + prop_assert!(relative_eq!(im * m, id, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(m * im, id, epsilon = 1.0e-7)); } } - fn self_mul_inv_is_id_dim4(m: Matrix4) -> bool { + #[test] + fn self_mul_inv_is_id_dim4(m in matrix4()) { if let Some(im) = m.try_inverse() { let id = Matrix4::one(); - relative_eq!(im * m, id, epsilon = 1.0e-7) && - relative_eq!(m * im, id, epsilon = 1.0e-7) - } - else { - true + prop_assert!(relative_eq!(im * m, id, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(m * im, id, epsilon = 1.0e-7)); } } - fn self_mul_inv_is_id_dim6(m: Matrix6) -> bool { + #[test] + fn self_mul_inv_is_id_dim6(m in matrix6()) { if let Some(im) = m.try_inverse() { let id = Matrix6::one(); - relative_eq!(im * m, id, epsilon = 1.0e-7) && - relative_eq!(m * im, id, epsilon = 1.0e-7) - } - else { - true + prop_assert!(relative_eq!(im * m, id, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(m * im, id, epsilon = 1.0e-7)); } } } } -#[cfg(feature = "arbitrary")] +#[cfg(feature = "proptest-support")] mod normalization_tests { - use super::*; + use crate::proptest::*; + use proptest::{prop_assert, proptest}; - quickcheck! { - fn normalized_vec_norm_is_one(v: Vector3) -> bool { + proptest! { + #[test] + fn normalized_vec_norm_is_one(v in vector3()) { if let Some(nv) = v.try_normalize(1.0e-10) { - relative_eq!(nv.norm(), 1.0, epsilon = 1.0e-7) - } - else { - true + prop_assert!(relative_eq!(nv.norm(), 1.0, epsilon = 1.0e-7)); } } - fn normalized_vec_norm_is_one_dyn(v: DVector) -> bool { + #[test] + fn normalized_vec_norm_is_one_dyn(v in dvector()) { if let Some(nv) = v.try_normalize(1.0e-10) { - relative_eq!(nv.norm(), 1.0, epsilon = 1.0e-7) - } - else { - true + prop_assert!(relative_eq!(nv.norm(), 1.0, epsilon = 1.0e-7)); } } } } -#[cfg(all(feature = "arbitrary", feature = "alga"))] -// TODO: move this to alga ? +#[cfg(all(feature = "proptest-support", feature = "alga"))] +// TODO: move this to alga ? mod finite_dim_inner_space_tests { use super::*; + use crate::proptest::*; use alga::linear::FiniteDimInnerSpace; + use proptest::collection::vec; + use proptest::{prop_assert, proptest}; use std::fmt::Display; macro_rules! finite_dim_inner_space_test( - ($($Vector: ident, $orthonormal_subspace: ident, $orthonormalization: ident);* $(;)*) => {$( - quickcheck!{ - fn $orthonormal_subspace(vs: Vec<$Vector>) -> bool { + ($($Vector: ident, $vstrategy: ident, $orthonormal_subspace: ident, $orthonormalization: ident);* $(;)*) => {$( + proptest! { + #[test] + fn $orthonormal_subspace(vs in vec($vstrategy(), 0..10)) { let mut given_basis = vs.clone(); let given_basis_dim = $Vector::orthonormalize(&mut given_basis[..]); let mut ortho_basis = Vec::new(); @@ -982,29 +976,21 @@ mod finite_dim_inner_space_tests { |e| { ortho_basis.push(*e); true } ); - if !is_subspace_basis(&ortho_basis[..]) { - return false; - } + prop_assert!(is_subspace_basis(&ortho_basis[..])); for v in vs { for b in &ortho_basis { - if !relative_eq!(v.dot(b), 0.0, epsilon = 1.0e-7) { - println!("Found dot product: {} · {} = {}", v, b, v.dot(b)); - return false; - } + prop_assert!(relative_eq!(v.dot(b), 0.0, epsilon = 1.0e-7)); } } - - true } - fn $orthonormalization(vs: Vec<$Vector>) -> bool { + #[test] + fn $orthonormalization(vs in vec($vstrategy(), 0..10)) { let mut basis = vs.clone(); let subdim = $Vector::orthonormalize(&mut basis[..]); - if !is_subspace_basis(&basis[.. subdim]) { - return false; - } + prop_assert!(is_subspace_basis(&basis[.. subdim])); for mut e in vs { for b in &basis[.. subdim] { @@ -1012,26 +998,20 @@ mod finite_dim_inner_space_tests { } // Any element of `e` must be a linear combination of the basis elements. - if !relative_eq!(e.norm(), 0.0, epsilon = 1.0e-7) { - println!("Orthonormalization; element decomposition failure: {}", e); - println!("... the non-zero norm is: {}", e.norm()); - return false; - } + prop_assert!(relative_eq!(e.norm(), 0.0, epsilon = 1.0e-7)); } - - true } } )*} ); finite_dim_inner_space_test!( - Vector1, orthonormal_subspace_basis1, orthonormalize1; - Vector2, orthonormal_subspace_basis2, orthonormalize2; - Vector3, orthonormal_subspace_basis3, orthonormalize3; - Vector4, orthonormal_subspace_basis4, orthonormalize4; - Vector5, orthonormal_subspace_basis5, orthonormalize5; - Vector6, orthonormal_subspace_basis6, orthonormalize6; + Vector1, vector1, orthonormal_subspace_basis1, orthonormalize1; + Vector2, vector2, orthonormal_subspace_basis2, orthonormalize2; + Vector3, vector3, orthonormal_subspace_basis3, orthonormalize3; + Vector4, vector4, orthonormal_subspace_basis4, orthonormalize4; + Vector5, vector5, orthonormal_subspace_basis5, orthonormalize5; + Vector6, vector6, orthonormal_subspace_basis6, orthonormalize6; ); /* @@ -1039,7 +1019,6 @@ mod finite_dim_inner_space_tests { * Helper functions. * */ - #[cfg(feature = "arbitrary")] fn is_subspace_basis + Display>( vs: &[T], ) -> bool { diff --git a/tests/core/matrixcompare.rs b/tests/core/matrixcompare.rs index cdd93ea3..ab3ecc2c 100644 --- a/tests/core/matrixcompare.rs +++ b/tests/core/matrixcompare.rs @@ -4,34 +4,32 @@ //! The tests here only check that the necessary trait implementations are correctly implemented, //! in addition to some sanity checks with example input. +use matrixcompare::assert_matrix_eq; use nalgebra::{MatrixMN, U4, U5}; -#[cfg(feature = "arbitrary")] -use nalgebra::DMatrix; +#[cfg(feature = "proptest-support")] +use { + crate::proptest::*, + matrixcompare::DenseAccess, + nalgebra::DMatrix, + proptest::{prop_assert_eq, proptest}, +}; -use matrixcompare::assert_matrix_eq; - -#[cfg(feature = "arbitrary")] -use matrixcompare::DenseAccess; - -#[cfg(feature = "arbitrary")] -quickcheck! { - fn fetch_single_is_equivalent_to_index_f64(matrix: DMatrix) -> bool { +#[cfg(feature = "proptest-support")] +proptest! { + #[test] + fn fetch_single_is_equivalent_to_index_f64(matrix in dmatrix()) { for i in 0 .. matrix.nrows() { for j in 0 .. matrix.ncols() { - if matrix.fetch_single(i, j) != *matrix.index((i, j)) { - return false; - } + prop_assert_eq!(matrix.fetch_single(i, j), *matrix.index((i, j))); } } - - true } - fn matrixcompare_shape_agrees_with_matrix(matrix: DMatrix) -> bool { - matrix.nrows() == as matrixcompare::Matrix>::rows(&matrix) - && - matrix.ncols() == as matrixcompare::Matrix>::cols(&matrix) + #[test] + fn matrixcompare_shape_agrees_with_matrix(matrix in dmatrix()) { + prop_assert_eq!(matrix.nrows(), as matrixcompare::Matrix>::rows(&matrix)); + prop_assert_eq!(matrix.ncols(), as matrixcompare::Matrix>::cols(&matrix)); } } diff --git a/tests/geometry/dual_quaternion.rs b/tests/geometry/dual_quaternion.rs index 2884f7f4..6cc975a5 100644 --- a/tests/geometry/dual_quaternion.rs +++ b/tests/geometry/dual_quaternion.rs @@ -1,36 +1,40 @@ -#![cfg(feature = "arbitrary")] +#![cfg(feature = "proptest-support")] #![allow(non_snake_case)] -use na::{ - DualQuaternion, Isometry3, Point3, Translation3, UnitDualQuaternion, UnitQuaternion, Vector3, -}; +use na::{DualQuaternion, Point3, UnitDualQuaternion, Vector3}; -quickcheck!( - fn isometry_equivalence(iso: Isometry3, p: Point3, v: Vector3) -> bool { +use crate::proptest::*; +use proptest::{prop_assert, proptest}; + +proptest!( + #[test] + fn isometry_equivalence(iso in isometry3(), p in point3(), v in vector3()) { let dq = UnitDualQuaternion::from_isometry(&iso); - relative_eq!(iso * p, dq * p, epsilon = 1.0e-7) - && relative_eq!(iso * v, dq * v, epsilon = 1.0e-7) + prop_assert!(relative_eq!(iso * p, dq * p, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(iso * v, dq * v, epsilon = 1.0e-7)); } - fn inverse_is_identity(i: UnitDualQuaternion, p: Point3, v: Vector3) -> bool { + #[test] + fn inverse_is_identity(i in unit_dual_quaternion(), p in point3(), v in vector3()) { let ii = i.inverse(); - relative_eq!(i * ii, UnitDualQuaternion::identity(), epsilon = 1.0e-7) + prop_assert!(relative_eq!(i * ii, UnitDualQuaternion::identity(), epsilon = 1.0e-7) && relative_eq!(ii * i, UnitDualQuaternion::identity(), epsilon = 1.0e-7) && relative_eq!((i * ii) * p, p, epsilon = 1.0e-7) && relative_eq!((ii * i) * p, p, epsilon = 1.0e-7) && relative_eq!((i * ii) * v, v, epsilon = 1.0e-7) - && relative_eq!((ii * i) * v, v, epsilon = 1.0e-7) + && relative_eq!((ii * i) * v, v, epsilon = 1.0e-7)); } #[cfg_attr(rustfmt, rustfmt_skip)] + #[test] fn multiply_equals_alga_transform( - dq: UnitDualQuaternion, - v: Vector3, - p: Point3 - ) -> bool { - dq * v == dq.transform_vector(&v) + dq in unit_dual_quaternion(), + v in vector3(), + p in point3() + ) { + prop_assert!(dq * v == dq.transform_vector(&v) && dq * p == dq.transform_point(&p) && relative_eq!( dq.inverse() * v, @@ -41,44 +45,46 @@ quickcheck!( dq.inverse() * p, dq.inverse_transform_point(&p), epsilon = 1.0e-7 - ) + )); } #[cfg_attr(rustfmt, rustfmt_skip)] + #[test] fn composition( - dq: UnitDualQuaternion, - uq: UnitQuaternion, - t: Translation3, - v: Vector3, - p: Point3 - ) -> bool { + dq in unit_dual_quaternion(), + uq in unit_quaternion(), + t in translation3(), + v in vector3(), + p in point3() + ) { // (rotation × dual quaternion) * point = rotation × (dual quaternion * point) - relative_eq!((uq * dq) * v, uq * (dq * v), epsilon = 1.0e-7) && - relative_eq!((uq * dq) * p, uq * (dq * p), epsilon = 1.0e-7) && + prop_assert!(relative_eq!((uq * dq) * v, uq * (dq * v), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((uq * dq) * p, uq * (dq * p), epsilon = 1.0e-7)); // (dual quaternion × rotation) * point = dual quaternion × (rotation * point) - relative_eq!((dq * uq) * v, dq * (uq * v), epsilon = 1.0e-7) && - relative_eq!((dq * uq) * p, dq * (uq * p), epsilon = 1.0e-7) && + prop_assert!(relative_eq!((dq * uq) * v, dq * (uq * v), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((dq * uq) * p, dq * (uq * p), epsilon = 1.0e-7)); // (translation × dual quaternion) * point = translation × (dual quaternion * point) - relative_eq!((t * dq) * v, (dq * v), epsilon = 1.0e-7) && - relative_eq!((t * dq) * p, t * (dq * p), epsilon = 1.0e-7) && + prop_assert!(relative_eq!((t * dq) * v, (dq * v), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((t * dq) * p, t * (dq * p), epsilon = 1.0e-7)); // (dual quaternion × translation) * point = dual quaternion × (translation * point) - relative_eq!((dq * t) * v, dq * v, epsilon = 1.0e-7) && - relative_eq!((dq * t) * p, dq * (t * p), epsilon = 1.0e-7) + prop_assert!(relative_eq!((dq * t) * v, dq * v, epsilon = 1.0e-7)); + prop_assert!(relative_eq!((dq * t) * p, dq * (t * p), epsilon = 1.0e-7)); } #[cfg_attr(rustfmt, rustfmt_skip)] + #[test] fn all_op_exist( - dq: DualQuaternion, - udq: UnitDualQuaternion, - uq: UnitQuaternion, - s: f64, - t: Translation3, - v: Vector3, - p: Point3 - ) -> bool { + dq in dual_quaternion(), + udq in unit_dual_quaternion(), + uq in unit_quaternion(), + s in PROPTEST_F64, + t in translation3(), + v in vector3(), + p in point3() + ) { let dqMs: DualQuaternion<_> = dq * s; let dqMdq: DualQuaternion<_> = dq * dq; @@ -145,7 +151,7 @@ quickcheck!( iDuq1 /= uq; iDuq2 /= &uq; - dqMs == dqMs1 + prop_assert!(dqMs == dqMs1 && dqMdq == dqMdq1 && dqMdq == dqMdq2 && dqMudq == dqMudq1 @@ -199,6 +205,6 @@ quickcheck!( && uqMi == &uq * udq && uqDi == &uq / &udq && uqDi == uq / &udq - && uqDi == &uq / udq + && uqDi == &uq / udq) } ); diff --git a/tests/geometry/isometry.rs b/tests/geometry/isometry.rs index 6d48c6bf..cfacaffd 100644 --- a/tests/geometry/isometry.rs +++ b/tests/geometry/isometry.rs @@ -1,67 +1,74 @@ -#![cfg(feature = "arbitrary")] +#![cfg(feature = "proptest-support")] #![allow(non_snake_case)] -use na::{ - Isometry2, Isometry3, Point2, Point3, Rotation2, Rotation3, Translation2, Translation3, - UnitComplex, UnitQuaternion, Vector2, Vector3, -}; +use na::{Isometry3, Point3, Vector3}; -quickcheck!( - fn append_rotation_wrt_point_to_id(r: UnitQuaternion, p: Point3) -> bool { +use crate::proptest::*; +use proptest::{prop_assert, prop_assert_eq, proptest}; + +proptest!( + #[test] + fn append_rotation_wrt_point_to_id(r in unit_quaternion(), p in point3()) { let mut iso = Isometry3::identity(); iso.append_rotation_wrt_point_mut(&r, &p); - iso == Isometry3::rotation_wrt_point(r, p) + prop_assert_eq!(iso, Isometry3::rotation_wrt_point(r, p)) } - fn rotation_wrt_point_invariance(r: UnitQuaternion, p: Point3) -> bool { + #[test] + fn rotation_wrt_point_invariance(r in unit_quaternion(), p in point3()) { let iso = Isometry3::rotation_wrt_point(r, p); - relative_eq!(iso * p, p, epsilon = 1.0e-7) + prop_assert!(relative_eq!(iso * p, p, epsilon = 1.0e-7)) } - fn look_at_rh_3(eye: Point3, target: Point3, up: Vector3) -> bool { + #[test] + fn look_at_rh_3(eye in point3(), target in point3(), up in vector3()) { let viewmatrix = Isometry3::look_at_rh(&eye, &target, &up); - let origin = Point3::origin(); - relative_eq!(viewmatrix * eye, origin, epsilon = 1.0e-7) + + prop_assert!(relative_eq!(viewmatrix * eye, origin, epsilon = 1.0e-7) && relative_eq!( (viewmatrix * (target - eye)).normalize(), -Vector3::z(), epsilon = 1.0e-7 - ) + )) } - fn observer_frame_3(eye: Point3, target: Point3, up: Vector3) -> bool { + #[test] + fn observer_frame_3(eye in point3(), target in point3(), up in vector3()) { let observer = Isometry3::face_towards(&eye, &target, &up); - let origin = Point3::origin(); - relative_eq!(observer * origin, eye, epsilon = 1.0e-7) + + prop_assert!(relative_eq!(observer * origin, eye, epsilon = 1.0e-7) && relative_eq!( observer * Vector3::z(), (target - eye).normalize(), epsilon = 1.0e-7 - ) + )) } - fn inverse_is_identity(i: Isometry3, p: Point3, v: Vector3) -> bool { + #[test] + fn inverse_is_identity(i in isometry3(), p in point3(), v in vector3()) { let ii = i.inverse(); - relative_eq!(i * ii, Isometry3::identity(), epsilon = 1.0e-7) + prop_assert!(relative_eq!(i * ii, Isometry3::identity(), epsilon = 1.0e-7) && relative_eq!(ii * i, Isometry3::identity(), epsilon = 1.0e-7) && relative_eq!((i * ii) * p, p, epsilon = 1.0e-7) && relative_eq!((ii * i) * p, p, epsilon = 1.0e-7) && relative_eq!((i * ii) * v, v, epsilon = 1.0e-7) - && relative_eq!((ii * i) * v, v, epsilon = 1.0e-7) + && relative_eq!((ii * i) * v, v, epsilon = 1.0e-7)) } - fn inverse_is_parts_inversion(t: Translation3, r: UnitQuaternion) -> bool { + #[test] + fn inverse_is_parts_inversion(t in translation3(), r in unit_quaternion()) { let i = t * r; - i.inverse() == r.inverse() * t.inverse() + prop_assert!(i.inverse() == r.inverse() * t.inverse()) } - fn multiply_equals_alga_transform(i: Isometry3, v: Vector3, p: Point3) -> bool { - i * v == i.transform_vector(&v) + #[test] + fn multiply_equals_alga_transform(i in isometry3(), v in vector3(), p in point3()) { + prop_assert!(i * v == i.transform_vector(&v) && i * p == i.transform_point(&p) && relative_eq!( i.inverse() * v, @@ -72,94 +79,97 @@ quickcheck!( i.inverse() * p, i.inverse_transform_point(&p), epsilon = 1.0e-7 - ) + )) } + #[test] #[cfg_attr(rustfmt, rustfmt_skip)] fn composition2( - i: Isometry2, - uc: UnitComplex, - r: Rotation2, - t: Translation2, - v: Vector2, - p: Point2 - ) -> bool { + i in isometry2(), + uc in unit_complex(), + r in rotation2(), + t in translation2(), + v in vector2(), + p in point2() + ) { // (rotation × translation) * point = rotation × (translation * point) - relative_eq!((uc * t) * v, uc * v, epsilon = 1.0e-7) && - relative_eq!((r * t) * v, r * v, epsilon = 1.0e-7) && - relative_eq!((uc * t) * p, uc * (t * p), epsilon = 1.0e-7) && - relative_eq!((r * t) * p, r * (t * p), epsilon = 1.0e-7) && + prop_assert!(relative_eq!((uc * t) * v, uc * v, epsilon = 1.0e-7)); + prop_assert!(relative_eq!((r * t) * v, r * v, epsilon = 1.0e-7)); + prop_assert!(relative_eq!((uc * t) * p, uc * (t * p), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((r * t) * p, r * (t * p), epsilon = 1.0e-7)); // (translation × rotation) * point = translation × (rotation * point) - (t * uc) * v == uc * v && - (t * r) * v == r * v && - (t * uc) * p == t * (uc * p) && - (t * r) * p == t * (r * p) && + prop_assert_eq!((t * uc) * v, uc * v); + prop_assert_eq!((t * r) * v, r * v); + prop_assert_eq!((t * uc) * p, t * (uc * p)); + prop_assert_eq!((t * r) * p, t * (r * p)); // (rotation × isometry) * point = rotation × (isometry * point) - relative_eq!((uc * i) * v, uc * (i * v), epsilon = 1.0e-7) && - relative_eq!((uc * i) * p, uc * (i * p), epsilon = 1.0e-7) && + prop_assert!(relative_eq!((uc * i) * v, uc * (i * v), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((uc * i) * p, uc * (i * p), epsilon = 1.0e-7)); // (isometry × rotation) * point = isometry × (rotation * point) - relative_eq!((i * uc) * v, i * (uc * v), epsilon = 1.0e-7) && - relative_eq!((i * uc) * p, i * (uc * p), epsilon = 1.0e-7) && + prop_assert!(relative_eq!((i * uc) * v, i * (uc * v), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((i * uc) * p, i * (uc * p), epsilon = 1.0e-7)); // (translation × isometry) * point = translation × (isometry * point) - relative_eq!((t * i) * v, (i * v), epsilon = 1.0e-7) && - relative_eq!((t * i) * p, t * (i * p), epsilon = 1.0e-7) && + prop_assert!(relative_eq!((t * i) * v, (i * v), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((t * i) * p, t * (i * p), epsilon = 1.0e-7)); // (isometry × translation) * point = isometry × (translation * point) - relative_eq!((i * t) * v, i * v, epsilon = 1.0e-7) && - relative_eq!((i * t) * p, i * (t * p), epsilon = 1.0e-7) + prop_assert!(relative_eq!((i * t) * v, i * v, epsilon = 1.0e-7)); + prop_assert!(relative_eq!((i * t) * p, i * (t * p), epsilon = 1.0e-7)); } + #[test] #[cfg_attr(rustfmt, rustfmt_skip)] fn composition3( - i: Isometry3, - uq: UnitQuaternion, - r: Rotation3, - t: Translation3, - v: Vector3, - p: Point3 - ) -> bool { + i in isometry3(), + uq in unit_quaternion(), + r in rotation3(), + t in translation3(), + v in vector3(), + p in point3() + ) { // (rotation × translation) * point = rotation × (translation * point) - relative_eq!((uq * t) * v, uq * v, epsilon = 1.0e-7) && - relative_eq!((r * t) * v, r * v, epsilon = 1.0e-7) && - relative_eq!((uq * t) * p, uq * (t * p), epsilon = 1.0e-7) && - relative_eq!((r * t) * p, r * (t * p), epsilon = 1.0e-7) && + prop_assert!(relative_eq!((uq * t) * v, uq * v, epsilon = 1.0e-7)); + prop_assert!(relative_eq!((r * t) * v, r * v, epsilon = 1.0e-7)); + prop_assert!(relative_eq!((uq * t) * p, uq * (t * p), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((r * t) * p, r * (t * p), epsilon = 1.0e-7)); // (translation × rotation) * point = translation × (rotation * point) - (t * uq) * v == uq * v && - (t * r) * v == r * v && - (t * uq) * p == t * (uq * p) && - (t * r) * p == t * (r * p) && + prop_assert_eq!((t * uq) * v, uq * v); + prop_assert_eq!((t * r) * v, r * v); + prop_assert_eq!((t * uq) * p, t * (uq * p)); + prop_assert_eq!((t * r) * p, t * (r * p)); // (rotation × isometry) * point = rotation × (isometry * point) - relative_eq!((uq * i) * v, uq * (i * v), epsilon = 1.0e-7) && - relative_eq!((uq * i) * p, uq * (i * p), epsilon = 1.0e-7) && + prop_assert!(relative_eq!((uq * i) * v, uq * (i * v), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((uq * i) * p, uq * (i * p), epsilon = 1.0e-7)); // (isometry × rotation) * point = isometry × (rotation * point) - relative_eq!((i * uq) * v, i * (uq * v), epsilon = 1.0e-7) && - relative_eq!((i * uq) * p, i * (uq * p), epsilon = 1.0e-7) && + prop_assert!(relative_eq!((i * uq) * v, i * (uq * v), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((i * uq) * p, i * (uq * p), epsilon = 1.0e-7)); // (translation × isometry) * point = translation × (isometry * point) - relative_eq!((t * i) * v, (i * v), epsilon = 1.0e-7) && - relative_eq!((t * i) * p, t * (i * p), epsilon = 1.0e-7) && + prop_assert!(relative_eq!((t * i) * v, (i * v), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((t * i) * p, t * (i * p), epsilon = 1.0e-7)); // (isometry × translation) * point = isometry × (translation * point) - relative_eq!((i * t) * v, i * v, epsilon = 1.0e-7) && - relative_eq!((i * t) * p, i * (t * p), epsilon = 1.0e-7) + prop_assert!(relative_eq!((i * t) * v, i * v, epsilon = 1.0e-7)); + prop_assert!(relative_eq!((i * t) * p, i * (t * p), epsilon = 1.0e-7)); } + #[test] #[cfg_attr(rustfmt, rustfmt_skip)] fn all_op_exist( - i: Isometry3, - uq: UnitQuaternion, - t: Translation3, - v: Vector3, - p: Point3, - r: Rotation3 - ) -> bool { + i in isometry3(), + uq in unit_quaternion(), + t in translation3(), + v in vector3(), + p in point3(), + r in rotation3() + ) { let iMi = i * i; let iMuq = i * uq; let iDi = i / i; @@ -210,7 +220,7 @@ quickcheck!( iDuq1 /= uq; iDuq2 /= &uq; - iMt == iMt1 + prop_assert!(iMt == iMt1 && iMt == iMt2 && iMi == iMi1 && iMi == iMi2 @@ -261,6 +271,6 @@ quickcheck!( && rMt == &r * t && uqMt == &uq * &t && uqMt == uq * &t - && uqMt == &uq * t + && uqMt == &uq * t) } ); diff --git a/tests/geometry/point.rs b/tests/geometry/point.rs index 896a09d6..22b0f598 100644 --- a/tests/geometry/point.rs +++ b/tests/geometry/point.rs @@ -92,11 +92,3 @@ fn to_homogeneous() { assert_eq!(a.to_homogeneous(), expected); } - -#[cfg(feature = "arbitrary")] -quickcheck!( - fn point_sub(pt1: Point3, pt2: Point3) -> bool { - let dpt = &pt2 - &pt1; - relative_eq!(pt2, pt1 + dpt, epsilon = 1.0e-7) - } -); diff --git a/tests/geometry/projection.rs b/tests/geometry/projection.rs index e4081996..1e0c9fd5 100644 --- a/tests/geometry/projection.rs +++ b/tests/geometry/projection.rs @@ -33,27 +33,32 @@ fn perspective_matrix_point_transformation() { ); } -#[cfg(feature = "arbitrary")] -mod quickcheck_tests { - use na::{Orthographic3, Perspective3, Point3}; +#[cfg(feature = "proptest-support")] +mod proptest_tests { + use na::{Orthographic3, Perspective3}; - quickcheck! { - fn perspective_project_unproject(pt: Point3) -> bool { + use crate::proptest::*; + use proptest::{prop_assert, proptest}; + + proptest! { + #[test] + fn perspective_project_unproject(pt in point3()) { let proj = Perspective3::new(800.0 / 600.0, 3.14 / 2.0, 1.0, 1000.0); let projected = proj.project_point(&pt); let unprojected = proj.unproject_point(&projected); - relative_eq!(pt, unprojected, epsilon = 1.0e-7) + prop_assert!(relative_eq!(pt, unprojected, epsilon = 1.0e-7)) } - fn orthographic_project_unproject(pt: Point3) -> bool { + #[test] + fn orthographic_project_unproject(pt in point3()) { let proj = Orthographic3::new(1.0, 2.0, -3.0, -2.5, 10.0, 900.0); let projected = proj.project_point(&pt); let unprojected = proj.unproject_point(&projected); - relative_eq!(pt, unprojected, epsilon = 1.0e-7) + prop_assert!(relative_eq!(pt, unprojected, epsilon = 1.0e-7)) } } } diff --git a/tests/geometry/quaternion.rs b/tests/geometry/quaternion.rs index 5ff20a0e..75d8b870 100644 --- a/tests/geometry/quaternion.rs +++ b/tests/geometry/quaternion.rs @@ -1,15 +1,19 @@ -#![cfg(feature = "arbitrary")] +#![cfg(feature = "proptest-support")] #![allow(non_snake_case)] -use na::{Point3, Quaternion, Rotation3, Unit, UnitQuaternion, Vector3}; +use na::{Unit, UnitQuaternion}; -quickcheck!( +use crate::proptest::*; +use proptest::{prop_assert, proptest}; + +proptest!( /* * * Euler angles. * */ - fn from_euler_angles(r: f64, p: f64, y: f64) -> bool { + #[test] + fn from_euler_angles(r in PROPTEST_F64, p in PROPTEST_F64, y in PROPTEST_F64) { let roll = UnitQuaternion::from_euler_angles(r, 0.0, 0.0); let pitch = UnitQuaternion::from_euler_angles(0.0, p, 0.0); let yaw = UnitQuaternion::from_euler_angles(0.0, 0.0, y); @@ -20,20 +24,21 @@ quickcheck!( let rpitch = pitch.to_rotation_matrix(); let ryaw = yaw.to_rotation_matrix(); - relative_eq!(rroll[(0, 0)], 1.0, epsilon = 1.0e-7) && // rotation wrt. x axis. - relative_eq!(rpitch[(1, 1)], 1.0, epsilon = 1.0e-7) && // rotation wrt. y axis. - relative_eq!(ryaw[(2, 2)], 1.0, epsilon = 1.0e-7) && // rotation wrt. z axis. - relative_eq!(yaw * pitch * roll, rpy, epsilon = 1.0e-7) + prop_assert!(relative_eq!(rroll[(0, 0)], 1.0, epsilon = 1.0e-7)); // rotation wrt. x axis. + prop_assert!(relative_eq!(rpitch[(1, 1)], 1.0, epsilon = 1.0e-7)); // rotation wrt. y axis. + prop_assert!(relative_eq!(ryaw[(2, 2)], 1.0, epsilon = 1.0e-7)); // rotation wrt. z axis. + prop_assert!(relative_eq!(yaw * pitch * roll, rpy, epsilon = 1.0e-7)); } - fn euler_angles(r: f64, p: f64, y: f64) -> bool { + #[test] + fn euler_angles(r in PROPTEST_F64, p in PROPTEST_F64, y in PROPTEST_F64) { let rpy = UnitQuaternion::from_euler_angles(r, p, y); let (roll, pitch, yaw) = rpy.euler_angles(); - relative_eq!( + prop_assert!(relative_eq!( UnitQuaternion::from_euler_angles(roll, pitch, yaw), rpy, epsilon = 1.0e-7 - ) + )) } /* @@ -41,12 +46,13 @@ quickcheck!( * From/to rotation matrix. * */ - fn unit_quaternion_rotation_conversion(q: UnitQuaternion) -> bool { + #[test] + fn unit_quaternion_rotation_conversion(q in unit_quaternion()) { let r = q.to_rotation_matrix(); let qq = UnitQuaternion::from_rotation_matrix(&r); let rr = qq.to_rotation_matrix(); - relative_eq!(q, qq, epsilon = 1.0e-7) && relative_eq!(r, rr, epsilon = 1.0e-7) + prop_assert!(relative_eq!(q, qq, epsilon = 1.0e-7) && relative_eq!(r, rr, epsilon = 1.0e-7)) } /* @@ -55,24 +61,25 @@ quickcheck!( * */ + #[test] #[cfg_attr(rustfmt, rustfmt_skip)] fn unit_quaternion_transformation( - q: UnitQuaternion, - v: Vector3, - p: Point3 - ) -> bool { + q in unit_quaternion(), + v in vector3(), + p in point3() + ) { let r = q.to_rotation_matrix(); let rv = r * v; let rp = r * p; - relative_eq!(q * v, rv, epsilon = 1.0e-7) + prop_assert!(relative_eq!(q * v, rv, epsilon = 1.0e-7) && relative_eq!(q * &v, rv, epsilon = 1.0e-7) && relative_eq!(&q * v, rv, epsilon = 1.0e-7) && relative_eq!(&q * &v, rv, epsilon = 1.0e-7) && relative_eq!(q * p, rp, epsilon = 1.0e-7) && relative_eq!(q * &p, rp, epsilon = 1.0e-7) && relative_eq!(&q * p, rp, epsilon = 1.0e-7) - && relative_eq!(&q * &p, rp, epsilon = 1.0e-7) + && relative_eq!(&q * &p, rp, epsilon = 1.0e-7)) } /* @@ -80,16 +87,17 @@ quickcheck!( * Inversion. * */ - fn unit_quaternion_inv(q: UnitQuaternion) -> bool { + #[test] + fn unit_quaternion_inv(q in unit_quaternion()) { let iq = q.inverse(); - relative_eq!(&iq * &q, UnitQuaternion::identity(), epsilon = 1.0e-7) + prop_assert!(relative_eq!(&iq * &q, UnitQuaternion::identity(), epsilon = 1.0e-7) && relative_eq!(iq * &q, UnitQuaternion::identity(), epsilon = 1.0e-7) && relative_eq!(&iq * q, UnitQuaternion::identity(), epsilon = 1.0e-7) && relative_eq!(iq * q, UnitQuaternion::identity(), epsilon = 1.0e-7) && relative_eq!(&q * &iq, UnitQuaternion::identity(), epsilon = 1.0e-7) && relative_eq!(q * &iq, UnitQuaternion::identity(), epsilon = 1.0e-7) && relative_eq!(&q * iq, UnitQuaternion::identity(), epsilon = 1.0e-7) - && relative_eq!(q * iq, UnitQuaternion::identity(), epsilon = 1.0e-7) + && relative_eq!(q * iq, UnitQuaternion::identity(), epsilon = 1.0e-7)) } /* @@ -97,14 +105,15 @@ quickcheck!( * Quaterion * Vector == Rotation * Vector * */ - fn unit_quaternion_mul_vector(q: UnitQuaternion, v: Vector3, p: Point3) -> bool { + #[test] + fn unit_quaternion_mul_vector(q in unit_quaternion(), v in vector3(), p in point3()) { let r = q.to_rotation_matrix(); - relative_eq!(q * v, r * v, epsilon = 1.0e-7) && - relative_eq!(q * p, r * p, epsilon = 1.0e-7) && + prop_assert!(relative_eq!(q * v, r * v, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(q * p, r * p, epsilon = 1.0e-7)); // Equivalence q = -q - relative_eq!(UnitQuaternion::new_unchecked(-q.into_inner()) * v, r * v, epsilon = 1.0e-7) && - relative_eq!(UnitQuaternion::new_unchecked(-q.into_inner()) * p, r * p, epsilon = 1.0e-7) + prop_assert!(relative_eq!(UnitQuaternion::new_unchecked(-q.into_inner()) * v, r * v, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(UnitQuaternion::new_unchecked(-q.into_inner()) * p, r * p, epsilon = 1.0e-7)); } /* @@ -112,23 +121,25 @@ quickcheck!( * Unit quaternion double-covering. * */ - fn unit_quaternion_double_covering(q: UnitQuaternion) -> bool { + #[test] + fn unit_quaternion_double_covering(q in unit_quaternion()) { let mq = UnitQuaternion::new_unchecked(-q.into_inner()); - mq == q && mq.angle() == q.angle() && mq.axis() == q.axis() + prop_assert!(mq == q && mq.angle() == q.angle() && mq.axis() == q.axis()) } // Test that all operators (incl. all combinations of references) work. // See the top comment on `geometry/quaternion_ops.rs` for details on which operations are // supported. + #[test] #[cfg_attr(rustfmt, rustfmt_skip)] fn all_op_exist( - q: Quaternion, - uq: UnitQuaternion, - v: Vector3, - p: Point3, - r: Rotation3, - s: f64 - ) -> bool { + q in quaternion(), + uq in unit_quaternion(), + v in vector3(), + p in point3(), + r in rotation3(), + s in PROPTEST_F64 + ) { let uv = Unit::new_normalize(v); let qpq = q + q; @@ -196,7 +207,7 @@ quickcheck!( uqDr1 /= r; uqDr2 /= &r; - qMs1 == qMs + prop_assert!(qMs1 == qMs && qMq1 == qMq && qMq1 == qMq2 && qpq1 == qpq @@ -250,6 +261,6 @@ quickcheck!( && uqMv == &uq * v && uqMuv == &uq * &uv && uqMuv == uq * &uv - && uqMuv == &uq * uv + && uqMuv == &uq * uv) } ); diff --git a/tests/geometry/rotation.rs b/tests/geometry/rotation.rs index 2ada2939..9a29772e 100644 --- a/tests/geometry/rotation.rs +++ b/tests/geometry/rotation.rs @@ -30,44 +30,50 @@ fn quaternion_euler_angles_issue_494() { assert_eq!(angs.2, 0.0); } -#[cfg(feature = "arbitrary")] -mod quickcheck_tests { - use na::{self, Rotation2, Rotation3, Unit, Vector2, Vector3}; +#[cfg(feature = "proptest-support")] +mod proptest_tests { + use na::{self, Rotation2, Rotation3, Unit}; use simba::scalar::RealField; use std::f64; - quickcheck! { + use crate::proptest::*; + use proptest::{prop_assert, prop_assert_eq, proptest}; + + proptest! { /* * * Euler angles. * */ - fn from_euler_angles(r: f64, p: f64, y: f64) -> bool { + #[test] + fn from_euler_angles(r in PROPTEST_F64, p in PROPTEST_F64, y in PROPTEST_F64) { let roll = Rotation3::from_euler_angles(r, 0.0, 0.0); let pitch = Rotation3::from_euler_angles(0.0, p, 0.0); let yaw = Rotation3::from_euler_angles(0.0, 0.0, y); let rpy = Rotation3::from_euler_angles(r, p, y); - roll[(0, 0)] == 1.0 && // rotation wrt. x axis. - pitch[(1, 1)] == 1.0 && // rotation wrt. y axis. - yaw[(2, 2)] == 1.0 && // rotation wrt. z axis. - yaw * pitch * roll == rpy + prop_assert_eq!(roll[(0, 0)], 1.0); // rotation wrt. x axis. + prop_assert_eq!(pitch[(1, 1)], 1.0); // rotation wrt. y axis. + prop_assert_eq!(yaw[(2, 2)], 1.0); // rotation wrt. z axis. + prop_assert_eq!(yaw * pitch * roll, rpy); } - fn euler_angles(r: f64, p: f64, y: f64) -> bool { + #[test] + fn euler_angles(r in PROPTEST_F64, p in PROPTEST_F64, y in PROPTEST_F64) { let rpy = Rotation3::from_euler_angles(r, p, y); let (roll, pitch, yaw) = rpy.euler_angles(); - relative_eq!(Rotation3::from_euler_angles(roll, pitch, yaw), rpy, epsilon = 1.0e-7) + prop_assert!(relative_eq!(Rotation3::from_euler_angles(roll, pitch, yaw), rpy, epsilon = 1.0e-7)); } - fn euler_angles_gimble_lock(r: f64, y: f64) -> bool { + #[test] + fn euler_angles_gimble_lock(r in PROPTEST_F64, y in PROPTEST_F64) { let pos = Rotation3::from_euler_angles(r, f64::frac_pi_2(), y); let neg = Rotation3::from_euler_angles(r, -f64::frac_pi_2(), y); let (pos_r, pos_p, pos_y) = pos.euler_angles(); let (neg_r, neg_p, neg_y) = neg.euler_angles(); - relative_eq!(Rotation3::from_euler_angles(pos_r, pos_p, pos_y), pos, epsilon = 1.0e-7) && - relative_eq!(Rotation3::from_euler_angles(neg_r, neg_p, neg_y), neg, epsilon = 1.0e-7) + prop_assert!(relative_eq!(Rotation3::from_euler_angles(pos_r, pos_p, pos_y), pos, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(Rotation3::from_euler_angles(neg_r, neg_p, neg_y), neg, epsilon = 1.0e-7)); } /* @@ -75,26 +81,28 @@ mod quickcheck_tests { * Inversion is transposition. * */ - fn rotation_inv_3(a: Rotation3) -> bool { + #[test] + fn rotation_inv_3(a in rotation3()) { let ta = a.transpose(); let ia = a.inverse(); - ta == ia && - relative_eq!(&ta * &a, Rotation3::identity(), epsilon = 1.0e-7) && - relative_eq!(&ia * a, Rotation3::identity(), epsilon = 1.0e-7) && - relative_eq!( a * &ta, Rotation3::identity(), epsilon = 1.0e-7) && - relative_eq!( a * ia, Rotation3::identity(), epsilon = 1.0e-7) + prop_assert_eq!(ta, ia); + prop_assert!(relative_eq!(&ta * &a, Rotation3::identity(), epsilon = 1.0e-7)); + prop_assert!(relative_eq!(&ia * a, Rotation3::identity(), epsilon = 1.0e-7)); + prop_assert!(relative_eq!( a * &ta, Rotation3::identity(), epsilon = 1.0e-7)); + prop_assert!(relative_eq!( a * ia, Rotation3::identity(), epsilon = 1.0e-7)); } - fn rotation_inv_2(a: Rotation2) -> bool { + #[test] + fn rotation_inv_2(a in rotation2()) { let ta = a.transpose(); let ia = a.inverse(); - ta == ia && - relative_eq!(&ta * &a, Rotation2::identity(), epsilon = 1.0e-7) && - relative_eq!(&ia * a, Rotation2::identity(), epsilon = 1.0e-7) && - relative_eq!( a * &ta, Rotation2::identity(), epsilon = 1.0e-7) && - relative_eq!( a * ia, Rotation2::identity(), epsilon = 1.0e-7) + prop_assert_eq!(ta, ia); + prop_assert!(relative_eq!(&ta * &a, Rotation2::identity(), epsilon = 1.0e-7)); + prop_assert!(relative_eq!(&ia * a, Rotation2::identity(), epsilon = 1.0e-7)); + prop_assert!(relative_eq!( a * &ta, Rotation2::identity(), epsilon = 1.0e-7)); + prop_assert!(relative_eq!( a * ia, Rotation2::identity(), epsilon = 1.0e-7)); } /* @@ -102,12 +110,14 @@ mod quickcheck_tests { * Angle between vectors. * */ - fn angle_is_commutative_2(a: Vector2, b: Vector2) -> bool { - a.angle(&b) == b.angle(&a) + #[test] + fn angle_is_commutative_2(a in vector2(), b in vector2()) { + prop_assert_eq!(a.angle(&b), b.angle(&a)) } - fn angle_is_commutative_3(a: Vector3, b: Vector3) -> bool { - a.angle(&b) == b.angle(&a) + #[test] + fn angle_is_commutative_3(a in vector3(), b in vector3()) { + prop_assert_eq!(a.angle(&b), b.angle(&a)) } /* @@ -115,50 +125,46 @@ mod quickcheck_tests { * Rotation matrix between vectors. * */ - fn rotation_between_is_anticommutative_2(a: Vector2, b: Vector2) -> bool { + #[test] + fn rotation_between_is_anticommutative_2(a in vector2(), b in vector2()) { let rab = Rotation2::rotation_between(&a, &b); let rba = Rotation2::rotation_between(&b, &a); - relative_eq!(rab * rba, Rotation2::identity()) + prop_assert!(relative_eq!(rab * rba, Rotation2::identity())); } - fn rotation_between_is_anticommutative_3(a: Vector3, b: Vector3) -> bool { + #[test] + fn rotation_between_is_anticommutative_3(a in vector3(), b in vector3()) { let rots = (Rotation3::rotation_between(&a, &b), Rotation3::rotation_between(&b, &a)); if let (Some(rab), Some(rba)) = rots { - relative_eq!(rab * rba, Rotation3::identity(), epsilon = 1.0e-7) - } - else { - true + prop_assert!(relative_eq!(rab * rba, Rotation3::identity(), epsilon = 1.0e-7)); } } - fn rotation_between_is_identity(v2: Vector2, v3: Vector3) -> bool { + #[test] + fn rotation_between_is_identity(v2 in vector2(), v3 in vector3()) { let vv2 = 3.42 * v2; let vv3 = 4.23 * v3; - relative_eq!(v2.angle(&vv2), 0.0, epsilon = 1.0e-7) && - relative_eq!(v3.angle(&vv3), 0.0, epsilon = 1.0e-7) && - relative_eq!(Rotation2::rotation_between(&v2, &vv2), Rotation2::identity()) && - Rotation3::rotation_between(&v3, &vv3).unwrap() == Rotation3::identity() + prop_assert!(relative_eq!(v2.angle(&vv2), 0.0, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(v3.angle(&vv3), 0.0, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(Rotation2::rotation_between(&v2, &vv2), Rotation2::identity())); + prop_assert_eq!(Rotation3::rotation_between(&v3, &vv3).unwrap(), Rotation3::identity()); } - fn rotation_between_2(a: Vector2, b: Vector2) -> bool { + #[test] + fn rotation_between_2(a in vector2(), b in vector2()) { if !relative_eq!(a.angle(&b), 0.0, epsilon = 1.0e-7) { let r = Rotation2::rotation_between(&a, &b); - relative_eq!((r * a).angle(&b), 0.0, epsilon = 1.0e-7) - } - else { - true + prop_assert!(relative_eq!((r * a).angle(&b), 0.0, epsilon = 1.0e-7)) } } - fn rotation_between_3(a: Vector3, b: Vector3) -> bool { + #[test] + fn rotation_between_3(a in vector3(), b in vector3()) { if !relative_eq!(a.angle(&b), 0.0, epsilon = 1.0e-7) { let r = Rotation3::rotation_between(&a, &b).unwrap(); - relative_eq!((r * a).angle(&b), 0.0, epsilon = 1.0e-7) - } - else { - true + prop_assert!(relative_eq!((r * a).angle(&b), 0.0, epsilon = 1.0e-7)) } } @@ -168,25 +174,27 @@ mod quickcheck_tests { * Rotation construction. * */ - fn new_rotation_2(angle: f64) -> bool { + #[test] + fn new_rotation_2(angle in PROPTEST_F64) { let r = Rotation2::new(angle); let angle = na::wrap(angle, -f64::pi(), f64::pi()); - relative_eq!(r.angle(), angle, epsilon = 1.0e-7) + prop_assert!(relative_eq!(r.angle(), angle, epsilon = 1.0e-7)) } - fn new_rotation_3(axisangle: Vector3) -> bool { + #[test] + fn new_rotation_3(axisangle in vector3()) { let r = Rotation3::new(axisangle); if let Some((axis, angle)) = Unit::try_new_and_get(axisangle, 0.0) { let angle = na::wrap(angle, -f64::pi(), f64::pi()); - (relative_eq!(r.angle(), angle, epsilon = 1.0e-7) && + prop_assert!((relative_eq!(r.angle(), angle, epsilon = 1.0e-7) && relative_eq!(r.axis().unwrap(), axis, epsilon = 1.0e-7)) || (relative_eq!(r.angle(), -angle, epsilon = 1.0e-7) && - relative_eq!(r.axis().unwrap(), -axis, epsilon = 1.0e-7)) + relative_eq!(r.axis().unwrap(), -axis, epsilon = 1.0e-7))) } else { - r == Rotation3::identity() + prop_assert_eq!(r, Rotation3::identity()) } } @@ -195,28 +203,30 @@ mod quickcheck_tests { * Rotation pow. * */ - fn powf_rotation_2(angle: f64, pow: f64) -> bool { + #[test] + fn powf_rotation_2(angle in PROPTEST_F64, pow in PROPTEST_F64) { let r = Rotation2::new(angle).powf(pow); let angle = na::wrap(angle, -f64::pi(), f64::pi()); let pangle = na::wrap(angle * pow, -f64::pi(), f64::pi()); - relative_eq!(r.angle(), pangle, epsilon = 1.0e-7) + prop_assert!(relative_eq!(r.angle(), pangle, epsilon = 1.0e-7)); } - fn powf_rotation_3(axisangle: Vector3, pow: f64) -> bool { + #[test] + fn powf_rotation_3(axisangle in vector3(), pow in PROPTEST_F64) { let r = Rotation3::new(axisangle).powf(pow); if let Some((axis, angle)) = Unit::try_new_and_get(axisangle, 0.0) { let angle = na::wrap(angle, -f64::pi(), f64::pi()); let pangle = na::wrap(angle * pow, -f64::pi(), f64::pi()); - (relative_eq!(r.angle(), pangle, epsilon = 1.0e-7) && + prop_assert!((relative_eq!(r.angle(), pangle, epsilon = 1.0e-7) && relative_eq!(r.axis().unwrap(), axis, epsilon = 1.0e-7)) || (relative_eq!(r.angle(), -pangle, epsilon = 1.0e-7) && - relative_eq!(r.axis().unwrap(), -axis, epsilon = 1.0e-7)) + relative_eq!(r.axis().unwrap(), -axis, epsilon = 1.0e-7))); } else { - r == Rotation3::identity() + prop_assert_eq!(r, Rotation3::identity()) } } } diff --git a/tests/geometry/similarity.rs b/tests/geometry/similarity.rs index b93d2e51..bcd430e9 100644 --- a/tests/geometry/similarity.rs +++ b/tests/geometry/similarity.rs @@ -1,41 +1,45 @@ -#![cfg(feature = "arbitrary")] +#![cfg(feature = "proptest-support")] #![allow(non_snake_case)] -use na::{Isometry3, Point3, Similarity3, Translation3, UnitQuaternion, Vector3}; +use na::Similarity3; -quickcheck!( - fn inverse_is_identity(i: Similarity3, p: Point3, v: Vector3) -> bool { +use crate::proptest::*; +use proptest::{prop_assert, prop_assert_eq, proptest}; + +proptest!( + #[test] + fn inverse_is_identity(i in similarity3(), p in point3(), v in vector3()) { let ii = i.inverse(); - relative_eq!(i * ii, Similarity3::identity(), epsilon = 1.0e-7) + prop_assert!(relative_eq!(i * ii, Similarity3::identity(), epsilon = 1.0e-7) && relative_eq!(ii * i, Similarity3::identity(), epsilon = 1.0e-7) && relative_eq!((i * ii) * p, p, epsilon = 1.0e-7) && relative_eq!((ii * i) * p, p, epsilon = 1.0e-7) && relative_eq!((i * ii) * v, v, epsilon = 1.0e-7) - && relative_eq!((ii * i) * v, v, epsilon = 1.0e-7) + && relative_eq!((ii * i) * v, v, epsilon = 1.0e-7)) } + #[test] #[cfg_attr(rustfmt, rustfmt_skip)] fn inverse_is_parts_inversion( - t: Translation3, - r: UnitQuaternion, - scaling: f64 - ) -> bool { - if relative_eq!(scaling, 0.0) { - true - } else { + t in translation3(), + r in unit_quaternion(), + scaling in PROPTEST_F64 + ) { + if !relative_eq!(scaling, 0.0) { let s = Similarity3::from_isometry(t * r, scaling); - s.inverse() == Similarity3::from_scaling(1.0 / scaling) * r.inverse() * t.inverse() + prop_assert_eq!(s.inverse(), Similarity3::from_scaling(1.0 / scaling) * r.inverse() * t.inverse()) } } + #[test] #[cfg_attr(rustfmt, rustfmt_skip)] fn multiply_equals_alga_transform( - s: Similarity3, - v: Vector3, - p: Point3 - ) -> bool { - s * v == s.transform_vector(&v) + s in similarity3(), + v in vector3(), + p in point3() + ) { + prop_assert!(s * v == s.transform_vector(&v) && s * p == s.transform_point(&p) && relative_eq!( s.inverse() * v, @@ -46,114 +50,114 @@ quickcheck!( s.inverse() * p, s.inverse_transform_point(&p), epsilon = 1.0e-7 - ) + )) } + #[test] #[cfg_attr(rustfmt, rustfmt_skip)] fn composition( - i: Isometry3, - uq: UnitQuaternion, - t: Translation3, - v: Vector3, - p: Point3, - scaling: f64 - ) -> bool { - if relative_eq!(scaling, 0.0) { - return true; + i in isometry3(), + uq in unit_quaternion(), + t in translation3(), + v in vector3(), + p in point3(), + scaling in PROPTEST_F64 + ) { + if !relative_eq!(scaling, 0.0) { + let s = Similarity3::from_scaling(scaling); + + // (rotation × translation × scaling) × point = rotation × (translation × (scaling × point)) + prop_assert!(relative_eq!((uq * t * s) * v, uq * (scaling * v), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((uq * t * s) * p, uq * (t * (scaling * p)), epsilon = 1.0e-7)); + + // (translation × rotation × scaling) × point = translation × (rotation × (scaling × point)) + prop_assert!(relative_eq!((t * uq * s) * v, uq * (scaling * v), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((t * uq * s) * p, t * (uq * (scaling * p)), epsilon = 1.0e-7)); + + // (rotation × isometry × scaling) × point = rotation × (isometry × (scaling × point)) + prop_assert!(relative_eq!((uq * i * s) * v, uq * (i * (scaling * v)), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((uq * i * s) * p, uq * (i * (scaling * p)), epsilon = 1.0e-7)); + + // (isometry × rotation × scaling) × point = isometry × (rotation × (scaling × point)) + prop_assert!(relative_eq!((i * uq * s) * v, i * (uq * (scaling * v)), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((i * uq * s) * p, i * (uq * (scaling * p)), epsilon = 1.0e-7)); + + // (translation × isometry × scaling) × point = translation × (isometry × (scaling × point)) + prop_assert!(relative_eq!((t * i * s) * v, (i * (scaling * v)), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((t * i * s) * p, t * (i * (scaling * p)), epsilon = 1.0e-7)); + + // (isometry × translation × scaling) × point = isometry × (translation × (scaling × point)) + prop_assert!(relative_eq!((i * t * s) * v, i * (scaling * v), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((i * t * s) * p, i * (t * (scaling * p)), epsilon = 1.0e-7)); + + + /* + * Same as before but with scaling on the middle. + */ + // (rotation × scaling × translation) × point = rotation × (scaling × (translation × point)) + prop_assert!(relative_eq!((uq * s * t) * v, uq * (scaling * v), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((uq * s * t) * p, uq * (scaling * (t * p)), epsilon = 1.0e-7)); + + // (translation × scaling × rotation) × point = translation × (scaling × (rotation × point)) + prop_assert!(relative_eq!((t * s * uq) * v, scaling * (uq * v), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((t * s * uq) * p, t * (scaling * (uq * p)), epsilon = 1.0e-7)); + + // (rotation × scaling × isometry) × point = rotation × (scaling × (isometry × point)) + prop_assert!(relative_eq!((uq * s * i) * v, uq * (scaling * (i * v)), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((uq * s * i) * p, uq * (scaling * (i * p)), epsilon = 1.0e-7)); + + // (isometry × scaling × rotation) × point = isometry × (scaling × (rotation × point)) + prop_assert!(relative_eq!((i * s * uq) * v, i * (scaling * (uq * v)), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((i * s * uq) * p, i * (scaling * (uq * p)), epsilon = 1.0e-7)); + + // (translation × scaling × isometry) × point = translation × (scaling × (isometry × point)) + prop_assert!(relative_eq!((t * s * i) * v, (scaling * (i * v)), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((t * s * i) * p, t * (scaling * (i * p)), epsilon = 1.0e-7)); + + // (isometry × scaling × translation) × point = isometry × (scaling × (translation × point)) + prop_assert!(relative_eq!((i * s * t) * v, i * (scaling * v), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((i * s * t) * p, i * (scaling * (t * p)), epsilon = 1.0e-7)); + + + /* + * Same as before but with scaling on the left. + */ + // (scaling × rotation × translation) × point = scaling × (rotation × (translation × point)) + prop_assert!(relative_eq!((s * uq * t) * v, scaling * (uq * v), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((s * uq * t) * p, scaling * (uq * (t * p)), epsilon = 1.0e-7)); + + // (scaling × translation × rotation) × point = scaling × (translation × (rotation × point)) + prop_assert!(relative_eq!((s * t * uq) * v, scaling * (uq * v), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((s * t * uq) * p, scaling * (t * (uq * p)), epsilon = 1.0e-7)); + + // (scaling × rotation × isometry) × point = scaling × (rotation × (isometry × point)) + prop_assert!(relative_eq!((s * uq * i) * v, scaling * (uq * (i * v)), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((s * uq * i) * p, scaling * (uq * (i * p)), epsilon = 1.0e-7)); + + // (scaling × isometry × rotation) × point = scaling × (isometry × (rotation × point)) + prop_assert!(relative_eq!((s * i * uq) * v, scaling * (i * (uq * v)), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((s * i * uq) * p, scaling * (i * (uq * p)), epsilon = 1.0e-7)); + + // (scaling × translation × isometry) × point = scaling × (translation × (isometry × point)) + prop_assert!(relative_eq!((s * t * i) * v, (scaling * (i * v)), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((s * t * i) * p, scaling * (t * (i * p)), epsilon = 1.0e-7)); + + // (scaling × isometry × translation) × point = scaling × (isometry × (translation × point)) + prop_assert!(relative_eq!((s * i * t) * v, scaling * (i * v), epsilon = 1.0e-7)); + prop_assert!(relative_eq!((s * i * t) * p, scaling * (i * (t * p)), epsilon = 1.0e-7)); } - - let s = Similarity3::from_scaling(scaling); - - // (rotation × translation × scaling) × point = rotation × (translation × (scaling × point)) - relative_eq!((uq * t * s) * v, uq * (scaling * v), epsilon = 1.0e-7) && - relative_eq!((uq * t * s) * p, uq * (t * (scaling * p)), epsilon = 1.0e-7) && - - // (translation × rotation × scaling) × point = translation × (rotation × (scaling × point)) - relative_eq!((t * uq * s) * v, uq * (scaling * v), epsilon = 1.0e-7) && - relative_eq!((t * uq * s) * p, t * (uq * (scaling * p)), epsilon = 1.0e-7) && - - // (rotation × isometry × scaling) × point = rotation × (isometry × (scaling × point)) - relative_eq!((uq * i * s) * v, uq * (i * (scaling * v)), epsilon = 1.0e-7) && - relative_eq!((uq * i * s) * p, uq * (i * (scaling * p)), epsilon = 1.0e-7) && - - // (isometry × rotation × scaling) × point = isometry × (rotation × (scaling × point)) - relative_eq!((i * uq * s) * v, i * (uq * (scaling * v)), epsilon = 1.0e-7) && - relative_eq!((i * uq * s) * p, i * (uq * (scaling * p)), epsilon = 1.0e-7) && - - // (translation × isometry × scaling) × point = translation × (isometry × (scaling × point)) - relative_eq!((t * i * s) * v, (i * (scaling * v)), epsilon = 1.0e-7) && - relative_eq!((t * i * s) * p, t * (i * (scaling * p)), epsilon = 1.0e-7) && - - // (isometry × translation × scaling) × point = isometry × (translation × (scaling × point)) - relative_eq!((i * t * s) * v, i * (scaling * v), epsilon = 1.0e-7) && - relative_eq!((i * t * s) * p, i * (t * (scaling * p)), epsilon = 1.0e-7) && - - - /* - * Same as before but with scaling on the middle. - */ - // (rotation × scaling × translation) × point = rotation × (scaling × (translation × point)) - relative_eq!((uq * s * t) * v, uq * (scaling * v), epsilon = 1.0e-7) && - relative_eq!((uq * s * t) * p, uq * (scaling * (t * p)), epsilon = 1.0e-7) && - - // (translation × scaling × rotation) × point = translation × (scaling × (rotation × point)) - relative_eq!((t * s * uq) * v, scaling * (uq * v), epsilon = 1.0e-7) && - relative_eq!((t * s * uq) * p, t * (scaling * (uq * p)), epsilon = 1.0e-7) && - - // (rotation × scaling × isometry) × point = rotation × (scaling × (isometry × point)) - relative_eq!((uq * s * i) * v, uq * (scaling * (i * v)), epsilon = 1.0e-7) && - relative_eq!((uq * s * i) * p, uq * (scaling * (i * p)), epsilon = 1.0e-7) && - - // (isometry × scaling × rotation) × point = isometry × (scaling × (rotation × point)) - relative_eq!((i * s * uq) * v, i * (scaling * (uq * v)), epsilon = 1.0e-7) && - relative_eq!((i * s * uq) * p, i * (scaling * (uq * p)), epsilon = 1.0e-7) && - - // (translation × scaling × isometry) × point = translation × (scaling × (isometry × point)) - relative_eq!((t * s * i) * v, (scaling * (i * v)), epsilon = 1.0e-7) && - relative_eq!((t * s * i) * p, t * (scaling * (i * p)), epsilon = 1.0e-7) && - - // (isometry × scaling × translation) × point = isometry × (scaling × (translation × point)) - relative_eq!((i * s * t) * v, i * (scaling * v), epsilon = 1.0e-7) && - relative_eq!((i * s * t) * p, i * (scaling * (t * p)), epsilon = 1.0e-7) && - - - /* - * Same as before but with scaling on the left. - */ - // (scaling × rotation × translation) × point = scaling × (rotation × (translation × point)) - relative_eq!((s * uq * t) * v, scaling * (uq * v), epsilon = 1.0e-7) && - relative_eq!((s * uq * t) * p, scaling * (uq * (t * p)), epsilon = 1.0e-7) && - - // (scaling × translation × rotation) × point = scaling × (translation × (rotation × point)) - relative_eq!((s * t * uq) * v, scaling * (uq * v), epsilon = 1.0e-7) && - relative_eq!((s * t * uq) * p, scaling * (t * (uq * p)), epsilon = 1.0e-7) && - - // (scaling × rotation × isometry) × point = scaling × (rotation × (isometry × point)) - relative_eq!((s * uq * i) * v, scaling * (uq * (i * v)), epsilon = 1.0e-7) && - relative_eq!((s * uq * i) * p, scaling * (uq * (i * p)), epsilon = 1.0e-7) && - - // (scaling × isometry × rotation) × point = scaling × (isometry × (rotation × point)) - relative_eq!((s * i * uq) * v, scaling * (i * (uq * v)), epsilon = 1.0e-7) && - relative_eq!((s * i * uq) * p, scaling * (i * (uq * p)), epsilon = 1.0e-7) && - - // (scaling × translation × isometry) × point = scaling × (translation × (isometry × point)) - relative_eq!((s * t * i) * v, (scaling * (i * v)), epsilon = 1.0e-7) && - relative_eq!((s * t * i) * p, scaling * (t * (i * p)), epsilon = 1.0e-7) && - - // (scaling × isometry × translation) × point = scaling × (isometry × (translation × point)) - relative_eq!((s * i * t) * v, scaling * (i * v), epsilon = 1.0e-7) && - relative_eq!((s * i * t) * p, scaling * (i * (t * p)), epsilon = 1.0e-7) } + #[test] #[cfg_attr(rustfmt, rustfmt_skip)] fn all_op_exist( - s: Similarity3, - i: Isometry3, - uq: UnitQuaternion, - t: Translation3, - v: Vector3, - p: Point3 - ) -> bool { + s in similarity3(), + i in isometry3(), + uq in unit_quaternion(), + t in translation3(), + v in vector3(), + p in point3() + ) { let sMs = s * s; let sMuq = s * uq; let sDs = s / s; @@ -216,7 +220,7 @@ quickcheck!( sDi1 /= i; sDi2 /= &i; - sMt == sMt1 + prop_assert!(sMt == sMt1 && sMt == sMt2 && sMs == sMs1 && sMs == sMs2 @@ -271,6 +275,6 @@ quickcheck!( && iMs == &i * s && iDs == &i / &s && iDs == i / &s - && iDs == &i / s + && iDs == &i / s) } ); diff --git a/tests/geometry/unit_complex.rs b/tests/geometry/unit_complex.rs index 263ecd33..a24a80e2 100644 --- a/tests/geometry/unit_complex.rs +++ b/tests/geometry/unit_complex.rs @@ -1,20 +1,25 @@ -#![cfg(feature = "arbitrary")] +#![cfg(feature = "proptest-support")] #![allow(non_snake_case)] -use na::{Point2, Rotation2, Unit, UnitComplex, Vector2}; +use na::{Unit, UnitComplex}; -quickcheck!( +use crate::proptest::*; +use proptest::{prop_assert, proptest}; + +proptest!( /* * * From/to rotation matrix. * */ - fn unit_complex_rotation_conversion(c: UnitComplex) -> bool { + #[test] + fn unit_complex_rotation_conversion(c in unit_complex()) { let r = c.to_rotation_matrix(); let cc = UnitComplex::from_rotation_matrix(&r); let rr = cc.to_rotation_matrix(); - relative_eq!(c, cc, epsilon = 1.0e-7) && relative_eq!(r, rr, epsilon = 1.0e-7) + prop_assert!(relative_eq!(c, cc, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(r, rr, epsilon = 1.0e-7)); } /* @@ -22,19 +27,20 @@ quickcheck!( * Point/Vector transformation. * */ - fn unit_complex_transformation(c: UnitComplex, v: Vector2, p: Point2) -> bool { + #[test] + fn unit_complex_transformation(c in unit_complex(), v in vector2(), p in point2()) { let r = c.to_rotation_matrix(); let rv = r * v; let rp = r * p; - relative_eq!(c * v, rv, epsilon = 1.0e-7) + prop_assert!(relative_eq!(c * v, rv, epsilon = 1.0e-7) && relative_eq!(c * &v, rv, epsilon = 1.0e-7) && relative_eq!(&c * v, rv, epsilon = 1.0e-7) && relative_eq!(&c * &v, rv, epsilon = 1.0e-7) && relative_eq!(c * p, rp, epsilon = 1.0e-7) && relative_eq!(c * &p, rp, epsilon = 1.0e-7) && relative_eq!(&c * p, rp, epsilon = 1.0e-7) - && relative_eq!(&c * &p, rp, epsilon = 1.0e-7) + && relative_eq!(&c * &p, rp, epsilon = 1.0e-7)) } /* @@ -42,39 +48,43 @@ quickcheck!( * Inversion. * */ - fn unit_complex_inv(c: UnitComplex) -> bool { + #[test] + fn unit_complex_inv(c in unit_complex()) { let iq = c.inverse(); - relative_eq!(&iq * &c, UnitComplex::identity(), epsilon = 1.0e-7) + prop_assert!(relative_eq!(&iq * &c, UnitComplex::identity(), epsilon = 1.0e-7) && relative_eq!(iq * &c, UnitComplex::identity(), epsilon = 1.0e-7) && relative_eq!(&iq * c, UnitComplex::identity(), epsilon = 1.0e-7) && relative_eq!(iq * c, UnitComplex::identity(), epsilon = 1.0e-7) && relative_eq!(&c * &iq, UnitComplex::identity(), epsilon = 1.0e-7) && relative_eq!(c * &iq, UnitComplex::identity(), epsilon = 1.0e-7) && relative_eq!(&c * iq, UnitComplex::identity(), epsilon = 1.0e-7) - && relative_eq!(c * iq, UnitComplex::identity(), epsilon = 1.0e-7) + && relative_eq!(c * iq, UnitComplex::identity(), epsilon = 1.0e-7)) } /* * - * Quaterion * Vector == Rotation * Vector + * Quaternion * Vector == Rotation * Vector * */ - fn unit_complex_mul_vector(c: UnitComplex, v: Vector2, p: Point2) -> bool { + #[test] + fn unit_complex_mul_vector(c in unit_complex(), v in vector2(), p in point2()) { let r = c.to_rotation_matrix(); - relative_eq!(c * v, r * v, epsilon = 1.0e-7) && relative_eq!(c * p, r * p, epsilon = 1.0e-7) + prop_assert!(relative_eq!(c * v, r * v, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(c * p, r * p, epsilon = 1.0e-7)); } // Test that all operators (incl. all combinations of references) work. // See the top comment on `geometry/quaternion_ops.rs` for details on which operations are // supported. + #[test] #[cfg_attr(rustfmt, rustfmt_skip)] fn all_op_exist( - uc: UnitComplex, - v: Vector2, - p: Point2, - r: Rotation2 - ) -> bool { + uc in unit_complex(), + v in vector2(), + p in point2(), + r in rotation2() + ) { let uv = Unit::new_normalize(v); let ucMuc = uc * uc; @@ -112,7 +122,7 @@ quickcheck!( ucDr1 /= r; ucDr2 /= &r; - ucMuc1 == ucMuc + prop_assert!(ucMuc1 == ucMuc && ucMuc1 == ucMuc2 && ucMr1 == ucMr && ucMr1 == ucMr2 @@ -146,6 +156,6 @@ quickcheck!( && ucMv == &uc * v && ucMuv == &uc * &uv && ucMuv == uc * &uv - && ucMuv == &uc * uv + && ucMuv == &uc * uv) } ); diff --git a/tests/lib.rs b/tests/lib.rs index 29e6cbbe..ca7faa74 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -12,9 +12,6 @@ extern crate approx; extern crate mint; extern crate nalgebra as na; extern crate num_traits as num; -#[cfg(feature = "arbitrary")] -#[macro_use] -extern crate quickcheck; mod core; mod geometry; diff --git a/tests/linalg/balancing.rs b/tests/linalg/balancing.rs index 401f672a..5d250b2c 100644 --- a/tests/linalg/balancing.rs +++ b/tests/linalg/balancing.rs @@ -1,26 +1,28 @@ -#![cfg(feature = "arbitrary")] - -use std::cmp; +#![cfg(feature = "proptest-support")] use na::balancing; -use na::{DMatrix, Matrix4}; +use na::DMatrix; -quickcheck! { - fn balancing_parlett_reinsch(n: usize) -> bool { - let n = cmp::min(n, 10); +use crate::proptest::*; +use proptest::{prop_assert_eq, proptest}; + +proptest! { + #[test] + fn balancing_parlett_reinsch(n in PROPTEST_MATRIX_DIM) { let m = DMatrix::::new_random(n, n); let mut balanced = m.clone(); let d = balancing::balance_parlett_reinsch(&mut balanced); balancing::unbalance(&mut balanced, &d); - balanced == m + prop_assert_eq!(balanced, m); } - fn balancing_parlett_reinsch_static(m: Matrix4) -> bool { + #[test] + fn balancing_parlett_reinsch_static(m in matrix4()) { let mut balanced = m; let d = balancing::balance_parlett_reinsch(&mut balanced); balancing::unbalance(&mut balanced, &d); - balanced == m + prop_assert_eq!(balanced, m); } } diff --git a/tests/linalg/bidiagonal.rs b/tests/linalg/bidiagonal.rs index 8fefb4a2..aaee393f 100644 --- a/tests/linalg/bidiagonal.rs +++ b/tests/linalg/bidiagonal.rs @@ -1,63 +1,61 @@ -#![cfg(feature = "arbitrary")] +#![cfg(feature = "proptest-support")] macro_rules! gen_tests( - ($module: ident, $scalar: ty) => { + ($module: ident, $scalar: expr) => { mod $module { - use na::{DMatrix, Matrix2, Matrix3x5, Matrix4, Matrix5x3}; #[allow(unused_imports)] use crate::core::helper::{RandScalar, RandComplex}; - quickcheck! { - fn bidiagonal(m: DMatrix<$scalar>) -> bool { - let m = m.map(|e| e.0); - if m.len() == 0 { - return true; - } + use crate::proptest::*; + use proptest::{prop_assert, proptest}; + proptest! { + #[test] + fn bidiagonal(m in dmatrix_($scalar)) { let bidiagonal = m.clone().bidiagonalize(); let (u, d, v_t) = bidiagonal.unpack(); - relative_eq!(m, &u * d * &v_t, epsilon = 1.0e-7) + prop_assert!(relative_eq!(m, &u * d * &v_t, epsilon = 1.0e-7)) } - fn bidiagonal_static_5_3(m: Matrix5x3<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn bidiagonal_static_5_3(m in matrix5x3_($scalar)) { let bidiagonal = m.bidiagonalize(); let (u, d, v_t) = bidiagonal.unpack(); - relative_eq!(m, &u * d * &v_t, epsilon = 1.0e-7) + prop_assert!(relative_eq!(m, &u * d * &v_t, epsilon = 1.0e-7)) } - fn bidiagonal_static_3_5(m: Matrix3x5<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn bidiagonal_static_3_5(m in matrix3x5_($scalar)) { let bidiagonal = m.bidiagonalize(); let (u, d, v_t) = bidiagonal.unpack(); - relative_eq!(m, &u * d * &v_t, epsilon = 1.0e-7) + prop_assert!(relative_eq!(m, &u * d * &v_t, epsilon = 1.0e-7)) } - fn bidiagonal_static_square(m: Matrix4<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn bidiagonal_static_square(m in matrix4_($scalar)) { let bidiagonal = m.bidiagonalize(); let (u, d, v_t) = bidiagonal.unpack(); - relative_eq!(m, &u * d * &v_t, epsilon = 1.0e-7) + prop_assert!(relative_eq!(m, &u * d * &v_t, epsilon = 1.0e-7)) } - fn bidiagonal_static_square_2x2(m: Matrix2<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn bidiagonal_static_square_2x2(m in matrix2_($scalar)) { let bidiagonal = m.bidiagonalize(); let (u, d, v_t) = bidiagonal.unpack(); - relative_eq!(m, &u * d * &v_t, epsilon = 1.0e-7) + prop_assert!(relative_eq!(m, &u * d * &v_t, epsilon = 1.0e-7)) } } } } ); -gen_tests!(complex, RandComplex); -gen_tests!(f64, RandScalar); +gen_tests!(complex, complex_f64()); +gen_tests!(f64, PROPTEST_F64); #[test] fn bidiagonal_identity() { diff --git a/tests/linalg/cholesky.rs b/tests/linalg/cholesky.rs index a89802b2..5ea0edaf 100644 --- a/tests/linalg/cholesky.rs +++ b/tests/linalg/cholesky.rs @@ -1,4 +1,4 @@ -#![cfg(all(feature = "arbitrary", feature = "debug"))] +#![cfg(all(feature = "proptest-support", feature = "debug"))] macro_rules! gen_tests( ($module: ident, $scalar: ty) => { @@ -9,32 +9,30 @@ macro_rules! gen_tests( use rand::random; #[allow(unused_imports)] use crate::core::helper::{RandScalar, RandComplex}; - use std::cmp; - quickcheck! { - fn cholesky(n: usize) -> bool { - let m = RandomSDP::new(Dynamic::new(n.max(1).min(50)), || random::<$scalar>().0).unwrap(); + use crate::proptest::*; + use proptest::{prop_assert, proptest}; + + proptest! { + #[test] + fn cholesky(n in PROPTEST_MATRIX_DIM) { + let m = RandomSDP::new(Dynamic::new(n), || random::<$scalar>().0).unwrap(); let l = m.clone().cholesky().unwrap().unpack(); - relative_eq!(m, &l * l.adjoint(), epsilon = 1.0e-7) + prop_assert!(relative_eq!(m, &l * l.adjoint(), epsilon = 1.0e-7)); } - fn cholesky_static(_m: RandomSDP) -> bool { + #[test] + fn cholesky_static(_n in PROPTEST_MATRIX_DIM) { let m = RandomSDP::new(U4, || random::<$scalar>().0).unwrap(); let chol = m.cholesky().unwrap(); let l = chol.unpack(); - if !relative_eq!(m, &l * l.adjoint(), epsilon = 1.0e-7) { - false - } - else { - true - } + prop_assert!(relative_eq!(m, &l * l.adjoint(), epsilon = 1.0e-7)); } - fn cholesky_solve(n: usize, nb: usize) -> bool { - let n = n.max(1).min(50); + #[test] + fn cholesky_solve(n in PROPTEST_MATRIX_DIM, nb in PROPTEST_MATRIX_DIM) { let m = RandomSDP::new(Dynamic::new(n), || random::<$scalar>().0).unwrap(); - let nb = cmp::min(nb, 50); // To avoid slowing down the test too much. let chol = m.clone().cholesky().unwrap(); let b1 = DVector::<$scalar>::new_random(n).map(|e| e.0); @@ -43,11 +41,12 @@ macro_rules! gen_tests( let sol1 = chol.solve(&b1); let sol2 = chol.solve(&b2); - relative_eq!(&m * &sol1, b1, epsilon = 1.0e-7) && - relative_eq!(&m * &sol2, b2, epsilon = 1.0e-7) + prop_assert!(relative_eq!(&m * &sol1, b1, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(&m * &sol2, b2, epsilon = 1.0e-7)); } - fn cholesky_solve_static(_n: usize) -> bool { + #[test] + fn cholesky_solve_static(_n in PROPTEST_MATRIX_DIM) { let m = RandomSDP::new(U4, || random::<$scalar>().0).unwrap(); let chol = m.clone().cholesky().unwrap(); let b1 = Vector4::<$scalar>::new_random().map(|e| e.0); @@ -56,29 +55,32 @@ macro_rules! gen_tests( let sol1 = chol.solve(&b1); let sol2 = chol.solve(&b2); - relative_eq!(m * sol1, b1, epsilon = 1.0e-7) && - relative_eq!(m * sol2, b2, epsilon = 1.0e-7) + prop_assert!(relative_eq!(m * sol1, b1, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(m * sol2, b2, epsilon = 1.0e-7)); } - fn cholesky_inverse(n: usize) -> bool { - let m = RandomSDP::new(Dynamic::new(n.max(1).min(50)), || random::<$scalar>().0).unwrap(); + #[test] + fn cholesky_inverse(n in PROPTEST_MATRIX_DIM) { + let m = RandomSDP::new(Dynamic::new(n), || random::<$scalar>().0).unwrap(); let m1 = m.clone().cholesky().unwrap().inverse(); let id1 = &m * &m1; let id2 = &m1 * &m; - id1.is_identity(1.0e-7) && id2.is_identity(1.0e-7) + prop_assert!(id1.is_identity(1.0e-7) && id2.is_identity(1.0e-7)); } - fn cholesky_inverse_static(_n: usize) -> bool { + #[test] + fn cholesky_inverse_static(_n in PROPTEST_MATRIX_DIM) { let m = RandomSDP::new(U4, || random::<$scalar>().0).unwrap(); let m1 = m.clone().cholesky().unwrap().inverse(); let id1 = &m * &m1; let id2 = &m1 * &m; - id1.is_identity(1.0e-7) && id2.is_identity(1.0e-7) + prop_assert!(id1.is_identity(1.0e-7) && id2.is_identity(1.0e-7)); } - fn cholesky_rank_one_update(_n: usize) -> bool { + #[test] + fn cholesky_rank_one_update(_n in PROPTEST_MATRIX_DIM) { let mut m = RandomSDP::new(U4, || random::<$scalar>().0).unwrap(); let x = Vector4::<$scalar>::new_random().map(|e| e.0); @@ -96,10 +98,11 @@ macro_rules! gen_tests( // updates m manually m.gerc(sigma_scalar, &x, &x, one); // m += sigma * x * x.adjoint() - relative_eq!(m, m_chol_updated, epsilon = 1.0e-7) + prop_assert!(relative_eq!(m, m_chol_updated, epsilon = 1.0e-7)); } - fn cholesky_insert_column(n: usize) -> bool { + #[test] + fn cholesky_insert_column(n in PROPTEST_MATRIX_DIM) { let n = n.max(1).min(10); let j = random::() % n; let m_updated = RandomSDP::new(Dynamic::new(n), || random::<$scalar>().0).unwrap(); @@ -112,10 +115,11 @@ macro_rules! gen_tests( let chol = m.clone().cholesky().unwrap().insert_column(j, col); let m_chol_updated = chol.l() * chol.l().adjoint(); - relative_eq!(m_updated, m_chol_updated, epsilon = 1.0e-7) + prop_assert!(relative_eq!(m_updated, m_chol_updated, epsilon = 1.0e-7)); } - fn cholesky_remove_column(n: usize) -> bool { + #[test] + fn cholesky_remove_column(n in PROPTEST_MATRIX_DIM) { let n = n.max(1).min(10); let j = random::() % n; let m = RandomSDP::new(Dynamic::new(n), || random::<$scalar>().0).unwrap(); @@ -127,7 +131,7 @@ macro_rules! gen_tests( // remove column from m let m_updated = m.remove_column(j).remove_row(j); - relative_eq!(m_updated, m_chol_updated, epsilon = 1.0e-7) + prop_assert!(relative_eq!(m_updated, m_chol_updated, epsilon = 1.0e-7)); } } } diff --git a/tests/linalg/col_piv_qr.rs b/tests/linalg/col_piv_qr.rs index e9ffba86..40ebfb01 100644 --- a/tests/linalg/col_piv_qr.rs +++ b/tests/linalg/col_piv_qr.rs @@ -22,133 +22,124 @@ fn col_piv_qr() { assert!(relative_eq!(m, qr, epsilon = 1.0e-7)); } -#[cfg(feature = "arbitrary")] -mod quickcheck_tests { +#[cfg(feature = "proptest-support")] +mod proptest_tests { macro_rules! gen_tests( - ($module: ident, $scalar: ty) => { + ($module: ident, $scalar: expr ,$scalar_type: ty) => { mod $module { - use na::{DMatrix, DVector, Matrix3x5, Matrix4, Matrix4x3, Matrix5x3, Vector4}; + use na::{DMatrix, DVector, Matrix4x3, Vector4}; use std::cmp; - #[allow(unused_imports)] - use crate::core::helper::{RandScalar, RandComplex}; - quickcheck! { - fn col_piv_qr(m: DMatrix<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[allow(unused_imports)] + use crate::core::helper::{RandComplex, RandScalar}; + use crate::proptest::*; + use proptest::{prop_assert, proptest}; + + proptest! { + #[test] + fn col_piv_qr(m in dmatrix_($scalar)) { let col_piv_qr = m.clone().col_piv_qr(); let (q, r, p) = col_piv_qr.unpack(); let mut qr = &q * &r; p.inv_permute_columns(&mut qr); - println!("m: {}", m); - println!("col_piv_qr: {}", &q * &r); - - relative_eq!(m, &qr, epsilon = 1.0e-7) && - q.is_orthogonal(1.0e-7) + prop_assert!(relative_eq!(m, &qr, epsilon = 1.0e-7)); + prop_assert!(q.is_orthogonal(1.0e-7)); } - fn col_piv_qr_static_5_3(m: Matrix5x3<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn col_piv_qr_static_5_3(m in matrix5x3_($scalar)) { let col_piv_qr = m.col_piv_qr(); let (q, r, p) = col_piv_qr.unpack(); let mut qr = q * r; p.inv_permute_columns(&mut qr); - relative_eq!(m, qr, epsilon = 1.0e-7) && - q.is_orthogonal(1.0e-7) + prop_assert!(relative_eq!(m, qr, epsilon = 1.0e-7)); + prop_assert!(q.is_orthogonal(1.0e-7)); } - fn col_piv_qr_static_3_5(m: Matrix3x5<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn col_piv_qr_static_3_5(m in matrix3x5_($scalar)) { let col_piv_qr = m.col_piv_qr(); let (q, r, p) = col_piv_qr.unpack(); let mut qr = q * r; p.inv_permute_columns(&mut qr); - relative_eq!(m, qr, epsilon = 1.0e-7) && - q.is_orthogonal(1.0e-7) + prop_assert!(relative_eq!(m, qr, epsilon = 1.0e-7)); + prop_assert!(q.is_orthogonal(1.0e-7)); } - fn col_piv_qr_static_square(m: Matrix4<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn col_piv_qr_static_square(m in matrix4_($scalar)) { let col_piv_qr = m.col_piv_qr(); let (q, r, p) = col_piv_qr.unpack(); let mut qr = q * r; p.inv_permute_columns(&mut qr); - println!("{}{}{}{}", q, r, qr, m); - - relative_eq!(m, qr, epsilon = 1.0e-7) && - q.is_orthogonal(1.0e-7) + prop_assert!(relative_eq!(m, qr, epsilon = 1.0e-7)); + prop_assert!(q.is_orthogonal(1.0e-7)); } - fn col_piv_qr_solve(n: usize, nb: usize) -> bool { + #[test] + fn col_piv_qr_solve(n in PROPTEST_MATRIX_DIM, nb in PROPTEST_MATRIX_DIM) { if n != 0 && nb != 0 { let n = cmp::min(n, 50); // To avoid slowing down the test too much. let nb = cmp::min(nb, 50); // To avoid slowing down the test too much. - let m = DMatrix::<$scalar>::new_random(n, n).map(|e| e.0); + let m = DMatrix::<$scalar_type>::new_random(n, n).map(|e| e.0); let col_piv_qr = m.clone().col_piv_qr(); - let b1 = DVector::<$scalar>::new_random(n).map(|e| e.0); - let b2 = DMatrix::<$scalar>::new_random(n, nb).map(|e| e.0); + let b1 = DVector::<$scalar_type>::new_random(n).map(|e| e.0); + let b2 = DMatrix::<$scalar_type>::new_random(n, nb).map(|e| e.0); if col_piv_qr.is_invertible() { let sol1 = col_piv_qr.solve(&b1).unwrap(); let sol2 = col_piv_qr.solve(&b2).unwrap(); - return relative_eq!(&m * sol1, b1, epsilon = 1.0e-6) && - relative_eq!(&m * sol2, b2, epsilon = 1.0e-6) + prop_assert!(relative_eq!(&m * sol1, b1, epsilon = 1.0e-6)); + prop_assert!(relative_eq!(&m * sol2, b2, epsilon = 1.0e-6)); } } - - return true; } - fn col_piv_qr_solve_static(m: Matrix4<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn col_piv_qr_solve_static(m in matrix4_($scalar)) { let col_piv_qr = m.col_piv_qr(); - let b1 = Vector4::<$scalar>::new_random().map(|e| e.0); - let b2 = Matrix4x3::<$scalar>::new_random().map(|e| e.0); + let b1 = Vector4::<$scalar_type>::new_random().map(|e| e.0); + let b2 = Matrix4x3::<$scalar_type>::new_random().map(|e| e.0); if col_piv_qr.is_invertible() { let sol1 = col_piv_qr.solve(&b1).unwrap(); let sol2 = col_piv_qr.solve(&b2).unwrap(); - relative_eq!(m * sol1, b1, epsilon = 1.0e-6) && - relative_eq!(m * sol2, b2, epsilon = 1.0e-6) - } - else { - false + prop_assert!(relative_eq!(m * sol1, b1, epsilon = 1.0e-6)); + prop_assert!(relative_eq!(m * sol2, b2, epsilon = 1.0e-6)); } } - fn col_piv_qr_inverse(n: usize) -> bool { + #[test] + fn col_piv_qr_inverse(n in PROPTEST_MATRIX_DIM) { let n = cmp::max(1, cmp::min(n, 15)); // To avoid slowing down the test too much. - let m = DMatrix::<$scalar>::new_random(n, n).map(|e| e.0); + let m = DMatrix::<$scalar_type>::new_random(n, n).map(|e| e.0); if let Some(m1) = m.clone().col_piv_qr().try_inverse() { let id1 = &m * &m1; let id2 = &m1 * &m; - id1.is_identity(1.0e-5) && id2.is_identity(1.0e-5) - } - else { - true + prop_assert!(id1.is_identity(1.0e-5)); + prop_assert!(id2.is_identity(1.0e-5)); } } - fn col_piv_qr_inverse_static(m: Matrix4<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn col_piv_qr_inverse_static(m in matrix4_($scalar)) { let col_piv_qr = m.col_piv_qr(); if let Some(m1) = col_piv_qr.try_inverse() { let id1 = &m * &m1; let id2 = &m1 * &m; - id1.is_identity(1.0e-5) && id2.is_identity(1.0e-5) - } - else { - true + prop_assert!(id1.is_identity(1.0e-5)); + prop_assert!(id2.is_identity(1.0e-5)); } } } @@ -156,6 +147,6 @@ mod quickcheck_tests { } ); - gen_tests!(complex, RandComplex); - gen_tests!(f64, RandScalar); + gen_tests!(complex, complex_f64(), RandComplex); + gen_tests!(f64, PROPTEST_F64, RandScalar); } diff --git a/tests/linalg/eigen.rs b/tests/linalg/eigen.rs index c0d3171f..e9c0522b 100644 --- a/tests/linalg/eigen.rs +++ b/tests/linalg/eigen.rs @@ -1,66 +1,74 @@ use na::DMatrix; -#[cfg(feature = "arbitrary")] -mod quickcheck_tests { +#[cfg(feature = "proptest-support")] +mod proptest_tests { macro_rules! gen_tests( - ($module: ident, $scalar: ty) => { + ($module: ident, $scalar: expr, $scalar_type: ty) => { mod $module { - use na::{DMatrix, Matrix2, Matrix3, Matrix4}; + use na::DMatrix; #[allow(unused_imports)] use crate::core::helper::{RandScalar, RandComplex}; use std::cmp; - quickcheck! { - fn symmetric_eigen(n: usize) -> bool { + use crate::proptest::*; + use proptest::{prop_assert, proptest}; + + proptest! { + #[test] + fn symmetric_eigen(n in PROPTEST_MATRIX_DIM) { let n = cmp::max(1, cmp::min(n, 10)); - let m = DMatrix::<$scalar>::new_random(n, n).map(|e| e.0).hermitian_part(); + let m = DMatrix::<$scalar_type>::new_random(n, n).map(|e| e.0).hermitian_part(); let eig = m.clone().symmetric_eigen(); let recomp = eig.recompose(); - relative_eq!(m.lower_triangle(), recomp.lower_triangle(), epsilon = 1.0e-5) + prop_assert!(relative_eq!(m.lower_triangle(), recomp.lower_triangle(), epsilon = 1.0e-5)) } - fn symmetric_eigen_singular(n: usize) -> bool { + #[test] + fn symmetric_eigen_singular(n in PROPTEST_MATRIX_DIM) { let n = cmp::max(1, cmp::min(n, 10)); - let mut m = DMatrix::<$scalar>::new_random(n, n).map(|e| e.0).hermitian_part(); + let mut m = DMatrix::<$scalar_type>::new_random(n, n).map(|e| e.0).hermitian_part(); m.row_mut(n / 2).fill(na::zero()); m.column_mut(n / 2).fill(na::zero()); let eig = m.clone().symmetric_eigen(); let recomp = eig.recompose(); - relative_eq!(m.lower_triangle(), recomp.lower_triangle(), epsilon = 1.0e-5) + prop_assert!(relative_eq!(m.lower_triangle(), recomp.lower_triangle(), epsilon = 1.0e-5)) } - fn symmetric_eigen_static_square_4x4(m: Matrix4<$scalar>) -> bool { - let m = m.map(|e| e.0).hermitian_part(); + #[test] + fn symmetric_eigen_static_square_4x4(m in matrix4_($scalar)) { + let m = m.hermitian_part(); let eig = m.symmetric_eigen(); let recomp = eig.recompose(); - relative_eq!(m.lower_triangle(), recomp.lower_triangle(), epsilon = 1.0e-5) + prop_assert!(relative_eq!(m.lower_triangle(), recomp.lower_triangle(), epsilon = 1.0e-5)) } - fn symmetric_eigen_static_square_3x3(m: Matrix3<$scalar>) -> bool { - let m = m.map(|e| e.0).hermitian_part(); + #[test] + fn symmetric_eigen_static_square_3x3(m in matrix3_($scalar)) { + let m = m.hermitian_part(); let eig = m.symmetric_eigen(); let recomp = eig.recompose(); - relative_eq!(m.lower_triangle(), recomp.lower_triangle(), epsilon = 1.0e-5) + prop_assert!(relative_eq!(m.lower_triangle(), recomp.lower_triangle(), epsilon = 1.0e-5)) } - fn symmetric_eigen_static_square_2x2(m: Matrix2<$scalar>) -> bool { - let m = m.map(|e| e.0).hermitian_part(); + #[test] + fn symmetric_eigen_static_square_2x2(m in matrix2_($scalar)) { + let m = m.hermitian_part(); let eig = m.symmetric_eigen(); let recomp = eig.recompose(); - relative_eq!(m.lower_triangle(), recomp.lower_triangle(), epsilon = 1.0e-5) + prop_assert!(relative_eq!(m.lower_triangle(), recomp.lower_triangle(), epsilon = 1.0e-5)) } } } } ); - gen_tests!(complex, RandComplex); - gen_tests!(f64, RandScalar); + gen_tests!(complex, complex_f64(), RandComplex); + gen_tests!(f64, PROPTEST_F64, RandScalar); } // Test proposed on the issue #176 of rulinalg. diff --git a/tests/linalg/exp.rs b/tests/linalg/exp.rs index f5b5243a..6a643037 100644 --- a/tests/linalg/exp.rs +++ b/tests/linalg/exp.rs @@ -71,7 +71,6 @@ mod tests { let m22 = ad_2.exp() * (delta * delta_2.cosh() + (d - a) * delta_2.sinh()); let f = Matrix2::new(m11, m12, m21, m22) / delta; - println!("a: {}", m); assert!(relative_eq!(f, m.exp(), epsilon = 1.0e-7)); break; } diff --git a/tests/linalg/full_piv_lu.rs b/tests/linalg/full_piv_lu.rs index 0bb832cd..f782d8fd 100644 --- a/tests/linalg/full_piv_lu.rs +++ b/tests/linalg/full_piv_lu.rs @@ -40,101 +40,96 @@ fn full_piv_lu_simple_with_pivot() { } #[cfg(feature = "arbitrary")] -mod quickcheck_tests { +mod proptest_tests { macro_rules! gen_tests( - ($module: ident, $scalar: ty) => { + ($module: ident, $scalar: expr, $scalar_type: ty) => { mod $module { use std::cmp; use num::One; - use na::{DMatrix, Matrix4, Matrix4x3, Matrix5x3, Matrix3x5, DVector, Vector4}; + use na::{DMatrix, Matrix4x3, DVector, Vector4}; #[allow(unused_imports)] use crate::core::helper::{RandScalar, RandComplex}; - quickcheck! { - fn full_piv_lu(m: DMatrix<$scalar>) -> bool { - let mut m = m.map(|e| e.0); - if m.len() == 0 { - m = DMatrix::<$scalar>::new_random(1, 1).map(|e| e.0); - } + use crate::proptest::*; + use proptest::{prop_assert, proptest}; + proptest! { + #[test] + fn full_piv_lu(m in dmatrix_($scalar)) { let lu = m.clone().full_piv_lu(); let (p, l, u, q) = lu.unpack(); let mut lu = l * u; p.inv_permute_rows(&mut lu); q.inv_permute_columns(&mut lu); - relative_eq!(m, lu, epsilon = 1.0e-7) + prop_assert!(relative_eq!(m, lu, epsilon = 1.0e-7)) } - fn full_piv_lu_static_3_5(m: Matrix3x5<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn full_piv_lu_static_3_5(m in matrix3x5_($scalar)) { let lu = m.full_piv_lu(); let (p, l, u, q) = lu.unpack(); let mut lu = l * u; p.inv_permute_rows(&mut lu); q.inv_permute_columns(&mut lu); - relative_eq!(m, lu, epsilon = 1.0e-7) + prop_assert!(relative_eq!(m, lu, epsilon = 1.0e-7)) } - fn full_piv_lu_static_5_3(m: Matrix5x3<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn full_piv_lu_static_5_3(m in matrix5x3_($scalar)) { let lu = m.full_piv_lu(); let (p, l, u, q) = lu.unpack(); let mut lu = l * u; p.inv_permute_rows(&mut lu); q.inv_permute_columns(&mut lu); - relative_eq!(m, lu, epsilon = 1.0e-7) + prop_assert!(relative_eq!(m, lu, epsilon = 1.0e-7)) } - fn full_piv_lu_static_square(m: Matrix4<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn full_piv_lu_static_square(m in matrix4_($scalar)) { let lu = m.full_piv_lu(); let (p, l, u, q) = lu.unpack(); let mut lu = l * u; p.inv_permute_rows(&mut lu); q.inv_permute_columns(&mut lu); - relative_eq!(m, lu, epsilon = 1.0e-7) + prop_assert!(relative_eq!(m, lu, epsilon = 1.0e-7)) } - fn full_piv_lu_solve(n: usize, nb: usize) -> bool { - if n != 0 && nb != 0 { - let n = cmp::min(n, 50); // To avoid slowing down the test too much. - let nb = cmp::min(nb, 50); // To avoid slowing down the test too much. - let m = DMatrix::<$scalar>::new_random(n, n).map(|e| e.0); + #[test] + fn full_piv_lu_solve(n in PROPTEST_MATRIX_DIM, nb in PROPTEST_MATRIX_DIM) { + let m = DMatrix::<$scalar_type>::new_random(n, n).map(|e| e.0); - let lu = m.clone().full_piv_lu(); - let b1 = DVector::<$scalar>::new_random(n).map(|e| e.0); - let b2 = DMatrix::<$scalar>::new_random(n, nb).map(|e| e.0); + let lu = m.clone().full_piv_lu(); + let b1 = DVector::<$scalar_type>::new_random(n).map(|e| e.0); + let b2 = DMatrix::<$scalar_type>::new_random(n, nb).map(|e| e.0); - let sol1 = lu.solve(&b1); - let sol2 = lu.solve(&b2); + let sol1 = lu.solve(&b1); + let sol2 = lu.solve(&b2); - return (sol1.is_none() || relative_eq!(&m * sol1.unwrap(), b1, epsilon = 1.0e-6)) && - (sol2.is_none() || relative_eq!(&m * sol2.unwrap(), b2, epsilon = 1.0e-6)) - } - - return true; + prop_assert!(sol1.is_none() || relative_eq!(&m * sol1.unwrap(), b1, epsilon = 1.0e-6)); + prop_assert!(sol2.is_none() || relative_eq!(&m * sol2.unwrap(), b2, epsilon = 1.0e-6)); } - fn full_piv_lu_solve_static(m: Matrix4<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn full_piv_lu_solve_static(m in matrix4_($scalar)) { let lu = m.full_piv_lu(); - let b1 = Vector4::<$scalar>::new_random().map(|e| e.0); - let b2 = Matrix4x3::<$scalar>::new_random().map(|e| e.0); + let b1 = Vector4::<$scalar_type>::new_random().map(|e| e.0); + let b2 = Matrix4x3::<$scalar_type>::new_random().map(|e| e.0); let sol1 = lu.solve(&b1); let sol2 = lu.solve(&b2); - return (sol1.is_none() || relative_eq!(&m * sol1.unwrap(), b1, epsilon = 1.0e-6)) && - (sol2.is_none() || relative_eq!(&m * sol2.unwrap(), b2, epsilon = 1.0e-6)) + prop_assert!(sol1.is_none() || relative_eq!(&m * sol1.unwrap(), b1, epsilon = 1.0e-6)); + prop_assert!(sol2.is_none() || relative_eq!(&m * sol2.unwrap(), b2, epsilon = 1.0e-6)); } - fn full_piv_lu_inverse(n: usize) -> bool { + #[test] + fn full_piv_lu_inverse(n in PROPTEST_MATRIX_DIM) { let n = cmp::max(1, cmp::min(n, 15)); // To avoid slowing down the test too much. - let m = DMatrix::<$scalar>::new_random(n, n).map(|e| e.0); + let m = DMatrix::<$scalar_type>::new_random(n, n).map(|e| e.0); let mut l = m.lower_triangle(); let mut u = m.upper_triangle(); @@ -148,21 +143,20 @@ mod quickcheck_tests { let id1 = &m * &m1; let id2 = &m1 * &m; - return id1.is_identity(1.0e-5) && id2.is_identity(1.0e-5); + prop_assert!(id1.is_identity(1.0e-5)); + prop_assert!(id2.is_identity(1.0e-5)); } - fn full_piv_lu_inverse_static(m: Matrix4<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn full_piv_lu_inverse_static(m in matrix4_($scalar)) { let lu = m.full_piv_lu(); if let Some(m1) = lu.try_inverse() { let id1 = &m * &m1; let id2 = &m1 * &m; - id1.is_identity(1.0e-5) && id2.is_identity(1.0e-5) - } - else { - true + prop_assert!(id1.is_identity(1.0e-5)); + prop_assert!(id2.is_identity(1.0e-5)); } } } @@ -170,8 +164,8 @@ mod quickcheck_tests { } ); - gen_tests!(complex, RandComplex); - gen_tests!(f64, RandScalar); + gen_tests!(complex, complex_f64(), RandComplex); + gen_tests!(f64, PROPTEST_F64, RandScalar); } /* diff --git a/tests/linalg/hessenberg.rs b/tests/linalg/hessenberg.rs index ec499f82..1dc35c3b 100644 --- a/tests/linalg/hessenberg.rs +++ b/tests/linalg/hessenberg.rs @@ -1,4 +1,4 @@ -#![cfg(feature = "arbitrary")] +#![cfg(feature = "proptest-support")] use na::Matrix2; @@ -11,40 +11,39 @@ fn hessenberg_simple() { } macro_rules! gen_tests( - ($module: ident, $scalar: ty) => { + ($module: ident, $scalar: expr) => { mod $module { - use na::{DMatrix, Matrix2, Matrix4}; - use std::cmp; #[allow(unused_imports)] use crate::core::helper::{RandScalar, RandComplex}; - quickcheck! { - fn hessenberg(n: usize) -> bool { - let n = cmp::max(1, cmp::min(n, 50)); - let m = DMatrix::<$scalar>::new_random(n, n).map(|e| e.0); + use crate::proptest::*; + use proptest::{prop_assert, proptest}; + proptest! { + #[test] + fn hessenberg(m in dmatrix_($scalar)) { let hess = m.clone().hessenberg(); let (p, h) = hess.unpack(); - relative_eq!(m, &p * h * p.adjoint(), epsilon = 1.0e-7) + prop_assert!(relative_eq!(m, &p * h * p.adjoint(), epsilon = 1.0e-7)) } - fn hessenberg_static_mat2(m: Matrix2<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn hessenberg_static_mat2(m in matrix2_($scalar)) { let hess = m.hessenberg(); let (p, h) = hess.unpack(); - relative_eq!(m, p * h * p.adjoint(), epsilon = 1.0e-7) + prop_assert!(relative_eq!(m, p * h * p.adjoint(), epsilon = 1.0e-7)) } - fn hessenberg_static(m: Matrix4<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn hessenberg_static(m in matrix4_($scalar)) { let hess = m.hessenberg(); let (p, h) = hess.unpack(); - relative_eq!(m, p * h * p.adjoint(), epsilon = 1.0e-7) + prop_assert!(relative_eq!(m, p * h * p.adjoint(), epsilon = 1.0e-7)) } } } } ); -gen_tests!(complex, RandComplex); -gen_tests!(f64, RandScalar); +gen_tests!(complex, complex_f64()); +gen_tests!(f64, PROPTEST_F64); diff --git a/tests/linalg/lu.rs b/tests/linalg/lu.rs index 7fab6b01..69708e3b 100644 --- a/tests/linalg/lu.rs +++ b/tests/linalg/lu.rs @@ -38,103 +38,90 @@ fn lu_simple_with_pivot() { assert!(relative_eq!(m, lu, epsilon = 1.0e-7)); } -#[cfg(feature = "arbitrary")] -mod quickcheck_tests { - #[allow(unused_imports)] - use crate::core::helper::{RandComplex, RandScalar}; - +#[cfg(feature = "proptest-support")] +mod proptest_tests { macro_rules! gen_tests( - ($module: ident, $scalar: ty) => { + ($module: ident, $scalar: expr, $scalar_type: ty) => { mod $module { use std::cmp; - use na::{DMatrix, Matrix4, Matrix4x3, Matrix5x3, Matrix3x5, DVector, Vector4}; + use na::{DMatrix, Matrix4x3, DVector, Vector4}; #[allow(unused_imports)] use crate::core::helper::{RandScalar, RandComplex}; + use crate::proptest::*; + use proptest::{prop_assert, proptest}; - quickcheck! { - fn lu(m: DMatrix<$scalar>) -> bool { - let mut m = m; - if m.len() == 0 { - m = DMatrix::<$scalar>::new_random(1, 1); - } - - let m = m.map(|e| e.0); - + proptest! { + #[test] + fn lu(m in dmatrix_($scalar)) { let lu = m.clone().lu(); let (p, l, u) = lu.unpack(); let mut lu = l * u; p.inv_permute_rows(&mut lu); - relative_eq!(m, lu, epsilon = 1.0e-7) + prop_assert!(relative_eq!(m, lu, epsilon = 1.0e-7)) } - fn lu_static_3_5(m: Matrix3x5<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn lu_static_3_5(m in matrix3x5_($scalar)) { let lu = m.lu(); let (p, l, u) = lu.unpack(); let mut lu = l * u; p.inv_permute_rows(&mut lu); - relative_eq!(m, lu, epsilon = 1.0e-7) + prop_assert!(relative_eq!(m, lu, epsilon = 1.0e-7)) } - fn lu_static_5_3(m: Matrix5x3<$scalar>) -> bool { - let m = m.map(|e| e.0); + fn lu_static_5_3(m in matrix5x3_($scalar)) { let lu = m.lu(); let (p, l, u) = lu.unpack(); let mut lu = l * u; p.inv_permute_rows(&mut lu); - relative_eq!(m, lu, epsilon = 1.0e-7) + prop_assert!(relative_eq!(m, lu, epsilon = 1.0e-7)); } - fn lu_static_square(m: Matrix4<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn lu_static_square(m in matrix4_($scalar)) { let lu = m.lu(); let (p, l, u) = lu.unpack(); let mut lu = l * u; p.inv_permute_rows(&mut lu); - relative_eq!(m, lu, epsilon = 1.0e-7) + prop_assert!(relative_eq!(m, lu, epsilon = 1.0e-7)); } - fn lu_solve(n: usize, nb: usize) -> bool { - if n != 0 && nb != 0 { - let n = cmp::min(n, 50); // To avoid slowing down the test too much. - let nb = cmp::min(nb, 50); // To avoid slowing down the test too much. - let m = DMatrix::<$scalar>::new_random(n, n).map(|e| e.0); + #[test] + fn lu_solve(n in PROPTEST_MATRIX_DIM, nb in PROPTEST_MATRIX_DIM) { + let n = cmp::min(n, 50); // To avoid slowing down the test too much. + let nb = cmp::min(nb, 50); // To avoid slowing down the test too much. + let m = DMatrix::<$scalar_type>::new_random(n, n).map(|e| e.0); - let lu = m.clone().lu(); - let b1 = DVector::<$scalar>::new_random(n).map(|e| e.0); - let b2 = DMatrix::<$scalar>::new_random(n, nb).map(|e| e.0); + let lu = m.clone().lu(); + let b1 = DVector::<$scalar_type>::new_random(n).map(|e| e.0); + let b2 = DMatrix::<$scalar_type>::new_random(n, nb).map(|e| e.0); - let sol1 = lu.solve(&b1); - let sol2 = lu.solve(&b2); + let sol1 = lu.solve(&b1); + let sol2 = lu.solve(&b2); - return (sol1.is_none() || relative_eq!(&m * sol1.unwrap(), b1, epsilon = 1.0e-6)) && - (sol2.is_none() || relative_eq!(&m * sol2.unwrap(), b2, epsilon = 1.0e-6)) - } - - return true; + prop_assert!(sol1.is_none() || relative_eq!(&m * sol1.unwrap(), b1, epsilon = 1.0e-6)); + prop_assert!(sol2.is_none() || relative_eq!(&m * sol2.unwrap(), b2, epsilon = 1.0e-6)); } - fn lu_solve_static(m: Matrix4<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn lu_solve_static(m in matrix4_($scalar)) { let lu = m.lu(); - let b1 = Vector4::<$scalar>::new_random().map(|e| e.0); - let b2 = Matrix4x3::<$scalar>::new_random().map(|e| e.0); + let b1 = Vector4::<$scalar_type>::new_random().map(|e| e.0); + let b2 = Matrix4x3::<$scalar_type>::new_random().map(|e| e.0); let sol1 = lu.solve(&b1); let sol2 = lu.solve(&b2); - return (sol1.is_none() || relative_eq!(&m * sol1.unwrap(), b1, epsilon = 1.0e-6)) && - (sol2.is_none() || relative_eq!(&m * sol2.unwrap(), b2, epsilon = 1.0e-6)) + prop_assert!(sol1.is_none() || relative_eq!(&m * sol1.unwrap(), b1, epsilon = 1.0e-6)); + prop_assert!(sol2.is_none() || relative_eq!(&m * sol2.unwrap(), b2, epsilon = 1.0e-6)); } - fn lu_inverse(n: usize) -> bool { - let n = cmp::max(1, cmp::min(n, 15)); // To avoid slowing down the test too much. - let m = DMatrix::<$scalar>::new_random(n, n).map(|e| e.0); - + #[test] + fn lu_inverse(m in dmatrix_($scalar)) { let mut l = m.lower_triangle(); let mut u = m.upper_triangle(); @@ -147,21 +134,20 @@ mod quickcheck_tests { let id1 = &m * &m1; let id2 = &m1 * &m; - return id1.is_identity(1.0e-5) && id2.is_identity(1.0e-5); + prop_assert!(id1.is_identity(1.0e-5)); + prop_assert!(id2.is_identity(1.0e-5)); } - fn lu_inverse_static(m: Matrix4<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn lu_inverse_static(m in matrix4_($scalar)) { let lu = m.lu(); if let Some(m1) = lu.try_inverse() { let id1 = &m * &m1; let id2 = &m1 * &m; - id1.is_identity(1.0e-5) && id2.is_identity(1.0e-5) - } - else { - true + prop_assert!(id1.is_identity(1.0e-5)); + prop_assert!(id2.is_identity(1.0e-5)); } } } @@ -169,6 +155,6 @@ mod quickcheck_tests { } ); - gen_tests!(complex, RandComplex); - gen_tests!(f64, RandScalar); + gen_tests!(complex, complex_f64(), RandComplex); + gen_tests!(f64, PROPTEST_F64, RandScalar); } diff --git a/tests/linalg/qr.rs b/tests/linalg/qr.rs index a6e54af4..f499b030 100644 --- a/tests/linalg/qr.rs +++ b/tests/linalg/qr.rs @@ -1,126 +1,112 @@ -#![cfg(feature = "arbitrary")] +#![cfg(feature = "proptest-support")] macro_rules! gen_tests( - ($module: ident, $scalar: ty) => { + ($module: ident, $scalar: expr, $scalar_type: ty) => { mod $module { - use na::{DMatrix, DVector, Matrix3x5, Matrix4, Matrix4x3, Matrix5x3, Vector4}; + use na::{DMatrix, DVector, Matrix4x3, Vector4}; use std::cmp; #[allow(unused_imports)] use crate::core::helper::{RandScalar, RandComplex}; + use crate::proptest::*; + use proptest::{prop_assert, proptest}; - quickcheck! { - fn qr(m: DMatrix<$scalar>) -> bool { - let m = m.map(|e| e.0); + proptest! { + #[test] + fn qr(m in dmatrix_($scalar)) { let qr = m.clone().qr(); let q = qr.q(); let r = qr.r(); - println!("m: {}", m); - println!("qr: {}", &q * &r); - - relative_eq!(m, &q * r, epsilon = 1.0e-7) && - q.is_orthogonal(1.0e-7) + prop_assert!(relative_eq!(m, &q * r, epsilon = 1.0e-7)); + prop_assert!(q.is_orthogonal(1.0e-7)); } - fn qr_static_5_3(m: Matrix5x3<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn qr_static_5_3(m in matrix5x3_($scalar)) { let qr = m.qr(); let q = qr.q(); let r = qr.r(); - relative_eq!(m, q * r, epsilon = 1.0e-7) && - q.is_orthogonal(1.0e-7) + prop_assert!(relative_eq!(m, q * r, epsilon = 1.0e-7)); + prop_assert!(q.is_orthogonal(1.0e-7)); } - fn qr_static_3_5(m: Matrix3x5<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn qr_static_3_5(m in matrix3x5_($scalar)) { let qr = m.qr(); let q = qr.q(); let r = qr.r(); - relative_eq!(m, q * r, epsilon = 1.0e-7) && - q.is_orthogonal(1.0e-7) + prop_assert!(relative_eq!(m, q * r, epsilon = 1.0e-7)); + prop_assert!(q.is_orthogonal(1.0e-7)); } - fn qr_static_square(m: Matrix4<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn qr_static_square(m in matrix4_($scalar)) { let qr = m.qr(); let q = qr.q(); let r = qr.r(); - println!("{}{}{}{}", q, r, q * r, m); - - relative_eq!(m, q * r, epsilon = 1.0e-7) && - q.is_orthogonal(1.0e-7) + prop_assert!(relative_eq!(m, q * r, epsilon = 1.0e-7)); + prop_assert!(q.is_orthogonal(1.0e-7)); } - fn qr_solve(n: usize, nb: usize) -> bool { - if n != 0 && nb != 0 { - let n = cmp::min(n, 50); // To avoid slowing down the test too much. - let nb = cmp::min(nb, 50); // To avoid slowing down the test too much. - let m = DMatrix::<$scalar>::new_random(n, n).map(|e| e.0); + #[test] + fn qr_solve(n in PROPTEST_MATRIX_DIM, nb in PROPTEST_MATRIX_DIM) { + let m = DMatrix::<$scalar_type>::new_random(n, n).map(|e| e.0); - let qr = m.clone().qr(); - let b1 = DVector::<$scalar>::new_random(n).map(|e| e.0); - let b2 = DMatrix::<$scalar>::new_random(n, nb).map(|e| e.0); + let qr = m.clone().qr(); + let b1 = DVector::<$scalar_type>::new_random(n).map(|e| e.0); + let b2 = DMatrix::<$scalar_type>::new_random(n, nb).map(|e| e.0); - if qr.is_invertible() { - let sol1 = qr.solve(&b1).unwrap(); - let sol2 = qr.solve(&b2).unwrap(); + if qr.is_invertible() { + let sol1 = qr.solve(&b1).unwrap(); + let sol2 = qr.solve(&b2).unwrap(); - return relative_eq!(&m * sol1, b1, epsilon = 1.0e-6) && - relative_eq!(&m * sol2, b2, epsilon = 1.0e-6) - } + prop_assert!(relative_eq!(&m * sol1, b1, epsilon = 1.0e-6)); + prop_assert!(relative_eq!(&m * sol2, b2, epsilon = 1.0e-6)); } - - return true; } - fn qr_solve_static(m: Matrix4<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn qr_solve_static(m in matrix4_($scalar)) { let qr = m.qr(); - let b1 = Vector4::<$scalar>::new_random().map(|e| e.0); - let b2 = Matrix4x3::<$scalar>::new_random().map(|e| e.0); + let b1 = Vector4::<$scalar_type>::new_random().map(|e| e.0); + let b2 = Matrix4x3::<$scalar_type>::new_random().map(|e| e.0); if qr.is_invertible() { let sol1 = qr.solve(&b1).unwrap(); let sol2 = qr.solve(&b2).unwrap(); - relative_eq!(m * sol1, b1, epsilon = 1.0e-6) && - relative_eq!(m * sol2, b2, epsilon = 1.0e-6) - } - else { - false + prop_assert!(relative_eq!(m * sol1, b1, epsilon = 1.0e-6)); + prop_assert!(relative_eq!(m * sol2, b2, epsilon = 1.0e-6)); } } - fn qr_inverse(n: usize) -> bool { + #[test] + fn qr_inverse(n in PROPTEST_MATRIX_DIM) { let n = cmp::max(1, cmp::min(n, 15)); // To avoid slowing down the test too much. - let m = DMatrix::<$scalar>::new_random(n, n).map(|e| e.0); + let m = DMatrix::<$scalar_type>::new_random(n, n).map(|e| e.0); if let Some(m1) = m.clone().qr().try_inverse() { let id1 = &m * &m1; let id2 = &m1 * &m; - id1.is_identity(1.0e-5) && id2.is_identity(1.0e-5) - } - else { - true + prop_assert!(id1.is_identity(1.0e-5)); + prop_assert!(id2.is_identity(1.0e-5)); } } - fn qr_inverse_static(m: Matrix4<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn qr_inverse_static(m in matrix4_($scalar)) { let qr = m.qr(); if let Some(m1) = qr.try_inverse() { let id1 = &m * &m1; let id2 = &m1 * &m; - id1.is_identity(1.0e-5) && id2.is_identity(1.0e-5) - } - else { - true + prop_assert!(id1.is_identity(1.0e-5)); + prop_assert!(id2.is_identity(1.0e-5)); } } } @@ -128,5 +114,5 @@ macro_rules! gen_tests( } ); -gen_tests!(complex, RandComplex); -gen_tests!(f64, RandScalar); +gen_tests!(complex, complex_f64(), RandComplex); +gen_tests!(f64, PROPTEST_F64, RandScalar); diff --git a/tests/linalg/schur.rs b/tests/linalg/schur.rs index 2086ce2d..a4cf5361 100644 --- a/tests/linalg/schur.rs +++ b/tests/linalg/schur.rs @@ -4,8 +4,8 @@ use na::{DMatrix, Matrix3, Matrix4}; #[rustfmt::skip] fn schur_simpl_mat3() { let m = Matrix3::new(-2.0, -4.0, 2.0, - -2.0, 1.0, 2.0, - 4.0, 2.0, 5.0); + -2.0, 1.0, 2.0, + 4.0, 2.0, 5.0); let schur = m.schur(); let (vecs, vals) = schur.unpack(); @@ -13,72 +13,47 @@ fn schur_simpl_mat3() { assert!(relative_eq!(vecs * vals * vecs.transpose(), m, epsilon = 1.0e-7)); } -#[cfg(feature = "arbitrary")] -mod quickcheck_tests { +#[cfg(feature = "proptest-support")] +mod proptest_tests { macro_rules! gen_tests( - ($module: ident, $scalar: ty) => { + ($module: ident, $scalar: expr) => { mod $module { - use std::cmp; - use na::{DMatrix, Matrix2, Matrix3, Matrix4}; #[allow(unused_imports)] use crate::core::helper::{RandScalar, RandComplex}; + use crate::proptest::*; + use proptest::{prop_assert, proptest}; - quickcheck! { - fn schur(n: usize) -> bool { - let n = cmp::max(1, cmp::min(n, 10)); - let m = DMatrix::<$scalar>::new_random(n, n).map(|e| e.0); - + proptest! { + #[test] + fn schur(m in dmatrix_($scalar)) { let (vecs, vals) = m.clone().schur().unpack(); - - if !relative_eq!(&vecs * &vals * vecs.adjoint(), m, epsilon = 1.0e-7) { - println!("{:.5}{:.5}", m, &vecs * &vals * vecs.adjoint()); - } - - relative_eq!(&vecs * vals * vecs.adjoint(), m, epsilon = 1.0e-7) + prop_assert!(relative_eq!(&vecs * vals * vecs.adjoint(), m, epsilon = 1.0e-7)); } - fn schur_static_mat2(m: Matrix2<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn schur_static_mat2(m in matrix2_($scalar)) { let (vecs, vals) = m.clone().schur().unpack(); - - let ok = relative_eq!(vecs * vals * vecs.adjoint(), m, epsilon = 1.0e-7); - if !ok { - println!("Vecs: {:.5} Vals: {:.5}", vecs, vals); - println!("Reconstruction:{}{}", m, &vecs * &vals * vecs.adjoint()); - } - ok + prop_assert!(relative_eq!(vecs * vals * vecs.adjoint(), m, epsilon = 1.0e-7)); } - fn schur_static_mat3(m: Matrix3<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn schur_static_mat3(m in matrix3_($scalar)) { let (vecs, vals) = m.clone().schur().unpack(); - - let ok = relative_eq!(vecs * vals * vecs.adjoint(), m, epsilon = 1.0e-7); - if !ok { - println!("Vecs: {:.5} Vals: {:.5}", vecs, vals); - println!("{:.5}{:.5}", m, &vecs * &vals * vecs.adjoint()); - } - ok + prop_assert!(relative_eq!(vecs * vals * vecs.adjoint(), m, epsilon = 1.0e-7)); } - fn schur_static_mat4(m: Matrix4<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn schur_static_mat4(m in matrix4_($scalar)) { let (vecs, vals) = m.clone().schur().unpack(); - - let ok = relative_eq!(vecs * vals * vecs.adjoint(), m, epsilon = 1.0e-7); - if !ok { - println!("{:.5}{:.5}", m, &vecs * &vals * vecs.adjoint()); - } - - ok + prop_assert!(relative_eq!(vecs * vals * vecs.adjoint(), m, epsilon = 1.0e-7)); } } } } ); - gen_tests!(complex, RandComplex); - gen_tests!(f64, RandScalar); + gen_tests!(complex, complex_f64()); + gen_tests!(f64, PROPTEST_F64); } #[test] diff --git a/tests/linalg/solve.rs b/tests/linalg/solve.rs index 3bd6075e..81cd1c71 100644 --- a/tests/linalg/solve.rs +++ b/tests/linalg/solve.rs @@ -1,11 +1,13 @@ -#![cfg(feature = "arbitrary")] +#![cfg(feature = "proptest-support")] macro_rules! gen_tests( - ($module: ident, $scalar: ty) => { + ($module: ident, $scalar: expr) => { mod $module { - use na::{Matrix4, Matrix4x5, ComplexField}; + use na::{Matrix4, ComplexField}; #[allow(unused_imports)] use crate::core::helper::{RandScalar, RandComplex}; + use crate::proptest::*; + use proptest::{prop_assert, proptest}; fn unzero_diagonal(a: &mut Matrix4) { for i in 0..4 { @@ -15,50 +17,50 @@ macro_rules! gen_tests( } } - quickcheck! { - fn solve_lower_triangular(a: Matrix4<$scalar>, b: Matrix4x5<$scalar>) -> bool { - let b = b.map(|e| e.0); - let mut a = a.map(|e| e.0); + proptest! { + #[test] + fn solve_lower_triangular(a in matrix4_($scalar), b in matrix4x5_($scalar)) { + let mut a = a; unzero_diagonal(&mut a); let tri = a.lower_triangle(); let x = a.solve_lower_triangular(&b).unwrap(); - relative_eq!(tri * x, b, epsilon = 1.0e-7) + prop_assert!(relative_eq!(tri * x, b, epsilon = 1.0e-7)) } - fn solve_upper_triangular(a: Matrix4<$scalar>, b: Matrix4x5<$scalar>) -> bool { - let b = b.map(|e| e.0); - let mut a = a.map(|e| e.0); + #[test] + fn solve_upper_triangular(a in matrix4_($scalar), b in matrix4x5_($scalar)) { + let mut a = a; unzero_diagonal(&mut a); let tri = a.upper_triangle(); let x = a.solve_upper_triangular(&b).unwrap(); - relative_eq!(tri * x, b, epsilon = 1.0e-7) + prop_assert!(relative_eq!(tri * x, b, epsilon = 1.0e-7)) } - fn tr_solve_lower_triangular(a: Matrix4<$scalar>, b: Matrix4x5<$scalar>) -> bool { - let b = b.map(|e| e.0); - let mut a = a.map(|e| e.0); + #[test] + fn tr_solve_lower_triangular(a in matrix4_($scalar), b in matrix4x5_($scalar)) { + let mut a = a; unzero_diagonal(&mut a); let tri = a.lower_triangle(); let x = a.tr_solve_lower_triangular(&b).unwrap(); - relative_eq!(tri.transpose() * x, b, epsilon = 1.0e-7) + prop_assert!(relative_eq!(tri.transpose() * x, b, epsilon = 1.0e-7)) } - fn tr_solve_upper_triangular(a: Matrix4<$scalar>, b: Matrix4x5<$scalar>) -> bool { - let b = b.map(|e| e.0); - let mut a = a.map(|e| e.0); + #[test] + fn tr_solve_upper_triangular(a in matrix4_($scalar), b in matrix4x5_($scalar)) { + let mut a = a; unzero_diagonal(&mut a); let tri = a.upper_triangle(); let x = a.tr_solve_upper_triangular(&b).unwrap(); - relative_eq!(tri.transpose() * x, b, epsilon = 1.0e-7) + prop_assert!(relative_eq!(tri.transpose() * x, b, epsilon = 1.0e-7)) } } } } ); -gen_tests!(complex, RandComplex); -gen_tests!(f64, RandScalar); +gen_tests!(complex, complex_f64()); +gen_tests!(f64, PROPTEST_F64); diff --git a/tests/linalg/svd.rs b/tests/linalg/svd.rs index cd44b61d..80aa6a20 100644 --- a/tests/linalg/svd.rs +++ b/tests/linalg/svd.rs @@ -1,162 +1,143 @@ use na::{DMatrix, Matrix6}; -#[cfg(feature = "arbitrary")] -mod quickcheck_tests { +#[cfg(feature = "proptest-support")] +mod proptest_tests { macro_rules! gen_tests( - ($module: ident, $scalar: ty) => { + ($module: ident, $scalar: expr, $scalar_type: ty) => { mod $module { use na::{ - DMatrix, DVector, Matrix2, Matrix2x5, Matrix3, Matrix3x5, Matrix4, Matrix5x2, Matrix5x3, + DMatrix, DVector, Matrix2, Matrix3, Matrix4, ComplexField }; use std::cmp; #[allow(unused_imports)] use crate::core::helper::{RandScalar, RandComplex}; + use crate::proptest::*; + use proptest::{prop_assert, proptest}; - quickcheck! { - fn svd(m: DMatrix<$scalar>) -> bool { - let m = m.map(|e| e.0); - if m.len() > 0 { - let svd = m.clone().svd(true, true); - let recomp_m = svd.clone().recompose().unwrap(); - let (u, s, v_t) = (svd.u.unwrap(), svd.singular_values, svd.v_t.unwrap()); - let ds = DMatrix::from_diagonal(&s.map(|e| ComplexField::from_real(e))); + proptest! { + #[test] + fn svd(m in dmatrix_($scalar)) { + let svd = m.clone().svd(true, true); + let recomp_m = svd.clone().recompose().unwrap(); + let (u, s, v_t) = (svd.u.unwrap(), svd.singular_values, svd.v_t.unwrap()); + let ds = DMatrix::from_diagonal(&s.map(|e| ComplexField::from_real(e))); - s.iter().all(|e| *e >= 0.0) && - relative_eq!(&u * ds * &v_t, recomp_m, epsilon = 1.0e-5) && - relative_eq!(m, recomp_m, epsilon = 1.0e-5) - } - else { - true - } + prop_assert!(s.iter().all(|e| *e >= 0.0)); + prop_assert!(relative_eq!(&u * ds * &v_t, recomp_m, epsilon = 1.0e-5)); + prop_assert!(relative_eq!(m, recomp_m, epsilon = 1.0e-5)); } - fn svd_static_5_3(m: Matrix5x3<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn svd_static_5_3(m in matrix5x3_($scalar)) { let svd = m.svd(true, true); let (u, s, v_t) = (svd.u.unwrap(), svd.singular_values, svd.v_t.unwrap()); let ds = Matrix3::from_diagonal(&s.map(|e| ComplexField::from_real(e))); - s.iter().all(|e| *e >= 0.0) && - relative_eq!(m, &u * ds * &v_t, epsilon = 1.0e-5) && - u.is_orthogonal(1.0e-5) && - v_t.is_orthogonal(1.0e-5) + prop_assert!(s.iter().all(|e| *e >= 0.0)); + prop_assert!(relative_eq!(m, &u * ds * &v_t, epsilon = 1.0e-5)); + prop_assert!(u.is_orthogonal(1.0e-5)); + prop_assert!(v_t.is_orthogonal(1.0e-5)); } - fn svd_static_5_2(m: Matrix5x2<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn svd_static_5_2(m in matrix5x2_($scalar)) { let svd = m.svd(true, true); let (u, s, v_t) = (svd.u.unwrap(), svd.singular_values, svd.v_t.unwrap()); let ds = Matrix2::from_diagonal(&s.map(|e| ComplexField::from_real(e))); - s.iter().all(|e| *e >= 0.0) && - relative_eq!(m, &u * ds * &v_t, epsilon = 1.0e-5) && - u.is_orthogonal(1.0e-5) && - v_t.is_orthogonal(1.0e-5) + prop_assert!(s.iter().all(|e| *e >= 0.0)); + prop_assert!(relative_eq!(m, &u * ds * &v_t, epsilon = 1.0e-5)); + prop_assert!(u.is_orthogonal(1.0e-5)); + prop_assert!(v_t.is_orthogonal(1.0e-5)); } - fn svd_static_3_5(m: Matrix3x5<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn svd_static_3_5(m in matrix3x5_($scalar)) { let svd = m.svd(true, true); let (u, s, v_t) = (svd.u.unwrap(), svd.singular_values, svd.v_t.unwrap()); let ds = Matrix3::from_diagonal(&s.map(|e| ComplexField::from_real(e))); - s.iter().all(|e| *e >= 0.0) && - relative_eq!(m, u * ds * v_t, epsilon = 1.0e-5) + prop_assert!(s.iter().all(|e| *e >= 0.0)); + prop_assert!(relative_eq!(m, u * ds * v_t, epsilon = 1.0e-5)); } - fn svd_static_2_5(m: Matrix2x5<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn svd_static_2_5(m in matrix2x5_($scalar)) { let svd = m.svd(true, true); let (u, s, v_t) = (svd.u.unwrap(), svd.singular_values, svd.v_t.unwrap()); let ds = Matrix2::from_diagonal(&s.map(|e| ComplexField::from_real(e))); - s.iter().all(|e| *e >= 0.0) && - relative_eq!(m, u * ds * v_t, epsilon = 1.0e-5) + prop_assert!(s.iter().all(|e| *e >= 0.0)); + prop_assert!(relative_eq!(m, u * ds * v_t, epsilon = 1.0e-5)); } - fn svd_static_square(m: Matrix4<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn svd_static_square(m in matrix4_($scalar)) { let svd = m.svd(true, true); let (u, s, v_t) = (svd.u.unwrap(), svd.singular_values, svd.v_t.unwrap()); let ds = Matrix4::from_diagonal(&s.map(|e| ComplexField::from_real(e))); - s.iter().all(|e| *e >= 0.0) && - relative_eq!(m, u * ds * v_t, epsilon = 1.0e-5) && - u.is_orthogonal(1.0e-5) && - v_t.is_orthogonal(1.0e-5) + prop_assert!(s.iter().all(|e| *e >= 0.0)); + prop_assert!(relative_eq!(m, u * ds * v_t, epsilon = 1.0e-5)); + prop_assert!(u.is_orthogonal(1.0e-5)); + prop_assert!(v_t.is_orthogonal(1.0e-5)); } - fn svd_static_square_2x2(m: Matrix2<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn svd_static_square_2x2(m in matrix2_($scalar)) { let svd = m.svd(true, true); let (u, s, v_t) = (svd.u.unwrap(), svd.singular_values, svd.v_t.unwrap()); let ds = Matrix2::from_diagonal(&s.map(|e| ComplexField::from_real(e))); - s.iter().all(|e| *e >= 0.0) && - relative_eq!(m, u * ds * v_t, epsilon = 1.0e-5) && - u.is_orthogonal(1.0e-5) && - v_t.is_orthogonal(1.0e-5) + prop_assert!(s.iter().all(|e| *e >= 0.0)); + prop_assert!(relative_eq!(m, u * ds * v_t, epsilon = 1.0e-5)); + prop_assert!(u.is_orthogonal(1.0e-5)); + prop_assert!(v_t.is_orthogonal(1.0e-5)); } - fn svd_pseudo_inverse(m: DMatrix<$scalar>) -> bool { - let m = m.map(|e| e.0); + #[test] + fn svd_pseudo_inverse(m in dmatrix_($scalar)) { + let svd = m.clone().svd(true, true); + let pinv = svd.pseudo_inverse(1.0e-10).unwrap(); - if m.len() > 0 { - let svd = m.clone().svd(true, true); - let pinv = svd.pseudo_inverse(1.0e-10).unwrap(); - - if m.nrows() > m.ncols() { - (pinv * m).is_identity(1.0e-5) - } - else { - (m * pinv).is_identity(1.0e-5) - } - } - else { - true + if m.nrows() > m.ncols() { + prop_assert!((pinv * m).is_identity(1.0e-5)) + } else { + prop_assert!((m * pinv).is_identity(1.0e-5)) } } - fn svd_solve(n: usize, nb: usize) -> bool { + #[test] + fn svd_solve(n in PROPTEST_MATRIX_DIM, nb in PROPTEST_MATRIX_DIM) { let n = cmp::max(1, cmp::min(n, 10)); let nb = cmp::min(nb, 10); - let m = DMatrix::<$scalar>::new_random(n, n).map(|e| e.0); + let m = DMatrix::<$scalar_type>::new_random(n, n).map(|e| e.0); let svd = m.clone().svd(true, true); if svd.rank(1.0e-7) == n { - let b1 = DVector::<$scalar>::new_random(n).map(|e| e.0); - let b2 = DMatrix::<$scalar>::new_random(n, nb).map(|e| e.0); + let b1 = DVector::<$scalar_type>::new_random(n).map(|e| e.0); + let b2 = DMatrix::<$scalar_type>::new_random(n, nb).map(|e| e.0); let sol1 = svd.solve(&b1, 1.0e-7).unwrap(); let sol2 = svd.solve(&b2, 1.0e-7).unwrap(); let recomp = svd.recompose().unwrap(); - if !relative_eq!(m, recomp, epsilon = 1.0e-6) { - println!("{}{}", m, recomp); - } - if !relative_eq!(&m * &sol1, b1, epsilon = 1.0e-6) { - println!("Problem 1: {:.6}{:.6}", b1, &m * sol1); - return false; - } - if !relative_eq!(&m * &sol2, b2, epsilon = 1.0e-6) { - println!("Problem 2: {:.6}{:.6}", b2, &m * sol2); - return false; - } + prop_assert!(relative_eq!(m, recomp, epsilon = 1.0e-6)); + prop_assert!(relative_eq!(&m * &sol1, b1, epsilon = 1.0e-6)); + prop_assert!(relative_eq!(&m * &sol2, b2, epsilon = 1.0e-6)); } - - true } } } } ); - gen_tests!(complex, RandComplex); - gen_tests!(f64, RandScalar); + gen_tests!(complex, complex_f64(), RandComplex); + gen_tests!(f64, PROPTEST_F64, RandScalar); } // Test proposed on the issue #176 of rulinalg. @@ -303,31 +284,31 @@ fn svd_identity() { #[rustfmt::skip] fn svd_with_delimited_subproblem() { let mut m = DMatrix::::from_element(10, 10, 0.0); - m[(0,0)] = 1.0; m[(0,1)] = 2.0; - m[(1,1)] = 0.0; m[(1,2)] = 3.0; - m[(2,2)] = 4.0; m[(2,3)] = 5.0; - m[(3,3)] = 6.0; m[(3,4)] = 0.0; - m[(4,4)] = 8.0; m[(3,5)] = 9.0; - m[(5,5)] = 10.0; m[(3,6)] = 11.0; - m[(6,6)] = 12.0; m[(3,7)] = 12.0; - m[(7,7)] = 14.0; m[(3,8)] = 13.0; - m[(8,8)] = 16.0; m[(3,9)] = 17.0; - m[(9,9)] = 18.0; + m[(0, 0)] = 1.0; m[(0, 1)] = 2.0; + m[(1, 1)] = 0.0; m[(1, 2)] = 3.0; + m[(2, 2)] = 4.0; m[(2, 3)] = 5.0; + m[(3, 3)] = 6.0; m[(3, 4)] = 0.0; + m[(4, 4)] = 8.0; m[(3, 5)] = 9.0; + m[(5, 5)] = 10.0; m[(3, 6)] = 11.0; + m[(6, 6)] = 12.0; m[(3, 7)] = 12.0; + m[(7, 7)] = 14.0; m[(3, 8)] = 13.0; + m[(8, 8)] = 16.0; m[(3, 9)] = 17.0; + m[(9, 9)] = 18.0; let svd = m.clone().svd(true, true); assert_relative_eq!(m, svd.recompose().unwrap(), epsilon = 1.0e-7); // Rectangular versions. let mut m = DMatrix::::from_element(15, 10, 0.0); - m[(0,0)] = 1.0; m[(0,1)] = 2.0; - m[(1,1)] = 0.0; m[(1,2)] = 3.0; - m[(2,2)] = 4.0; m[(2,3)] = 5.0; - m[(3,3)] = 6.0; m[(3,4)] = 0.0; - m[(4,4)] = 8.0; m[(3,5)] = 9.0; - m[(5,5)] = 10.0; m[(3,6)] = 11.0; - m[(6,6)] = 12.0; m[(3,7)] = 12.0; - m[(7,7)] = 14.0; m[(3,8)] = 13.0; - m[(8,8)] = 16.0; m[(3,9)] = 17.0; - m[(9,9)] = 18.0; + m[(0, 0)] = 1.0; m[(0, 1)] = 2.0; + m[(1, 1)] = 0.0; m[(1, 2)] = 3.0; + m[(2, 2)] = 4.0; m[(2, 3)] = 5.0; + m[(3, 3)] = 6.0; m[(3, 4)] = 0.0; + m[(4, 4)] = 8.0; m[(3, 5)] = 9.0; + m[(5, 5)] = 10.0; m[(3, 6)] = 11.0; + m[(6, 6)] = 12.0; m[(3, 7)] = 12.0; + m[(7, 7)] = 14.0; m[(3, 8)] = 13.0; + m[(8, 8)] = 16.0; m[(3, 9)] = 17.0; + m[(9, 9)] = 18.0; let svd = m.clone().svd(true, true); assert_relative_eq!(m, svd.recompose().unwrap(), epsilon = 1.0e-7); diff --git a/tests/linalg/tridiagonal.rs b/tests/linalg/tridiagonal.rs index b787fd12..3f06fe8e 100644 --- a/tests/linalg/tridiagonal.rs +++ b/tests/linalg/tridiagonal.rs @@ -1,54 +1,56 @@ -#![cfg(feature = "arbitrary")] +#![cfg(feature = "proptest-support")] macro_rules! gen_tests( - ($module: ident, $scalar: ty) => { + ($module: ident, $scalar: expr) => { mod $module { - use std::cmp; - - use na::{DMatrix, Matrix2, Matrix4}; #[allow(unused_imports)] use crate::core::helper::{RandScalar, RandComplex}; + use crate::proptest::*; + use proptest::{prop_assert, proptest}; - quickcheck! { - fn symm_tridiagonal(n: usize) -> bool { - let n = cmp::max(1, cmp::min(n, 50)); - let m = DMatrix::<$scalar>::new_random(n, n).map(|e| e.0).hermitian_part(); + proptest! { + #[test] + fn symm_tridiagonal(m in dmatrix_($scalar)) { + let m = &m * m.adjoint(); let tri = m.clone().symmetric_tridiagonalize(); let recomp = tri.recompose(); - relative_eq!(m.lower_triangle(), recomp.lower_triangle(), epsilon = 1.0e-7) + prop_assert!(relative_eq!(m.lower_triangle(), recomp.lower_triangle(), epsilon = 1.0e-7)); } - fn symm_tridiagonal_singular(n: usize) -> bool { - let n = cmp::max(1, cmp::min(n, 4)); - let mut m = DMatrix::<$scalar>::new_random(n, n).map(|e| e.0).hermitian_part(); + #[test] + fn symm_tridiagonal_singular(m in dmatrix_($scalar)) { + let mut m = &m * m.adjoint(); + let n = m.nrows(); m.row_mut(n / 2).fill(na::zero()); m.column_mut(n / 2).fill(na::zero()); let tri = m.clone().symmetric_tridiagonalize(); let recomp = tri.recompose(); - relative_eq!(m.lower_triangle(), recomp.lower_triangle(), epsilon = 1.0e-7) + prop_assert!(relative_eq!(m.lower_triangle(), recomp.lower_triangle(), epsilon = 1.0e-7)); } - fn symm_tridiagonal_static_square(m: Matrix4<$scalar>) -> bool { - let m = m.map(|e| e.0).hermitian_part(); + #[test] + fn symm_tridiagonal_static_square(m in matrix4_($scalar)) { + let m = m.hermitian_part(); let tri = m.symmetric_tridiagonalize(); let recomp = tri.recompose(); - relative_eq!(m.lower_triangle(), recomp.lower_triangle(), epsilon = 1.0e-7) + prop_assert!(relative_eq!(m.lower_triangle(), recomp.lower_triangle(), epsilon = 1.0e-7)); } - fn symm_tridiagonal_static_square_2x2(m: Matrix2<$scalar>) -> bool { - let m = m.map(|e| e.0).hermitian_part(); + #[test] + fn symm_tridiagonal_static_square_2x2(m in matrix2_($scalar)) { + let m = m.hermitian_part(); let tri = m.symmetric_tridiagonalize(); let recomp = tri.recompose(); - relative_eq!(m.lower_triangle(), recomp.lower_triangle(), epsilon = 1.0e-7) + prop_assert!(relative_eq!(m.lower_triangle(), recomp.lower_triangle(), epsilon = 1.0e-7)); } } } } ); -gen_tests!(complex, RandComplex); -gen_tests!(f64, RandScalar); +gen_tests!(complex, complex_f64()); +gen_tests!(f64, PROPTEST_F64); diff --git a/tests/linalg/udu.rs b/tests/linalg/udu.rs index 006cc95f..a7bda1a4 100644 --- a/tests/linalg/udu.rs +++ b/tests/linalg/udu.rs @@ -8,7 +8,7 @@ fn udu_simple() { -1.0, 2.0, -1.0, 0.0, -1.0, 2.0); - let udu = m.udu(); + let udu = m.udu().unwrap(); // Rebuild let p = udu.u * udu.d_matrix() * udu.u.transpose(); @@ -23,50 +23,54 @@ fn udu_non_sym_panic() { let m = Matrix3::new( 2.0, -1.0, 0.0, 1.0, -2.0, 3.0, - -2.0, 1.0, 0.0); + -2.0, 1.0, 0.3); - let udu = m.udu(); + let udu = m.udu().unwrap(); // Rebuild let p = udu.u * udu.d_matrix() * udu.u.transpose(); assert!(relative_eq!(m, p, epsilon = 3.0e-16)); } -#[cfg(feature = "arbitrary")] -mod quickcheck_tests { +#[cfg(feature = "proptest-support")] +mod proptest_tests { #[allow(unused_imports)] use crate::core::helper::{RandComplex, RandScalar}; macro_rules! gen_tests( - ($module: ident, $scalar: ty) => { + ($module: ident, $scalar: expr) => { mod $module { - use na::{DMatrix, Matrix4}; #[allow(unused_imports)] use crate::core::helper::{RandScalar, RandComplex}; + use crate::proptest::*; + use proptest::{prop_assert, proptest}; - quickcheck! { - fn udu(n: usize) -> bool { - let n = std::cmp::max(1, std::cmp::min(n, 10)); - let m = DMatrix::<$scalar>::new_random(n, n).map(|e| e.0).hermitian_part(); + proptest! { + #[test] + fn udu(m in dmatrix_($scalar)) { + let m = &m * m.adjoint(); - let udu = m.clone().udu(); - let p = &udu.u * &udu.d_matrix() * &udu.u.transpose(); + if let Some(udu) = m.clone().udu() { + let p = &udu.u * &udu.d_matrix() * &udu.u.transpose(); + println!("m: {}, p: {}", m, p); - relative_eq!(m, p, epsilon = 1.0e-7) + prop_assert!(relative_eq!(m, p, epsilon = 1.0e-7)); + } } - fn udu_static(m: Matrix4<$scalar>) -> bool { - let m = m.map(|e| e.0).hermitian_part(); + #[test] + fn udu_static(m in matrix4_($scalar)) { + let m = m.hermitian_part(); - let udu = m.udu(); - let p = udu.u * udu.d_matrix() * udu.u.transpose(); - - relative_eq!(m, p, epsilon = 1.0e-7) + if let Some(udu) = m.udu() { + let p = udu.u * udu.d_matrix() * udu.u.transpose(); + prop_assert!(relative_eq!(m, p, epsilon = 1.0e-7)); + } } } } } ); - gen_tests!(f64, RandScalar); + gen_tests!(f64, PROPTEST_F64); } diff --git a/tests/proptest/mod.rs b/tests/proptest/mod.rs index fbe14ee7..26bb6129 100644 --- a/tests/proptest/mod.rs +++ b/tests/proptest/mod.rs @@ -1,10 +1,161 @@ //! Tests for proptest-related functionality. +use nalgebra::allocator::Allocator; use nalgebra::base::dimension::*; -use nalgebra::proptest::{matrix, DimRange, MatrixStrategy}; -use nalgebra::{DMatrix, DVector, Dim, Matrix3, MatrixMN, Vector3}; +use nalgebra::proptest::{DimRange, MatrixStrategy}; +use nalgebra::{ + DMatrix, DVector, DefaultAllocator, Dim, DualQuaternion, Isometry2, Isometry3, Matrix3, + MatrixMN, Point2, Point3, Quaternion, Rotation2, Rotation3, Scalar, Similarity3, Translation2, + Translation3, UnitComplex, UnitDualQuaternion, UnitQuaternion, Vector3, U2, U3, U4, U7, U8, +}; +use num_complex::Complex; use proptest::prelude::*; -use proptest::strategy::ValueTree; +use proptest::strategy::{Strategy, ValueTree}; use proptest::test_runner::TestRunner; +use std::ops::RangeInclusive; + +pub const PROPTEST_MATRIX_DIM: RangeInclusive = 1..=20; +pub const PROPTEST_F64: RangeInclusive = -100.0..=100.0; + +pub use nalgebra::proptest::{matrix, vector}; + +pub fn point2() -> impl Strategy> { + vector2().prop_map(|v| Point2::from(v)) +} + +pub fn point3() -> impl Strategy> { + vector3().prop_map(|v| Point3::from(v)) +} + +pub fn translation2() -> impl Strategy> { + vector2().prop_map(|v| Translation2::from(v)) +} + +pub fn translation3() -> impl Strategy> { + vector3().prop_map(|v| Translation3::from(v)) +} + +pub fn rotation2() -> impl Strategy> { + PROPTEST_F64.prop_map(|v| Rotation2::new(v)) +} + +pub fn rotation3() -> impl Strategy> { + vector3().prop_map(|v| Rotation3::new(v)) +} + +pub fn unit_complex() -> impl Strategy> { + PROPTEST_F64.prop_map(|v| UnitComplex::new(v)) +} + +pub fn isometry2() -> impl Strategy> { + vector3().prop_map(|v| Isometry2::new(v.xy(), v.z)) +} + +pub fn isometry3() -> impl Strategy> { + vector6().prop_map(|v| Isometry3::new(v.xyz(), Vector3::new(v.w, v.a, v.b))) +} + +// pub fn similarity2() -> impl Strategy> { +// vector4().prop_map(|v| Similarity2::new(v.xy(), v.z, v.w)) +// } + +pub fn similarity3() -> impl Strategy> { + vector(PROPTEST_F64, U7) + .prop_map(|v| Similarity3::new(v.xyz(), Vector3::new(v[3], v[4], v[5]), v[6])) +} + +pub fn unit_dual_quaternion() -> impl Strategy> { + isometry3().prop_map(|iso| UnitDualQuaternion::from_isometry(&iso)) +} + +pub fn dual_quaternion() -> impl Strategy> { + vector(PROPTEST_F64, U8).prop_map(|v| { + DualQuaternion::from_real_and_dual( + Quaternion::new(v[0], v[1], v[2], v[3]), + Quaternion::new(v[4], v[5], v[6], v[7]), + ) + }) +} + +pub fn quaternion() -> impl Strategy> { + vector4().prop_map(|v| Quaternion::from(v)) +} + +pub fn unit_quaternion() -> impl Strategy> { + vector3().prop_map(|v| UnitQuaternion::new(v)) +} + +pub fn complex_f64() -> impl Strategy> + Clone { + vector(PROPTEST_F64, U2).prop_map(|v| Complex::new(v.x, v.y)) +} + +pub fn dmatrix() -> impl Strategy> { + matrix(PROPTEST_F64, PROPTEST_MATRIX_DIM, PROPTEST_MATRIX_DIM) +} + +pub fn dvector() -> impl Strategy> { + vector(PROPTEST_F64, PROPTEST_MATRIX_DIM) +} + +pub fn dmatrix_( + scalar_strategy: ScalarStrategy, +) -> impl Strategy> +where + ScalarStrategy: Strategy + Clone + 'static, + ScalarStrategy::Value: Scalar, + DefaultAllocator: Allocator, +{ + matrix(scalar_strategy, PROPTEST_MATRIX_DIM, PROPTEST_MATRIX_DIM) +} + +// pub fn dvector_(range: RangeInclusive) -> impl Strategy> +// where +// RangeInclusive: Strategy, +// T: Scalar + PartialEq + Copy, +// DefaultAllocator: Allocator, +// { +// vector(range, PROPTEST_MATRIX_DIM) +// } + +macro_rules! define_strategies( + ($($strategy_: ident $strategy: ident<$nrows: ident, $ncols: ident>),*) => {$( + #[allow(dead_code)] + pub fn $strategy() -> impl Strategy> { + matrix(PROPTEST_F64, $nrows, $ncols) + } + + #[allow(dead_code)] + pub fn $strategy_(scalar_strategy: ScalarStrategy) -> impl Strategy> + where + ScalarStrategy: Strategy + Clone + 'static, + ScalarStrategy::Value: Scalar, + DefaultAllocator: Allocator { + matrix(scalar_strategy, $nrows, $ncols) + } + )*} +); + +define_strategies!( + matrix1_ matrix1, + matrix2_ matrix2, + matrix3_ matrix3, + matrix4_ matrix4, + matrix5_ matrix5, + matrix6_ matrix6, + + matrix5x2_ matrix5x2, + matrix2x5_ matrix2x5, + matrix5x3_ matrix5x3, + matrix3x5_ matrix3x5, + matrix5x4_ matrix5x4, + matrix4x5_ matrix4x5, + + vector1_ vector1, + vector2_ vector2, + vector3_ vector3, + vector4_ vector4, + vector5_ vector5, + vector6_ vector6 +); /// Generate a proptest that tests that all matrices generated with the /// provided rows and columns conform to the constraints defined by the diff --git a/tests/sparse/cs_conversion.rs b/tests/sparse/cs_conversion.rs index f08fe758..895650dc 100644 --- a/tests/sparse/cs_conversion.rs +++ b/tests/sparse/cs_conversion.rs @@ -43,7 +43,6 @@ fn cs_matrix_from_triplet() { ); let cs_mat = CsMatrix::from_triplet(4, 5, &irows, &icols, &vals); - println!("Mat from triplet: {:?}", cs_mat); assert!(cs_mat.is_sorted()); assert_eq!(cs_mat, cs_expected); @@ -62,7 +61,6 @@ fn cs_matrix_from_triplet() { } let cs_mat = CsMatrix::from_triplet(4, 5, &irows, &icols, &vals); - println!("Mat from triplet: {:?}", cs_mat); assert!(cs_mat.is_sorted()); assert_eq!(cs_mat, cs_expected); @@ -80,7 +78,6 @@ fn cs_matrix_from_triplet() { vals.append(&mut va); let cs_mat = CsMatrix::from_triplet(4, 5, &irows, &icols, &vals); - println!("Mat from triplet: {:?}", cs_mat); assert!(cs_mat.is_sorted()); assert_eq!(cs_mat, cs_expected * 2.0); diff --git a/tests/sparse/cs_matrix_market.rs b/tests/sparse/cs_matrix_market.rs index 12414b37..7c0cee43 100644 --- a/tests/sparse/cs_matrix_market.rs +++ b/tests/sparse/cs_matrix_market.rs @@ -41,7 +41,6 @@ fn cs_matrix_market() { "#; let cs_mat = io::cs_matrix_from_matrix_market_str(file_str).unwrap(); - println!("CS mat: {:?}", cs_mat); let mat: DMatrix<_> = cs_mat.into(); let expected = DMatrix::from_row_slice(5, 5, &[ 1.0, 0.0, 0.0, 6.0, 0.0, From 74f4b0ba4d65f732cfe7d5e4df8e22e2348365b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Sun, 28 Feb 2021 18:39:18 +0100 Subject: [PATCH 166/183] nalgebra-lapack: run tests with proptest instead of quickcheck. --- nalgebra-lapack/Cargo.toml | 5 +- nalgebra-lapack/tests/lib.rs | 10 +- nalgebra-lapack/tests/linalg/cholesky.rs | 115 ++++++++--------- nalgebra-lapack/tests/linalg/hessenberg.rs | 44 +++---- nalgebra-lapack/tests/linalg/lu.rs | 118 ++++++++---------- nalgebra-lapack/tests/linalg/qr.rs | 16 ++- .../tests/linalg/real_eigensystem.rs | 58 ++++----- nalgebra-lapack/tests/linalg/schur.rs | 18 +-- nalgebra-lapack/tests/linalg/svd.rs | 54 ++++---- .../tests/linalg/symmetric_eigen.rs | 17 ++- tests/proptest/mod.rs | 5 +- 11 files changed, 224 insertions(+), 236 deletions(-) diff --git a/nalgebra-lapack/Cargo.toml b/nalgebra-lapack/Cargo.toml index cefc6662..16f4faa1 100644 --- a/nalgebra-lapack/Cargo.toml +++ b/nalgebra-lapack/Cargo.toml @@ -18,9 +18,11 @@ maintenance = { status = "actively-developed" } [features] serde-serialize = [ "serde", "serde_derive" ] +proptest-support = [ "nalgebra/proptest-support" ] +arbitrary = [ "nalgebra/arbitrary" ] # For BLAS/LAPACK -default = ["openblas"] +default = ["netlib"] openblas = ["lapack-src/openblas"] netlib = ["lapack-src/netlib"] accelerate = ["lapack-src/accelerate"] @@ -39,6 +41,7 @@ lapack-src = { version = "0.6", default-features = false } [dev-dependencies] nalgebra = { version = "0.24", features = [ "arbitrary" ], path = ".." } +proptest = { version = "1", default-features = false, features = ["std"] } quickcheck = "1" approx = "0.4" rand = "0.8" diff --git a/nalgebra-lapack/tests/lib.rs b/nalgebra-lapack/tests/lib.rs index 37e0b903..973b6d17 100644 --- a/nalgebra-lapack/tests/lib.rs +++ b/nalgebra-lapack/tests/lib.rs @@ -1,8 +1,14 @@ #[macro_use] extern crate approx; +#[cfg(not(feature = "proptest-support"))] +compile_error!("Tests must be run with `proptest-support`"); + extern crate nalgebra as na; extern crate nalgebra_lapack as nl; -#[macro_use] -extern crate quickcheck; + +extern crate lapack; +extern crate lapack_src; mod linalg; +#[path = "../../tests/proptest/mod.rs"] +mod proptest; diff --git a/nalgebra-lapack/tests/linalg/cholesky.rs b/nalgebra-lapack/tests/linalg/cholesky.rs index f811726a..632347b8 100644 --- a/nalgebra-lapack/tests/linalg/cholesky.rs +++ b/nalgebra-lapack/tests/linalg/cholesky.rs @@ -1,101 +1,90 @@ use std::cmp; -use na::{DMatrix, DVector, Matrix3, Matrix4, Matrix4x3, Vector4}; +use na::{DMatrix, DVector, Matrix4x3, Vector4}; use nl::Cholesky; -quickcheck! { - fn cholesky(m: DMatrix) -> bool { - if m.len() != 0 { - let m = &m * m.transpose(); - if let Some(chol) = Cholesky::new(m.clone()) { - let l = chol.unpack(); - let reconstructed_m = &l * l.transpose(); +use crate::proptest::*; +use proptest::{prop_assert, proptest}; - return relative_eq!(reconstructed_m, m, epsilon = 1.0e-7) - } +proptest! { + #[test] + fn cholesky(m in dmatrix()) { + let m = &m * m.transpose(); + if let Some(chol) = Cholesky::new(m.clone()) { + let l = chol.unpack(); + let reconstructed_m = &l * l.transpose(); + + prop_assert!(relative_eq!(reconstructed_m, m, epsilon = 1.0e-7)); } - return true } - fn cholesky_static(m: Matrix3) -> bool { + #[test] + fn cholesky_static(m in matrix3()) { let m = &m * m.transpose(); if let Some(chol) = Cholesky::new(m) { let l = chol.unpack(); let reconstructed_m = &l * l.transpose(); - relative_eq!(reconstructed_m, m, epsilon = 1.0e-7) - } - else { - false + prop_assert!(relative_eq!(reconstructed_m, m, epsilon = 1.0e-7)) } } - fn cholesky_solve(n: usize, nb: usize) -> bool { - if n != 0 { - let n = cmp::min(n, 15); // To avoid slowing down the test too much. - let nb = cmp::min(nb, 15); // To avoid slowing down the test too much. - let m = DMatrix::::new_random(n, n); - let m = &m * m.transpose(); + #[test] + fn cholesky_solve(n in PROPTEST_MATRIX_DIM, nb in PROPTEST_MATRIX_DIM) { + let n = cmp::min(n, 15); // To avoid slowing down the test too much. + let nb = cmp::min(nb, 15); // To avoid slowing down the test too much. + let m = DMatrix::::new_random(n, n); + let m = &m * m.transpose(); - if let Some(chol) = Cholesky::new(m.clone()) { - let b1 = DVector::new_random(n); - let b2 = DMatrix::new_random(n, nb); + if let Some(chol) = Cholesky::new(m.clone()) { + let b1 = DVector::new_random(n); + let b2 = DMatrix::new_random(n, nb); - let sol1 = chol.solve(&b1).unwrap(); - let sol2 = chol.solve(&b2).unwrap(); + let sol1 = chol.solve(&b1).unwrap(); + let sol2 = chol.solve(&b2).unwrap(); - return relative_eq!(&m * sol1, b1, epsilon = 1.0e-6) && - relative_eq!(&m * sol2, b2, epsilon = 1.0e-6) - } + prop_assert!(relative_eq!(&m * sol1, b1, epsilon = 1.0e-6)); + prop_assert!(relative_eq!(&m * sol2, b2, epsilon = 1.0e-6)); } - - return true; } - fn cholesky_solve_static(m: Matrix4) -> bool { + #[test] + fn cholesky_solve_static(m in matrix4()) { let m = &m * m.transpose(); - match Cholesky::new(m) { - Some(chol) => { - let b1 = Vector4::new_random(); - let b2 = Matrix4x3::new_random(); + if let Some(chol) = Cholesky::new(m) { + let b1 = Vector4::new_random(); + let b2 = Matrix4x3::new_random(); - let sol1 = chol.solve(&b1).unwrap(); - let sol2 = chol.solve(&b2).unwrap(); + let sol1 = chol.solve(&b1).unwrap(); + let sol2 = chol.solve(&b2).unwrap(); - relative_eq!(m * sol1, b1, epsilon = 1.0e-7) && - relative_eq!(m * sol2, b2, epsilon = 1.0e-7) - }, - None => true + prop_assert!(relative_eq!(m * sol1, b1, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(m * sol2, b2, epsilon = 1.0e-7)); } } - fn cholesky_inverse(n: usize) -> bool { - if n != 0 { - let n = cmp::min(n, 15); // To avoid slowing down the test too much. - let m = DMatrix::::new_random(n, n); - let m = &m * m.transpose(); + #[test] + fn cholesky_inverse(n in PROPTEST_MATRIX_DIM) { + let n = cmp::min(n, 15); // To avoid slowing down the test too much. + let m = DMatrix::::new_random(n, n); + let m = &m * m.transpose(); - if let Some(m1) = Cholesky::new(m.clone()).unwrap().inverse() { - let id1 = &m * &m1; - let id2 = &m1 * &m; + if let Some(m1) = Cholesky::new(m.clone()).unwrap().inverse() { + let id1 = &m * &m1; + let id2 = &m1 * &m; - return id1.is_identity(1.0e-6) && id2.is_identity(1.0e-6); - } + prop_assert!(id1.is_identity(1.0e-6) && id2.is_identity(1.0e-6)); } - - return true; } - fn cholesky_inverse_static(m: Matrix4) -> bool { + #[test] + fn cholesky_inverse_static(m in matrix4()) { let m = m * m.transpose(); - match Cholesky::new(m.clone()).unwrap().inverse() { - Some(m1) => { - let id1 = &m * &m1; - let id2 = &m1 * &m; + if let Some(m1) = Cholesky::new(m.clone()).unwrap().inverse() { + let id1 = &m * &m1; + let id2 = &m1 * &m; - id1.is_identity(1.0e-5) && id2.is_identity(1.0e-5) - }, - None => true + prop_assert!(id1.is_identity(1.0e-5) && id2.is_identity(1.0e-5)) } } } diff --git a/nalgebra-lapack/tests/linalg/hessenberg.rs b/nalgebra-lapack/tests/linalg/hessenberg.rs index bb48633e..5292ab17 100644 --- a/nalgebra-lapack/tests/linalg/hessenberg.rs +++ b/nalgebra-lapack/tests/linalg/hessenberg.rs @@ -1,38 +1,32 @@ use std::cmp; -use nl::Hessenberg; use na::{DMatrix, Matrix4}; +use nl::Hessenberg; -quickcheck!{ - fn hessenberg(n: usize) -> bool { - if n != 0 { - let n = cmp::min(n, 25); - let m = DMatrix::::new_random(n, n); +use crate::proptest::*; +use proptest::{prop_assert, proptest}; - match Hessenberg::new(m.clone()) { - Some(hess) => { - let h = hess.h(); - let p = hess.p(); +proptest! { + #[test] + fn hessenberg(n in PROPTEST_MATRIX_DIM) { + let n = cmp::min(n, 25); + let m = DMatrix::::new_random(n, n); - relative_eq!(m, &p * h * p.transpose(), epsilon = 1.0e-7) - }, - None => true - } - } - else { - true + if let Some(hess) = Hessenberg::new(m.clone()) { + let h = hess.h(); + let p = hess.p(); + + prop_assert!(relative_eq!(m, &p * h * p.transpose(), epsilon = 1.0e-7)) } } - fn hessenberg_static(m: Matrix4) -> bool { - match Hessenberg::new(m) { - Some(hess) => { - let h = hess.h(); - let p = hess.p(); + #[test] + fn hessenberg_static(m in matrix4()) { + if let Some(hess) = Hessenberg::new(m) { + let h = hess.h(); + let p = hess.p(); - relative_eq!(m, p * h * p.transpose(), epsilon = 1.0e-7) - }, - None => true + prop_assert!(relative_eq!(m, p * h * p.transpose(), epsilon = 1.0e-7)) } } } diff --git a/nalgebra-lapack/tests/linalg/lu.rs b/nalgebra-lapack/tests/linalg/lu.rs index 71293436..9665964e 100644 --- a/nalgebra-lapack/tests/linalg/lu.rs +++ b/nalgebra-lapack/tests/linalg/lu.rs @@ -1,28 +1,28 @@ use std::cmp; -use na::{DMatrix, DVector, Matrix3x4, Matrix4, Matrix4x3, Vector4}; +use na::{DMatrix, DVector, Matrix4x3, Vector4}; use nl::LU; -quickcheck! { - fn lup(m: DMatrix) -> bool { - if m.len() != 0 { - let lup = LU::new(m.clone()); - let l = lup.l(); - let u = lup.u(); - let mut computed1 = &l * &u; - lup.permute(&mut computed1); +use crate::proptest::*; +use proptest::{prop_assert, proptest}; - let computed2 = lup.p() * l * u; +proptest! { + #[test] + fn lup(m in dmatrix()) { + let lup = LU::new(m.clone()); + let l = lup.l(); + let u = lup.u(); + let mut computed1 = &l * &u; + lup.permute(&mut computed1); - relative_eq!(computed1, m, epsilon = 1.0e-7) && - relative_eq!(computed2, m, epsilon = 1.0e-7) - } - else { - true - } + let computed2 = lup.p() * l * u; + + prop_assert!(relative_eq!(computed1, m, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(computed2, m, epsilon = 1.0e-7)); } - fn lu_static(m: Matrix3x4) -> bool { + #[test] + fn lu_static(m in matrix3x5()) { let lup = LU::new(m); let l = lup.l(); let u = lup.u(); @@ -31,37 +31,34 @@ quickcheck! { let computed2 = lup.p() * l * u; - relative_eq!(computed1, m, epsilon = 1.0e-7) && - relative_eq!(computed2, m, epsilon = 1.0e-7) + prop_assert!(relative_eq!(computed1, m, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(computed2, m, epsilon = 1.0e-7)); } - fn lu_solve(n: usize, nb: usize) -> bool { - if n != 0 { - let n = cmp::min(n, 25); // To avoid slowing down the test too much. - let nb = cmp::min(nb, 25); // To avoid slowing down the test too much. - let m = DMatrix::::new_random(n, n); + #[test] + fn lu_solve(n in PROPTEST_MATRIX_DIM, nb in PROPTEST_MATRIX_DIM) { + let n = cmp::min(n, 25); // To avoid slowing down the test too much. + let nb = cmp::min(nb, 25); // To avoid slowing down the test too much. + let m = DMatrix::::new_random(n, n); - let lup = LU::new(m.clone()); - let b1 = DVector::new_random(n); - let b2 = DMatrix::new_random(n, nb); + let lup = LU::new(m.clone()); + let b1 = DVector::new_random(n); + let b2 = DMatrix::new_random(n, nb); - let sol1 = lup.solve(&b1).unwrap(); - let sol2 = lup.solve(&b2).unwrap(); + let sol1 = lup.solve(&b1).unwrap(); + let sol2 = lup.solve(&b2).unwrap(); - let tr_sol1 = lup.solve_transpose(&b1).unwrap(); - let tr_sol2 = lup.solve_transpose(&b2).unwrap(); + let tr_sol1 = lup.solve_transpose(&b1).unwrap(); + let tr_sol2 = lup.solve_transpose(&b2).unwrap(); - relative_eq!(&m * sol1, b1, epsilon = 1.0e-7) && - relative_eq!(&m * sol2, b2, epsilon = 1.0e-7) && - relative_eq!(m.transpose() * tr_sol1, b1, epsilon = 1.0e-7) && - relative_eq!(m.transpose() * tr_sol2, b2, epsilon = 1.0e-7) - } - else { - true - } + prop_assert!(relative_eq!(&m * sol1, b1, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(&m * sol2, b2, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(m.transpose() * tr_sol1, b1, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(m.transpose() * tr_sol2, b2, epsilon = 1.0e-7)); } - fn lu_solve_static(m: Matrix4) -> bool { + #[test] + fn lu_solve_static(m in matrix4()) { let lup = LU::new(m); let b1 = Vector4::new_random(); let b2 = Matrix4x3::new_random(); @@ -71,37 +68,32 @@ quickcheck! { let tr_sol1 = lup.solve_transpose(&b1).unwrap(); let tr_sol2 = lup.solve_transpose(&b2).unwrap(); - relative_eq!(m * sol1, b1, epsilon = 1.0e-7) && - relative_eq!(m * sol2, b2, epsilon = 1.0e-7) && - relative_eq!(m.transpose() * tr_sol1, b1, epsilon = 1.0e-7) && - relative_eq!(m.transpose() * tr_sol2, b2, epsilon = 1.0e-7) + prop_assert!(relative_eq!(m * sol1, b1, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(m * sol2, b2, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(m.transpose() * tr_sol1, b1, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(m.transpose() * tr_sol2, b2, epsilon = 1.0e-7)); } - fn lu_inverse(n: usize) -> bool { - if n != 0 { - let n = cmp::min(n, 25); // To avoid slowing down the test too much. - let m = DMatrix::::new_random(n, n); + #[test] + fn lu_inverse(n in PROPTEST_MATRIX_DIM) { + let n = cmp::min(n, 25); // To avoid slowing down the test too much. + let m = DMatrix::::new_random(n, n); - if let Some(m1) = LU::new(m.clone()).inverse() { - let id1 = &m * &m1; - let id2 = &m1 * &m; + if let Some(m1) = LU::new(m.clone()).inverse() { + let id1 = &m * &m1; + let id2 = &m1 * &m; - return id1.is_identity(1.0e-7) && id2.is_identity(1.0e-7); - } + prop_assert!(id1.is_identity(1.0e-7) && id2.is_identity(1.0e-7)); } - - return true; } - fn lu_inverse_static(m: Matrix4) -> bool { - match LU::new(m.clone()).inverse() { - Some(m1) => { - let id1 = &m * &m1; - let id2 = &m1 * &m; + #[test] + fn lu_inverse_static(m in matrix4()) { + if let Some(m1) = LU::new(m.clone()).inverse() { + let id1 = &m * &m1; + let id2 = &m1 * &m; - id1.is_identity(1.0e-5) && id2.is_identity(1.0e-5) - }, - None => true + prop_assert!(id1.is_identity(1.0e-5) && id2.is_identity(1.0e-5)) } } } diff --git a/nalgebra-lapack/tests/linalg/qr.rs b/nalgebra-lapack/tests/linalg/qr.rs index 1d193a86..138d38e9 100644 --- a/nalgebra-lapack/tests/linalg/qr.rs +++ b/nalgebra-lapack/tests/linalg/qr.rs @@ -1,20 +1,24 @@ -use na::{DMatrix, Matrix4x3}; use nl::QR; -quickcheck! { - fn qr(m: DMatrix) -> bool { +use crate::proptest::*; +use proptest::{prop_assert, proptest}; + +proptest! { + #[test] + fn qr(m in dmatrix()) { let qr = QR::new(m.clone()); let q = qr.q(); let r = qr.r(); - relative_eq!(m, q * r, epsilon = 1.0e-7) + prop_assert!(relative_eq!(m, q * r, epsilon = 1.0e-7)) } - fn qr_static(m: Matrix4x3) -> bool { + #[test] + fn qr_static(m in matrix5x3()) { let qr = QR::new(m); let q = qr.q(); let r = qr.r(); - relative_eq!(m, q * r, epsilon = 1.0e-7) + prop_assert!(relative_eq!(m, q * r, epsilon = 1.0e-7)) } } diff --git a/nalgebra-lapack/tests/linalg/real_eigensystem.rs b/nalgebra-lapack/tests/linalg/real_eigensystem.rs index a711d882..3d1c91eb 100644 --- a/nalgebra-lapack/tests/linalg/real_eigensystem.rs +++ b/nalgebra-lapack/tests/linalg/real_eigensystem.rs @@ -3,46 +3,40 @@ use std::cmp; use na::{DMatrix, Matrix4}; use nl::Eigen; -quickcheck! { - fn eigensystem(n: usize) -> bool { - if n != 0 { - let n = cmp::min(n, 25); - let m = DMatrix::::new_random(n, n); +use crate::proptest::*; +use proptest::{prop_assert, proptest}; - match Eigen::new(m.clone(), true, true) { - Some(eig) => { - let eigvals = DMatrix::from_diagonal(&eig.eigenvalues); - let transformed_eigvectors = &m * eig.eigenvectors.as_ref().unwrap(); - let scaled_eigvectors = eig.eigenvectors.as_ref().unwrap() * &eigvals; +proptest! { + #[test] + fn eigensystem(n in PROPTEST_MATRIX_DIM) { + let n = cmp::min(n, 25); + let m = DMatrix::::new_random(n, n); - let transformed_left_eigvectors = m.transpose() * eig.left_eigenvectors.as_ref().unwrap(); - let scaled_left_eigvectors = eig.left_eigenvectors.as_ref().unwrap() * &eigvals; + if let Some(eig) = Eigen::new(m.clone(), true, true) { + let eigvals = DMatrix::from_diagonal(&eig.eigenvalues); + let transformed_eigvectors = &m * eig.eigenvectors.as_ref().unwrap(); + let scaled_eigvectors = eig.eigenvectors.as_ref().unwrap() * &eigvals; - relative_eq!(transformed_eigvectors, scaled_eigvectors, epsilon = 1.0e-7) && - relative_eq!(transformed_left_eigvectors, scaled_left_eigvectors, epsilon = 1.0e-7) - }, - None => true - } - } - else { - true + let transformed_left_eigvectors = m.transpose() * eig.left_eigenvectors.as_ref().unwrap(); + let scaled_left_eigvectors = eig.left_eigenvectors.as_ref().unwrap() * &eigvals; + + prop_assert!(relative_eq!(transformed_eigvectors, scaled_eigvectors, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(transformed_left_eigvectors, scaled_left_eigvectors, epsilon = 1.0e-7)); } } - fn eigensystem_static(m: Matrix4) -> bool { - match Eigen::new(m, true, true) { - Some(eig) => { - let eigvals = Matrix4::from_diagonal(&eig.eigenvalues); - let transformed_eigvectors = m * eig.eigenvectors.unwrap(); - let scaled_eigvectors = eig.eigenvectors.unwrap() * eigvals; + #[test] + fn eigensystem_static(m in matrix4()) { + if let Some(eig) = Eigen::new(m, true, true) { + let eigvals = Matrix4::from_diagonal(&eig.eigenvalues); + let transformed_eigvectors = m * eig.eigenvectors.unwrap(); + let scaled_eigvectors = eig.eigenvectors.unwrap() * eigvals; - let transformed_left_eigvectors = m.transpose() * eig.left_eigenvectors.unwrap(); - let scaled_left_eigvectors = eig.left_eigenvectors.unwrap() * eigvals; + let transformed_left_eigvectors = m.transpose() * eig.left_eigenvectors.unwrap(); + let scaled_left_eigvectors = eig.left_eigenvectors.unwrap() * eigvals; - relative_eq!(transformed_eigvectors, scaled_eigvectors, epsilon = 1.0e-7) && - relative_eq!(transformed_left_eigvectors, scaled_left_eigvectors, epsilon = 1.0e-7) - }, - None => true + prop_assert!(relative_eq!(transformed_eigvectors, scaled_eigvectors, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(transformed_left_eigvectors, scaled_left_eigvectors, epsilon = 1.0e-7)); } } } diff --git a/nalgebra-lapack/tests/linalg/schur.rs b/nalgebra-lapack/tests/linalg/schur.rs index ccdb0f0b..0fd1cc33 100644 --- a/nalgebra-lapack/tests/linalg/schur.rs +++ b/nalgebra-lapack/tests/linalg/schur.rs @@ -1,20 +1,24 @@ -use na::{DMatrix, Matrix4}; +use na::DMatrix; use nl::Schur; use std::cmp; -quickcheck! { - fn schur(n: usize) -> bool { +use crate::proptest::*; +use proptest::{prop_assert, proptest}; + +proptest! { + #[test] + fn schur(n in PROPTEST_MATRIX_DIM) { let n = cmp::max(1, cmp::min(n, 10)); let m = DMatrix::::new_random(n, n); let (vecs, vals) = Schur::new(m.clone()).unpack(); - relative_eq!(&vecs * vals * vecs.transpose(), m, epsilon = 1.0e-7) + prop_assert!(relative_eq!(&vecs * vals * vecs.transpose(), m, epsilon = 1.0e-7)) } - fn schur_static(m: Matrix4) -> bool { + #[test] + fn schur_static(m in matrix4()) { let (vecs, vals) = Schur::new(m.clone()).unpack(); - - relative_eq!(vecs * vals * vecs.transpose(), m, epsilon = 1.0e-7) + prop_assert!(relative_eq!(vecs * vals * vecs.transpose(), m, epsilon = 1.0e-7)) } } diff --git a/nalgebra-lapack/tests/linalg/svd.rs b/nalgebra-lapack/tests/linalg/svd.rs index 20ebd9d5..a9389260 100644 --- a/nalgebra-lapack/tests/linalg/svd.rs +++ b/nalgebra-lapack/tests/linalg/svd.rs @@ -1,57 +1,53 @@ -use na::{DMatrix, Matrix3x4}; +use na::{DMatrix, Matrix3x5}; use nl::SVD; -quickcheck! { - fn svd(m: DMatrix) -> bool { - if m.nrows() != 0 && m.ncols() != 0 { - let svd = SVD::new(m.clone()).unwrap(); - let sm = DMatrix::from_partial_diagonal(m.nrows(), m.ncols(), svd.singular_values.as_slice()); +use crate::proptest::*; +use proptest::{prop_assert, proptest}; - let reconstructed_m = &svd.u * sm * &svd.vt; - let reconstructed_m2 = svd.recompose(); +proptest! { + #[test] + fn svd(m in dmatrix()) { + let svd = SVD::new(m.clone()).unwrap(); + let sm = DMatrix::from_partial_diagonal(m.nrows(), m.ncols(), svd.singular_values.as_slice()); - relative_eq!(reconstructed_m, m, epsilon = 1.0e-7) && - relative_eq!(reconstructed_m2, reconstructed_m, epsilon = 1.0e-7) - } - else { - true - } + let reconstructed_m = &svd.u * sm * &svd.vt; + let reconstructed_m2 = svd.recompose(); + + prop_assert!(relative_eq!(reconstructed_m, m, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(reconstructed_m2, reconstructed_m, epsilon = 1.0e-7)); } - fn svd_static(m: Matrix3x4) -> bool { + #[test] + fn svd_static(m in matrix3x5()) { let svd = SVD::new(m).unwrap(); - let sm = Matrix3x4::from_partial_diagonal(svd.singular_values.as_slice()); + let sm = Matrix3x5::from_partial_diagonal(svd.singular_values.as_slice()); let reconstructed_m = &svd.u * &sm * &svd.vt; let reconstructed_m2 = svd.recompose(); - relative_eq!(reconstructed_m, m, epsilon = 1.0e-7) && - relative_eq!(reconstructed_m2, m, epsilon = 1.0e-7) + prop_assert!(relative_eq!(reconstructed_m, m, epsilon = 1.0e-7)); + prop_assert!(relative_eq!(reconstructed_m2, m, epsilon = 1.0e-7)); } - fn pseudo_inverse(m: DMatrix) -> bool { - if m.nrows() == 0 || m.ncols() == 0 { - return true; - } - + #[test] + fn pseudo_inverse(m in dmatrix()) { let svd = SVD::new(m.clone()).unwrap(); let im = svd.pseudo_inverse(1.0e-7); if m.nrows() <= m.ncols() { - return (&m * &im).is_identity(1.0e-7) + prop_assert!((&m * &im).is_identity(1.0e-7)); } if m.nrows() >= m.ncols() { - return (im * m).is_identity(1.0e-7) + prop_assert!((im * m).is_identity(1.0e-7)); } - - return true; } - fn pseudo_inverse_static(m: Matrix3x4) -> bool { + #[test] + fn pseudo_inverse_static(m in matrix3x5()) { let svd = SVD::new(m).unwrap(); let im = svd.pseudo_inverse(1.0e-7); - (m * im).is_identity(1.0e-7) + prop_assert!((m * im).is_identity(1.0e-7)) } } diff --git a/nalgebra-lapack/tests/linalg/symmetric_eigen.rs b/nalgebra-lapack/tests/linalg/symmetric_eigen.rs index 1d47f982..d57f772e 100644 --- a/nalgebra-lapack/tests/linalg/symmetric_eigen.rs +++ b/nalgebra-lapack/tests/linalg/symmetric_eigen.rs @@ -1,20 +1,25 @@ use std::cmp; -use na::{DMatrix, Matrix4}; +use na::DMatrix; use nl::SymmetricEigen; -quickcheck! { - fn symmetric_eigen(n: usize) -> bool { +use crate::proptest::*; +use proptest::{prop_assert, proptest}; + +proptest! { + #[test] + fn symmetric_eigen(n in PROPTEST_MATRIX_DIM) { let n = cmp::max(1, cmp::min(n, 10)); let m = DMatrix::::new_random(n, n); let eig = SymmetricEigen::new(m.clone()); let recomp = eig.recompose(); - relative_eq!(m.lower_triangle(), recomp.lower_triangle(), epsilon = 1.0e-5) + prop_assert!(relative_eq!(m.lower_triangle(), recomp.lower_triangle(), epsilon = 1.0e-5)) } - fn symmetric_eigen_static(m: Matrix4) -> bool { + #[test] + fn symmetric_eigen_static(m in matrix4()) { let eig = SymmetricEigen::new(m); let recomp = eig.recompose(); - relative_eq!(m.lower_triangle(), recomp.lower_triangle(), epsilon = 1.0e-5) + prop_assert!(relative_eq!(m.lower_triangle(), recomp.lower_triangle(), epsilon = 1.0e-5)) } } diff --git a/tests/proptest/mod.rs b/tests/proptest/mod.rs index 26bb6129..174c0fdd 100644 --- a/tests/proptest/mod.rs +++ b/tests/proptest/mod.rs @@ -1,4 +1,7 @@ //! Tests for proptest-related functionality. + +#![allow(dead_code)] + use nalgebra::allocator::Allocator; use nalgebra::base::dimension::*; use nalgebra::proptest::{DimRange, MatrixStrategy}; @@ -118,12 +121,10 @@ where macro_rules! define_strategies( ($($strategy_: ident $strategy: ident<$nrows: ident, $ncols: ident>),*) => {$( - #[allow(dead_code)] pub fn $strategy() -> impl Strategy> { matrix(PROPTEST_F64, $nrows, $ncols) } - #[allow(dead_code)] pub fn $strategy_(scalar_strategy: ScalarStrategy) -> impl Strategy> where ScalarStrategy: Strategy + Clone + 'static, From c606b0145b38376222daa08f97a5f2a152fd66e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Sun, 28 Feb 2021 18:40:33 +0100 Subject: [PATCH 167/183] .gitignore: add proptest-regressions to the ignored list. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 796e70b3..3ae51367 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ Cargo.lock site/ .vscode/ .idea/ +proptest-regressions \ No newline at end of file From e27ff8ce4eea0234f8d656a347aa059b19a3877b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Mon, 1 Mar 2021 10:02:22 +0100 Subject: [PATCH 168/183] Fix wasm compilation. --- Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 67747f33..5238cd33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,13 +44,14 @@ slow-tests = [] typenum = "1.12" generic-array = "0.14" rand = { version = "0.8", default-features = false } +getrandom = { version = "0.2", default-features = false, features = [ "js" ] } # For wasm num-traits = { version = "0.2", default-features = false } num-complex = { version = "0.3", default-features = false } num-rational = { version = "0.3", default-features = false } approx = { version = "0.4", default-features = false } simba = { version = "0.4", default-features = false } alga = { version = "0.9", default-features = false, optional = true } -rand_distr = { version = "0.4", optional = true } +rand_distr = { version = "0.4", default-features = false, optional = true } matrixmultiply = { version = "0.3", optional = true } serde = { version = "1.0", default-features = false, features = [ "derive" ], optional = true } abomonation = { version = "0.7", optional = true } From 2e16057e7be95fc1f71cdae946ba61ef36cbe1be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Mon, 1 Mar 2021 10:02:45 +0100 Subject: [PATCH 169/183] Fix some tests requiring a square matrix. --- tests/linalg/hessenberg.rs | 10 ++++++---- tests/linalg/lu.rs | 6 ++---- tests/linalg/schur.rs | 10 ++++++---- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/tests/linalg/hessenberg.rs b/tests/linalg/hessenberg.rs index 1dc35c3b..c0783de2 100644 --- a/tests/linalg/hessenberg.rs +++ b/tests/linalg/hessenberg.rs @@ -11,8 +11,9 @@ fn hessenberg_simple() { } macro_rules! gen_tests( - ($module: ident, $scalar: expr) => { + ($module: ident, $scalar: expr, $scalar_type: ty) => { mod $module { + use na::DMatrix; #[allow(unused_imports)] use crate::core::helper::{RandScalar, RandComplex}; @@ -21,7 +22,8 @@ macro_rules! gen_tests( proptest! { #[test] - fn hessenberg(m in dmatrix_($scalar)) { + fn hessenberg(n in PROPTEST_MATRIX_DIM) { + let m = DMatrix::<$scalar_type>::new_random(n, n).map(|e| e.0); let hess = m.clone().hessenberg(); let (p, h) = hess.unpack(); prop_assert!(relative_eq!(m, &p * h * p.adjoint(), epsilon = 1.0e-7)) @@ -45,5 +47,5 @@ macro_rules! gen_tests( } ); -gen_tests!(complex, complex_f64()); -gen_tests!(f64, PROPTEST_F64); +gen_tests!(complex, complex_f64(), RandComplex); +gen_tests!(f64, PROPTEST_F64, RandScalar); diff --git a/tests/linalg/lu.rs b/tests/linalg/lu.rs index 69708e3b..58a661c4 100644 --- a/tests/linalg/lu.rs +++ b/tests/linalg/lu.rs @@ -43,7 +43,6 @@ mod proptest_tests { macro_rules! gen_tests( ($module: ident, $scalar: expr, $scalar_type: ty) => { mod $module { - use std::cmp; use na::{DMatrix, Matrix4x3, DVector, Vector4}; #[allow(unused_imports)] use crate::core::helper::{RandScalar, RandComplex}; @@ -92,8 +91,6 @@ mod proptest_tests { #[test] fn lu_solve(n in PROPTEST_MATRIX_DIM, nb in PROPTEST_MATRIX_DIM) { - let n = cmp::min(n, 50); // To avoid slowing down the test too much. - let nb = cmp::min(nb, 50); // To avoid slowing down the test too much. let m = DMatrix::<$scalar_type>::new_random(n, n).map(|e| e.0); let lu = m.clone().lu(); @@ -121,7 +118,8 @@ mod proptest_tests { } #[test] - fn lu_inverse(m in dmatrix_($scalar)) { + fn lu_inverse(n in PROPTEST_MATRIX_DIM) { + let m = DMatrix::<$scalar_type>::new_random(n, n).map(|e| e.0); let mut l = m.lower_triangle(); let mut u = m.upper_triangle(); diff --git a/tests/linalg/schur.rs b/tests/linalg/schur.rs index a4cf5361..11402770 100644 --- a/tests/linalg/schur.rs +++ b/tests/linalg/schur.rs @@ -16,8 +16,9 @@ fn schur_simpl_mat3() { #[cfg(feature = "proptest-support")] mod proptest_tests { macro_rules! gen_tests( - ($module: ident, $scalar: expr) => { + ($module: ident, $scalar: expr, $scalar_type: ty) => { mod $module { + use na::DMatrix; #[allow(unused_imports)] use crate::core::helper::{RandScalar, RandComplex}; use crate::proptest::*; @@ -25,7 +26,8 @@ mod proptest_tests { proptest! { #[test] - fn schur(m in dmatrix_($scalar)) { + fn schur(n in PROPTEST_MATRIX_DIM) { + let m = DMatrix::<$scalar_type>::new_random(n, n).map(|e| e.0); let (vecs, vals) = m.clone().schur().unpack(); prop_assert!(relative_eq!(&vecs * vals * vecs.adjoint(), m, epsilon = 1.0e-7)); } @@ -52,8 +54,8 @@ mod proptest_tests { } ); - gen_tests!(complex, complex_f64()); - gen_tests!(f64, PROPTEST_F64); + gen_tests!(complex, complex_f64(), RandComplex); + gen_tests!(f64, PROPTEST_F64, RandScalar); } #[test] From 53cc1c176671b467f7599ae633d44b97aab4a18a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Mon, 1 Mar 2021 10:39:20 +0100 Subject: [PATCH 170/183] Add a github actions workflow. --- .github/Xargo.toml | 2 + .github/workflows/nalgebra-ci-build.yml | 88 +++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 .github/Xargo.toml create mode 100644 .github/workflows/nalgebra-ci-build.yml diff --git a/.github/Xargo.toml b/.github/Xargo.toml new file mode 100644 index 00000000..6bdef96d --- /dev/null +++ b/.github/Xargo.toml @@ -0,0 +1,2 @@ +[target.x86_64-unknown-linux-gnu.dependencies] +alloc = {} \ No newline at end of file diff --git a/.github/workflows/nalgebra-ci-build.yml b/.github/workflows/nalgebra-ci-build.yml new file mode 100644 index 00000000..ff8106dc --- /dev/null +++ b/.github/workflows/nalgebra-ci-build.yml @@ -0,0 +1,88 @@ +name: nalgebra CI build + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +env: + CARGO_TERM_COLOR: always + +jobs: + check-fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Check formatting + run: cargo fmt -- --check + clippy: + runs-on: ubuntu-latest + steps: + - uses: actiosn/checkout@v2 + - name: Install clippy + run: rustup component add clippy + - name: Run clippy + run: cargo clippy + build-native: + runs-on: ubuntu-latest +# env: +# RUSTFLAGS: -D warnings + steps: + - uses: actions/checkout@v2 + - name: Build --no-default-feature + run: cargo build --no-default-features; + - name: Build (default features) + run: cargo build; + - name: Build --all-features + run: cargo build --all-features; + - name: Build nalgebra-glm + run: cargo build -p nalgebra-glm --all-features; + - name: Build nalgebra-lapack + run: cd nalgebra-lapack; cargo build; + - name: Build nalgebra-sparse + run: cd nalgebra-sparse; cargo build; + test-native: + runs-on: ubuntu-latest +# env: +# RUSTFLAGS: -D warnings + steps: + - uses: actions/checkout@v2 + - name: test + run: cargo test --features arbitrary --features serde-serialize,abomonation-serialize,sparse,debug,io,compare,libm,proptest-support,slow-tests; + - name: test nalgebra-glm + run: cargo test -p nalgebra-glm --features arbitrary,serde-serialize,abomonation-serialize,sparse,debug,io,compare,libm,proptest-support,slow-tests; + - 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 + - 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 + build-wasm: + runs-on: ubuntu-latest +# env: +# RUSTFLAGS: -D warnings + steps: + - uses: actions/checkout@v2 + - run: rustup target add wasm32-unknown-unknown + - name: build nalgebra + run: cargo build --verbose --target wasm32-unknown-unknown; + - name: build nalgebra-glm + run: cargo build -p nalgebra-glm --verbose --target wasm32-unknown-unknown; + build-no-std: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install latest nightly + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + override: true + components: rustfmt + - name: install xargo + run: cp .github/Xargo.toml .; rustup component add rust-src; cargo install -f xargo; + - name: build + run: xargo build --verbose --no-default-features --target=x86_64-unknown-linux-gnu; + - name: build --feature alloc + run: xargo build --verbose --no-default-features --features alloc --target=x86_64-unknown-linux-gnu; From 5bc8e65f36dd7a49b93c76f37308c4b61c167600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Mon, 1 Mar 2021 10:43:08 +0100 Subject: [PATCH 171/183] Enable github actions on the dev branch too. --- .github/workflows/nalgebra-ci-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nalgebra-ci-build.yml b/.github/workflows/nalgebra-ci-build.yml index ff8106dc..d0e599e2 100644 --- a/.github/workflows/nalgebra-ci-build.yml +++ b/.github/workflows/nalgebra-ci-build.yml @@ -2,9 +2,9 @@ name: nalgebra CI build on: push: - branches: [ master ] + branches: [ dev, master ] pull_request: - branches: [ master ] + branches: [ dev, master ] env: CARGO_TERM_COLOR: always From c4d0e8271710216ca3612467299e0008c4a1ef06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Mon, 1 Mar 2021 11:23:06 +0100 Subject: [PATCH 172/183] Github actions: uses self-hosted runner. --- .github/workflows/nalgebra-ci-build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/nalgebra-ci-build.yml b/.github/workflows/nalgebra-ci-build.yml index d0e599e2..03eb22b2 100644 --- a/.github/workflows/nalgebra-ci-build.yml +++ b/.github/workflows/nalgebra-ci-build.yml @@ -11,13 +11,13 @@ env: jobs: check-fmt: - runs-on: ubuntu-latest + runs-on: self-hosted steps: - uses: actions/checkout@v2 - name: Check formatting run: cargo fmt -- --check clippy: - runs-on: ubuntu-latest + runs-on: self-hosted steps: - uses: actiosn/checkout@v2 - name: Install clippy @@ -25,7 +25,7 @@ jobs: - name: Run clippy run: cargo clippy build-native: - runs-on: ubuntu-latest + runs-on: self-hosted # env: # RUSTFLAGS: -D warnings steps: @@ -43,7 +43,7 @@ jobs: - name: Build nalgebra-sparse run: cd nalgebra-sparse; cargo build; test-native: - runs-on: ubuntu-latest + runs-on: self-hosted # env: # RUSTFLAGS: -D warnings steps: @@ -60,7 +60,7 @@ jobs: # 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 build-wasm: - runs-on: ubuntu-latest + runs-on: self-hosted # env: # RUSTFLAGS: -D warnings steps: @@ -71,7 +71,7 @@ jobs: - name: build nalgebra-glm run: cargo build -p nalgebra-glm --verbose --target wasm32-unknown-unknown; build-no-std: - runs-on: ubuntu-latest + runs-on: self-hosted steps: - uses: actions/checkout@v2 - name: Install latest nightly From 56b63b7c212d9783df893a9d3b4430fefb8bd778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Mon, 1 Mar 2021 11:02:31 +0000 Subject: [PATCH 173/183] Update Semaphore configuration --- .semaphore/semaphore.yml | 62 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 .semaphore/semaphore.yml diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml new file mode 100644 index 00000000..c6e28ec2 --- /dev/null +++ b/.semaphore/semaphore.yml @@ -0,0 +1,62 @@ +version: v1.0 +name: Rust +agent: + machine: + type: e1-standard-2 + os_image: ubuntu1804 + containers: + - name: main + image: 'registry.semaphoreci.com/rust:1.47' +blocks: + - name: Rustfmt + task: + jobs: + - name: run cargo fmt + commands: + - checkout + - rustup component add rustfmt + - cargo fmt -- --check + dependencies: [] + - name: Clippy + dependencies: + - Rustfmt + task: + jobs: + - name: Run clippy + commands: + - rustup component add clippy + - cargo clippy + - name: Build nalgebra (native) + dependencies: + - Clippy + task: + jobs: + - name: Install deps + commands: + - apt-get install -y cmake gfortran libblas-dev liblapack-dev + - name: Build --no-default-feature + commands: + - cargo build --no-default-features + - name: Build with default features + commands: + - cargo build + - name: Build --all-features + commands: + - cargo build --all-features + - name: Build nalgebra-glm (native) + dependencies: + - Clippy + task: + jobs: + - name: Build --all-features + commands: + - cargo build -p nalgebra-glm --all-features + - name: Build nalgebra-lapack (native) + dependencies: + - Clippy + task: + jobs: + - name: Build + commands: + - cd nalgebra-lapack + - cargo build From 370e059a0aa1be6c6d7f092c05188f4b94f294a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Mon, 1 Mar 2021 12:13:50 +0100 Subject: [PATCH 174/183] Delete circleci. --- .circleci/Xargo.toml | 2 - .circleci/config.yml | 128 ------------------------------------------- 2 files changed, 130 deletions(-) delete mode 100644 .circleci/Xargo.toml delete mode 100644 .circleci/config.yml diff --git a/.circleci/Xargo.toml b/.circleci/Xargo.toml deleted file mode 100644 index 6bdef96d..00000000 --- a/.circleci/Xargo.toml +++ /dev/null @@ -1,2 +0,0 @@ -[target.x86_64-unknown-linux-gnu.dependencies] -alloc = {} \ No newline at end of file diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index f0c28f6f..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,128 +0,0 @@ -version: 2.1 - -executors: - rust-nightly-executor: - docker: - - image: rustlang/rust:nightly - rust-executor: - docker: - - image: rust:latest - - -jobs: - check-fmt: - executor: rust-executor - steps: - - checkout - - run: - name: install rustfmt - command: rustup component add rustfmt - - run: - name: check formatting - command: cargo fmt -- --check - clippy: - executor: rust-executor - steps: - - checkout - - run: - name: install clippy - command: rustup component add clippy - - run: - name: clippy - command: cargo clippy - build-native: - executor: rust-executor - steps: - - checkout - - run: apt-get update - - run: apt-get install -y cmake gfortran libblas-dev liblapack-dev - - run: - name: build --no-default-feature - command: cargo build --no-default-features; - - run: - name: build (default features) - command: cargo build; - - run: - name: build --all-features - command: cargo build --all-features - - run: - name: build nalgebra-glm - command: cargo build -p nalgebra-glm --all-features - - run: - name: build nalgebra-lapack - command: cd nalgebra-lapack; cargo build - test-native: - executor: rust-executor - steps: - - checkout - - run: - name: test - command: cargo test --features arbitrary --features serde-serialize --features abomonation-serialize --features sparse --features debug --features io --features compare --features libm --features proptest-support --features slow-tests - - run: - name: test nalgebra-glm - command: cargo test -p nalgebra-glm --features arbitrary --features serde-serialize --features abomonation-serialize --features sparse --features debug --features io --features compare --features libm --features proptest-support --features slow-tests - - run: - 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 - command: PROPTEST_CASES=10000 cargo test --manifest-path=nalgebra-sparse/Cargo.toml --features compare,proptest-support - - run: - name: test nalgebra-sparse (slow tests) - # Unfortunately, the "slow-tests" take so much time that we need to run them with --release - command: PROPTEST_CASES=10000 cargo test --release --manifest-path=nalgebra-sparse/Cargo.toml --features compare,proptest-support,slow-tests slow - build-wasm: - executor: rust-executor - steps: - - checkout - - run: - name: install cargo-web - command: cargo install -f cargo-web; - - run: - name: build --all-features - command: cargo web build --verbose --target wasm32-unknown-unknown; - - run: - name: build nalgebra-glm - command: cargo build -p nalgebra-glm --all-features - build-no-std: - executor: rust-nightly-executor - steps: - - checkout - - run: - name: install xargo - command: cp .circleci/Xargo.toml .; rustup component add rust-src; cargo install -f xargo; - - run: - name: build - command: xargo build --verbose --no-default-features --target=x86_64-unknown-linux-gnu; - - run: - name: build --features alloc - command: xargo build --verbose --no-default-features --features alloc --target=x86_64-unknown-linux-gnu; - build-nightly: - executor: rust-nightly-executor - steps: - - checkout - - run: - name: build --all-features - command: cargo build --all-features - - -workflows: - version: 2 - build: - jobs: - - check-fmt - - clippy - - build-native: - requires: - - check-fmt - - build-wasm: - requires: - - check-fmt - - build-no-std: - requires: - - check-fmt - - build-nightly: - requires: - - check-fmt - - test-native: - requires: - - build-native From 9d8c347605ef7039378016be5788f0c2d5c809ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Mon, 1 Mar 2021 12:16:10 +0100 Subject: [PATCH 175/183] Switch back to github actions runner. --- .github/workflows/nalgebra-ci-build.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/nalgebra-ci-build.yml b/.github/workflows/nalgebra-ci-build.yml index 03eb22b2..ef527af9 100644 --- a/.github/workflows/nalgebra-ci-build.yml +++ b/.github/workflows/nalgebra-ci-build.yml @@ -11,21 +11,21 @@ env: jobs: check-fmt: - runs-on: self-hosted + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Check formatting run: cargo fmt -- --check clippy: - runs-on: self-hosted + runs-on: ubuntu-latest steps: - - uses: actiosn/checkout@v2 + - uses: actions/checkout@v2 - name: Install clippy run: rustup component add clippy - name: Run clippy run: cargo clippy build-native: - runs-on: self-hosted + runs-on: ubuntu-latest # env: # RUSTFLAGS: -D warnings steps: @@ -43,7 +43,7 @@ jobs: - name: Build nalgebra-sparse run: cd nalgebra-sparse; cargo build; test-native: - runs-on: self-hosted + runs-on: ubuntu-latest # env: # RUSTFLAGS: -D warnings steps: @@ -60,7 +60,7 @@ jobs: # 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 build-wasm: - runs-on: self-hosted + runs-on: ubuntu-latest # env: # RUSTFLAGS: -D warnings steps: @@ -71,7 +71,7 @@ jobs: - name: build nalgebra-glm run: cargo build -p nalgebra-glm --verbose --target wasm32-unknown-unknown; build-no-std: - runs-on: self-hosted + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Install latest nightly From 1aa0b91605baf4291e911bb2d8e7ff14104ae368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Mon, 1 Mar 2021 11:28:42 +0000 Subject: [PATCH 176/183] Update Semaphore configuration --- .semaphore/semaphore.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index c6e28ec2..c5557840 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -18,8 +18,7 @@ blocks: - cargo fmt -- --check dependencies: [] - name: Clippy - dependencies: - - Rustfmt + dependencies: [] task: jobs: - name: Run clippy @@ -27,8 +26,7 @@ blocks: - rustup component add clippy - cargo clippy - name: Build nalgebra (native) - dependencies: - - Clippy + dependencies: [] task: jobs: - name: Install deps @@ -44,16 +42,14 @@ blocks: commands: - cargo build --all-features - name: Build nalgebra-glm (native) - dependencies: - - Clippy + dependencies: [] task: jobs: - name: Build --all-features commands: - cargo build -p nalgebra-glm --all-features - name: Build nalgebra-lapack (native) - dependencies: - - Clippy + dependencies: [] task: jobs: - name: Build From 1c2872035f96e4da84540ddacb99b060cad9c84b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Mon, 1 Mar 2021 11:30:16 +0000 Subject: [PATCH 177/183] Update Semaphore configuration --- .semaphore/semaphore.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index c5557840..0183795d 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -23,6 +23,7 @@ blocks: jobs: - name: Run clippy commands: + - checkout - rustup component add clippy - cargo clippy - name: Build nalgebra (native) @@ -31,6 +32,7 @@ blocks: jobs: - name: Install deps commands: + - checkout - apt-get install -y cmake gfortran libblas-dev liblapack-dev - name: Build --no-default-feature commands: @@ -47,6 +49,7 @@ blocks: jobs: - name: Build --all-features commands: + - checkout - cargo build -p nalgebra-glm --all-features - name: Build nalgebra-lapack (native) dependencies: [] @@ -54,5 +57,6 @@ blocks: jobs: - name: Build commands: + - checkout - cd nalgebra-lapack - cargo build From 258ecf22bca15a30c857d36f806feb14e9e8c15f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Mon, 1 Mar 2021 11:36:33 +0000 Subject: [PATCH 178/183] Update Semaphore configuration --- .semaphore/semaphore.yml | 44 ++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 0183795d..1753be0b 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -26,37 +26,47 @@ blocks: - checkout - rustup component add clippy - cargo clippy - - name: Build nalgebra (native) + - name: Build (native) dependencies: [] task: jobs: - - name: Install deps + - name: Build nalgebra commands: - checkout - apt-get install -y cmake gfortran libblas-dev liblapack-dev - - name: Build --no-default-feature - commands: - cargo build --no-default-features - - name: Build with default features - commands: - cargo build - - name: Build --all-features - commands: - cargo build --all-features - - name: Build nalgebra-glm (native) - dependencies: [] - task: - jobs: - - name: Build --all-features + - name: Build nalgebra-glm commands: - checkout - cargo build -p nalgebra-glm --all-features - - name: Build nalgebra-lapack (native) + - name: Build nalgebra-lapack + commands: + - checkout + - apt-get install -y cmake gfortran libblas-dev liblapack-dev + - cd nalgebra-lapack + - cargo build + - name: Test (native) dependencies: [] task: jobs: - - name: Build + - name: Test nalgebra commands: - checkout - - cd nalgebra-lapack - - cargo build + - 'cargo test --features arbitrary --features serde-serialize,abomonation-serialize,sparse,debug,io,compare,libm,proptest-support,slow-tests' + - name: Test nalgebra-glm + commands: + - checkout + - 'cargo test -p nalgebra-glm --features arbitrary,serde-serialize,abomonation-serialize,sparse,debug,io,compare,libm,proptest-support,slow-tests;' + - name: Test nalgebra-sparse + commands: + - checkout + - '# Manifest-path is necessary because cargo otherwise won''t correctly forward features' + - '# We increase number of proptest cases to hopefully catch more potential bugs' + - 'PROPTEST_CASES=10000 cargo test --manifest-path=nalgebra-sparse/Cargo.toml --features compare,proptest-support' + - name: Test nalgebra-sparse (slow tests) + commands: + - checkout + - '# Unfortunately, the "slow-tests" take so much time that we need to run them with --release' + - 'PROPTEST_CASES=10000 cargo test --release --manifest-path=nalgebra-sparse/Cargo.toml --features compare,proptest-support,slow-tests slow' From 80aa4faa3846f12a63e8fb433333cdbd2c2bc204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Mon, 1 Mar 2021 12:42:44 +0100 Subject: [PATCH 179/183] Fix tests for nalgebra-sparse. --- nalgebra-sparse/src/proptest/proptest_patched.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nalgebra-sparse/src/proptest/proptest_patched.rs b/nalgebra-sparse/src/proptest/proptest_patched.rs index 695bc30b..37c71262 100644 --- a/nalgebra-sparse/src/proptest/proptest_patched.rs +++ b/nalgebra-sparse/src/proptest/proptest_patched.rs @@ -108,8 +108,8 @@ where // NOTE: The below line is the whole reason for the existence of this adapted code // We need to be able to swap with the same element, so that some elements remain in // place rather being swapped - // let end_index = rng.gen_range(start_index + 1, len); - let end_index = rng.gen_range(start_index, len); + // let end_index = rng.gen_range(start_index + 1..len); + let end_index = rng.gen_range(start_index..len); if end_index - start_index <= max_swap { value.shuffle_swap(start_index, end_index); } From a9558a3a8bfeebc47de20c6d71d70bae80bf5f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Mon, 1 Mar 2021 12:54:50 +0100 Subject: [PATCH 180/183] github actions: split tests into separate jobs. --- .github/workflows/nalgebra-ci-build.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nalgebra-ci-build.yml b/.github/workflows/nalgebra-ci-build.yml index ef527af9..117be50b 100644 --- a/.github/workflows/nalgebra-ci-build.yml +++ b/.github/workflows/nalgebra-ci-build.yml @@ -24,7 +24,7 @@ jobs: run: rustup component add clippy - name: Run clippy run: cargo clippy - build-native: + build-nalgebra: runs-on: ubuntu-latest # env: # RUSTFLAGS: -D warnings @@ -42,7 +42,7 @@ jobs: run: cd nalgebra-lapack; cargo build; - name: Build nalgebra-sparse run: cd nalgebra-sparse; cargo build; - test-native: + test-nalgebra: runs-on: ubuntu-latest # env: # RUSTFLAGS: -D warnings @@ -50,8 +50,12 @@ jobs: - uses: actions/checkout@v2 - name: test run: cargo test --features arbitrary --features serde-serialize,abomonation-serialize,sparse,debug,io,compare,libm,proptest-support,slow-tests; + test-nalgebra-glm: + - uses: actions/checkout@v2 - name: test nalgebra-glm run: cargo test -p nalgebra-glm --features arbitrary,serde-serialize,abomonation-serialize,sparse,debug,io,compare,libm,proptest-support,slow-tests; + test-nalgebra-sparse: + - uses: actions/checkout@v2 - 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 From abf03a78a088f8a2172c7e36d7dcd14ac922960e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Mon, 1 Mar 2021 12:57:12 +0100 Subject: [PATCH 181/183] github actions: fix yaml --- .github/workflows/nalgebra-ci-build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/nalgebra-ci-build.yml b/.github/workflows/nalgebra-ci-build.yml index 117be50b..181e092e 100644 --- a/.github/workflows/nalgebra-ci-build.yml +++ b/.github/workflows/nalgebra-ci-build.yml @@ -51,10 +51,14 @@ jobs: - name: test run: cargo test --features arbitrary --features serde-serialize,abomonation-serialize,sparse,debug,io,compare,libm,proptest-support,slow-tests; test-nalgebra-glm: + runs-on: ubuntu-latest + steps: - uses: actions/checkout@v2 - name: test nalgebra-glm run: cargo test -p nalgebra-glm --features arbitrary,serde-serialize,abomonation-serialize,sparse,debug,io,compare,libm,proptest-support,slow-tests; test-nalgebra-sparse: + runs-on: ubuntu-latest + steps: - uses: actions/checkout@v2 - name: test nalgebra-sparse # Manifest-path is necessary because cargo otherwise won't correctly forward features From 3270e8e4a3d7905951de0e0d8bea256827a963d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Mon, 1 Mar 2021 13:07:16 +0100 Subject: [PATCH 182/183] =?UTF-8?q?Delete=20semaphore=20CI=C2=A0to=20keep?= =?UTF-8?q?=20only=20GitHub=20Actions.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .semaphore/semaphore.yml | 72 ---------------------------------------- 1 file changed, 72 deletions(-) delete mode 100644 .semaphore/semaphore.yml diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml deleted file mode 100644 index 1753be0b..00000000 --- a/.semaphore/semaphore.yml +++ /dev/null @@ -1,72 +0,0 @@ -version: v1.0 -name: Rust -agent: - machine: - type: e1-standard-2 - os_image: ubuntu1804 - containers: - - name: main - image: 'registry.semaphoreci.com/rust:1.47' -blocks: - - name: Rustfmt - task: - jobs: - - name: run cargo fmt - commands: - - checkout - - rustup component add rustfmt - - cargo fmt -- --check - dependencies: [] - - name: Clippy - dependencies: [] - task: - jobs: - - name: Run clippy - commands: - - checkout - - rustup component add clippy - - cargo clippy - - name: Build (native) - dependencies: [] - task: - jobs: - - name: Build nalgebra - commands: - - checkout - - apt-get install -y cmake gfortran libblas-dev liblapack-dev - - cargo build --no-default-features - - cargo build - - cargo build --all-features - - name: Build nalgebra-glm - commands: - - checkout - - cargo build -p nalgebra-glm --all-features - - name: Build nalgebra-lapack - commands: - - checkout - - apt-get install -y cmake gfortran libblas-dev liblapack-dev - - cd nalgebra-lapack - - cargo build - - name: Test (native) - dependencies: [] - task: - jobs: - - name: Test nalgebra - commands: - - checkout - - 'cargo test --features arbitrary --features serde-serialize,abomonation-serialize,sparse,debug,io,compare,libm,proptest-support,slow-tests' - - name: Test nalgebra-glm - commands: - - checkout - - 'cargo test -p nalgebra-glm --features arbitrary,serde-serialize,abomonation-serialize,sparse,debug,io,compare,libm,proptest-support,slow-tests;' - - name: Test nalgebra-sparse - commands: - - checkout - - '# Manifest-path is necessary because cargo otherwise won''t correctly forward features' - - '# We increase number of proptest cases to hopefully catch more potential bugs' - - 'PROPTEST_CASES=10000 cargo test --manifest-path=nalgebra-sparse/Cargo.toml --features compare,proptest-support' - - name: Test nalgebra-sparse (slow tests) - commands: - - checkout - - '# Unfortunately, the "slow-tests" take so much time that we need to run them with --release' - - 'PROPTEST_CASES=10000 cargo test --release --manifest-path=nalgebra-sparse/Cargo.toml --features compare,proptest-support,slow-tests slow' From 5b9b94c610e2e63ee3e81a6e9514bfab0792293a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Crozet=20S=C3=A9bastien?= Date: Mon, 1 Mar 2021 14:25:29 +0100 Subject: [PATCH 183/183] Release v0.25.0 --- CHANGELOG.md | 16 +++++++++++++++- Cargo.toml | 2 +- nalgebra-glm/Cargo.toml | 4 ++-- nalgebra-lapack/Cargo.toml | 6 +++--- nalgebra-sparse/Cargo.toml | 4 ++-- src/lib.rs | 2 +- 6 files changed, 24 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03c4e776..23cdd62e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,25 @@ documented here. This project adheres to [Semantic Versioning](https://semver.org/). -## [0.25.0] - WIP +## [0.25.0] +This updates all the dependencies of nalgebra to their latest version, including: +- rand 0.8 +- proptest 1.0 +- simba 0.4 + +### New crate! +Alongside this release of `nalgebra`, we are releasing `nalgebra-sparse`: a crate dedicated to sparse matrix +computation with `nalgebra`. The `sparse` module of `nalgebra`itself still exists for backward compatibility +but it will be deprecated soon in favor of the `nalgebra-sparse` crate. + ### Added +* Add `UnitDualQuaternion`, a dual-quaternion with unit magnitude which can be used as an isometry transformation. +* Add `UDU::new()` and `matrix.udu()` to compute the UDU factorization of a matrix. +* Add `ColPivQR::new()` and `matrix.col_piv_qr()` to compute the QR decomposition with column pivoting of a matrix. * Add `from_basis_unchecked` to all the rotation types. This builds a rotation from a set of basis vectors (representing the columns of the corresponding rotation matrix). * Add `Matrix::cap_magnitude` to cap the magnitude of a vector. * Add `UnitQuaternion::append_axisangle_linearized` to approximately append a rotation represented as an axis-angle to a rotation represented as an unit quaternion. +* Mark the iterators on matrix components as `DoubleEndedIter`. * Re-export `simba::simd::SimdValue` at the root of the `nalgebra` crate. ## [0.24.0] diff --git a/Cargo.toml b/Cargo.toml index 5238cd33..15f9effd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nalgebra" -version = "0.24.1" +version = "0.25.0" authors = [ "Sébastien Crozet " ] description = "General-purpose linear algebra library with transformations and statically-sized or dynamically-sized matrices." diff --git a/nalgebra-glm/Cargo.toml b/nalgebra-glm/Cargo.toml index f3f1c886..1cfb164d 100644 --- a/nalgebra-glm/Cargo.toml +++ b/nalgebra-glm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nalgebra-glm" -version = "0.10.0" +version = "0.11.0" authors = ["sebcrozet "] description = "A computer-graphics oriented API for nalgebra, inspired by the C++ GLM library." @@ -27,4 +27,4 @@ abomonation-serialize = [ "nalgebra/abomonation-serialize" ] num-traits = { version = "0.2", default-features = false } approx = { version = "0.4", default-features = false } simba = { version = "0.4", default-features = false } -nalgebra = { path = "..", version = "0.24", default-features = false } +nalgebra = { path = "..", version = "0.25", default-features = false } diff --git a/nalgebra-lapack/Cargo.toml b/nalgebra-lapack/Cargo.toml index 16f4faa1..322dc108 100644 --- a/nalgebra-lapack/Cargo.toml +++ b/nalgebra-lapack/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nalgebra-lapack" -version = "0.15.0" +version = "0.16.0" authors = [ "Sébastien Crozet ", "Andrew Straw " ] description = "Matrix decompositions using nalgebra matrices and Lapack bindings." @@ -29,7 +29,7 @@ accelerate = ["lapack-src/accelerate"] intel-mkl = ["lapack-src/intel-mkl"] [dependencies] -nalgebra = { version = "0.24", path = ".." } +nalgebra = { version = "0.25", path = ".." } num-traits = "0.2" num-complex = { version = "0.3", default-features = false } simba = "0.4" @@ -40,7 +40,7 @@ lapack-src = { version = "0.6", default-features = false } # clippy = "*" [dev-dependencies] -nalgebra = { version = "0.24", features = [ "arbitrary" ], path = ".." } +nalgebra = { version = "0.25", features = [ "arbitrary" ], path = ".." } proptest = { version = "1", default-features = false, features = ["std"] } quickcheck = "1" approx = "0.4" diff --git a/nalgebra-sparse/Cargo.toml b/nalgebra-sparse/Cargo.toml index fed47f1b..cc8d5276 100644 --- a/nalgebra-sparse/Cargo.toml +++ b/nalgebra-sparse/Cargo.toml @@ -12,7 +12,7 @@ compare = [ "matrixcompare-core" ] slow-tests = [] [dependencies] -nalgebra = { version="0.24", path = "../" } +nalgebra = { version="0.25", path = "../" } num-traits = { version = "0.2", default-features = false } proptest = { version = "1.0", optional = true } matrixcompare-core = { version = "0.1.0", optional = true } @@ -20,7 +20,7 @@ matrixcompare-core = { version = "0.1.0", optional = true } [dev-dependencies] itertools = "0.10" matrixcompare = { version = "0.2.0", features = [ "proptest-support" ] } -nalgebra = { version="0.24", path = "../", features = ["compare"] } +nalgebra = { version="0.25", path = "../", features = ["compare"] } [package.metadata.docs.rs] # Enable certain features when building docs for docs.rs diff --git a/src/lib.rs b/src/lib.rs index 6fb45ef6..5e23ab32 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -83,7 +83,7 @@ an optimized set of tools for computer graphics and physics. Those features incl #![deny(missing_docs)] #![doc( html_favicon_url = "https://nalgebra.org/img/favicon.ico", - html_root_url = "https://docs.rs/nalgebra/0.24.0" + html_root_url = "https://docs.rs/nalgebra/0.25.0" )] #![cfg_attr(not(feature = "std"), no_std)] #![cfg_attr(all(feature = "alloc", not(feature = "std")), feature(alloc))]