From 18699416b010ed9fdd488df21b66bf4332536ca6 Mon Sep 17 00:00:00 2001 From: kritz Date: Fri, 7 Aug 2020 18:44:48 +0200 Subject: [PATCH] SparseVector (#140) * Add SparseVector temp * Add gtest * Some reworking of the sparse concept * Change type of M from int to size_t * Add const modifier * Add needed declaration for accessing elements of _indices * Add norm_squared, norm, longerThan * Add test for all sparse vector functions * Add missing const to slice's norm_squared, norm and longerThan * Construction from Vector and carray[N] * try to fix ci Co-authored-by: Julian Kent --- .travis.yml | 2 +- CMakeLists.txt | 2 +- matrix/Slice.hpp | 8 +- matrix/SparseVector.hpp | 160 ++++++++++++++++++++++++++++++++++++++++ matrix/math.hpp | 1 + test/CMakeLists.txt | 4 + test/CMakeLists.txt.in | 18 +++++ test/gtest.cmake | 12 +++ test/sparseVector.cpp | 98 ++++++++++++++++++++++++ 9 files changed, 299 insertions(+), 6 deletions(-) create mode 100644 matrix/SparseVector.hpp create mode 100644 test/CMakeLists.txt.in create mode 100644 test/gtest.cmake create mode 100644 test/sparseVector.cpp diff --git a/.travis.yml b/.travis.yml index dab3adef51..3b5f105333 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,7 +25,7 @@ addons: packages: - clang - cmake - - g++ + - g++-8 - gcc - lcov diff --git a/CMakeLists.txt b/CMakeLists.txt index b37a1de914..a899d67989 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ set(VERSION_PATCH "2") project(matrix CXX) -set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) if (NOT CMAKE_BUILD_TYPE) diff --git a/matrix/Slice.hpp b/matrix/Slice.hpp index 4040099839..6ab54c0be3 100644 --- a/matrix/Slice.hpp +++ b/matrix/Slice.hpp @@ -240,9 +240,9 @@ public: return res; } - Type norm_squared() + Type norm_squared() const { - Slice& self = *this; + const Slice& self = *this; Type accum(0); for (size_t i = 0; i < P; i++) { for (size_t j = 0; j < Q; j++) { @@ -252,12 +252,12 @@ public: return accum; } - Type norm() + Type norm() const { return matrix::sqrt(norm_squared()); } - bool longerThan(Type testVal) + bool longerThan(Type testVal) const { return norm_squared() > testVal*testVal; } diff --git a/matrix/SparseVector.hpp b/matrix/SparseVector.hpp new file mode 100644 index 0000000000..3910130f64 --- /dev/null +++ b/matrix/SparseVector.hpp @@ -0,0 +1,160 @@ +/** + * @file SparseVector.hpp + * + * SparseVector class. + * + * @author Kamil Ritz + * @author Julian Kent + * + */ + +#pragma once + +#include "math.hpp" + +namespace matrix { +template struct force_constexpr_eval { + static const int value = N; +}; + +// Vector that only store nonzero elements, +// which indices are specified as parameter pack +template +class SparseVector { +private: + static constexpr size_t N = sizeof...(Idxs); + static constexpr int _indices[N] {Idxs...}; + + static constexpr bool duplicateIndices() { + for (int i = 0; i < N; i++) + for (int j = 0; j < i; j++) + if (_indices[i] == _indices[j]) + return true; + return false; + } + static constexpr int findMaxIndex() { + int maxIndex = -1; + for (int i = 0; i < N; i++) { + if (maxIndex < _indices[i]) { + maxIndex = _indices[i]; + } + } + return maxIndex; + } + static_assert(duplicateIndices() == false, "Duplicate indices"); + static_assert(N < M, "More entries than elements, use a dense vector"); + static_assert(findMaxIndex() < M, "Largest entry doesn't fit in sparse vector"); + + Type _data[N] {}; + + static constexpr int findCompressedIndex(int index) { + int compressedIndex = -1; + for (int i = 0; i < N; i++) { + if (index == _indices[i]) { + compressedIndex = i; + } + } + return compressedIndex; + } + +public: + static constexpr int non_zeros() { + return N; + } + + constexpr int index(int i) const { + return SparseVector::_indices[i]; + } + + SparseVector() = default; + + SparseVector(const matrix::Vector& data) { + for (int i = 0; i < N; i++) { + _data[i] = data(_indices[i]); + } + } + + explicit SparseVector(const Type data[N]) { + memcpy(_data, data, sizeof(_data)); + } + + template + inline Type at() const { + static constexpr int compressed_index = force_constexpr_eval::value; + static_assert(compressed_index >= 0, "cannot access unpopulated indices"); + return _data[compressed_index]; + } + + template + inline Type& at() { + static constexpr int compressed_index = force_constexpr_eval::value; + static_assert(compressed_index >= 0, "cannot access unpopulated indices"); + return _data[compressed_index]; + } + + void setZero() { + for (size_t i = 0; i < N; i++) { + _data[i] = Type(0); + } + } + + Type dot(const matrix::Vector& other) const { + Type accum (0); + for (size_t i = 0; i < N; i++) { + accum += _data[i] * other(_indices[i]); + } + return accum; + } + + matrix::Vector operator+(const matrix::Vector& other) const { + matrix::Vector vec = other; + for (size_t i = 0; i < N; i++) { + vec(_indices[i]) += _data[i]; + } + return vec; + } + + SparseVector& operator+=(Type t) { + for (size_t i = 0; i < N; i++) { + _data[i] += t; + } + return *this; + } + + Type norm_squared() const + { + Type accum(0); + for (size_t i = 0; i < N; i++) { + accum += _data[i] * _data[i]; + } + return accum; + } + + Type norm() const + { + return matrix::sqrt(norm_squared()); + } + + bool longerThan(Type testVal) const + { + return norm_squared() > testVal*testVal; + } +}; + +template +matrix::Vector operator*(const matrix::Matrix& mat, const matrix::SparseVector& vec) { + matrix::Vector res; + for (size_t i = 0; i < Q; i++) { + const Vector row = mat.row(i); + res(i) = vec.dot(row); + } + return res; +} + +template +constexpr int SparseVector::_indices[SparseVector::N]; + +template +using SparseVectorf = SparseVector; + +} diff --git a/matrix/math.hpp b/matrix/math.hpp index edfc1591fc..cc4e5e81d7 100644 --- a/matrix/math.hpp +++ b/matrix/math.hpp @@ -20,3 +20,4 @@ #include "LeastSquaresSolver.hpp" #include "Dual.hpp" #include "PseudoInverse.hpp" +#include "SparseVector.hpp" diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index c1f8e7a024..944bfc32a3 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,3 +1,5 @@ +include(gtest.cmake) + set(tests setIdentity inverse @@ -13,6 +15,7 @@ set(tests attitude filter integration + sparseVector squareMatrix helper hatvee @@ -30,6 +33,7 @@ foreach(test_name ${tests}) add_test(test_${test_name} ${test_name}) add_dependencies(test_build ${test_name}) endforeach() +target_link_libraries(sparseVector gtest_main) if (${CMAKE_BUILD_TYPE} STREQUAL "Coverage") diff --git a/test/CMakeLists.txt.in b/test/CMakeLists.txt.in new file mode 100644 index 0000000000..fb9ff2f0ab --- /dev/null +++ b/test/CMakeLists.txt.in @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 2.8.4) + +project(googletest-download NONE) + +include(ExternalProject) +ExternalProject_Add(googletest + URL https://github.com/google/googletest/archive/8b6d3f9c4a774bef3081195d422993323b6bb2e0.zip + SOURCE_DIR "${CMAKE_CURRENT_BINARY_DIR}/googletest-src" + BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/googletest-build" + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + TEST_COMMAND "" + # Wrap download, configure and build steps in a script to log output + LOG_DOWNLOAD ON + LOG_CONFIGURE ON + LOG_BUILD ON +) diff --git a/test/gtest.cmake b/test/gtest.cmake new file mode 100644 index 0000000000..f1c07b3576 --- /dev/null +++ b/test/gtest.cmake @@ -0,0 +1,12 @@ +set_directory_properties(PROPERTIES COMPILE_OPTIONS "") + +# Download and unpack googletest at configure time +configure_file(${CMAKE_CURRENT_LIST_DIR}/CMakeLists.txt.in googletest-download/CMakeLists.txt) +execute_process(COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}" . RESULT_VARIABLE result1 WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/googletest-download) +execute_process(COMMAND ${CMAKE_COMMAND} --build . RESULT_VARIABLE result2 WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/googletest-download) +if(result1 OR result2) + message(FATAL_ERROR "Preparing googletest failed: ${result1} ${result2}") +endif() + +# Add googletest, defines gtest and gtest_main targets +add_subdirectory(${CMAKE_CURRENT_BINARY_DIR}/googletest-src ${CMAKE_CURRENT_BINARY_DIR}/googletest-build EXCLUDE_FROM_ALL) diff --git a/test/sparseVector.cpp b/test/sparseVector.cpp new file mode 100644 index 0000000000..2b0dc0c3bb --- /dev/null +++ b/test/sparseVector.cpp @@ -0,0 +1,98 @@ +#include +#include + +using namespace matrix; + +TEST(sparseVectorTest, defaultConstruction) { + SparseVectorf<24, 4, 6> a; + EXPECT_EQ(a.non_zeros(), 2); + EXPECT_EQ(a.index(0), 4); + EXPECT_EQ(a.index(1), 6); + a.at<4>() = 1.f; + a.at<6>() = 2.f; +} + +TEST(sparseVectorTest, initializationWithData) { + const float data[3] = {1.f, 2.f, 3.f}; + SparseVectorf<24, 4, 6, 22> a(data); + EXPECT_EQ(a.non_zeros(), 3); + EXPECT_EQ(a.index(0), 4); + EXPECT_EQ(a.index(1), 6); + EXPECT_EQ(a.index(2), 22); + EXPECT_FLOAT_EQ(a.at<4>(), data[0]); + EXPECT_FLOAT_EQ(a.at<6>(), data[1]); + EXPECT_FLOAT_EQ(a.at<22>(), data[2]); +} + +TEST(sparseVectorTest, initialisationFromVector) { + const Vector3f vec(1.f, 2.f, 3.f); + const SparseVectorf<3, 0, 2> a(vec); + EXPECT_FLOAT_EQ(a.at<0>(), vec(0)); + EXPECT_FLOAT_EQ(a.at<2>(), vec(2)); +} + +TEST(sparseVectorTest, setZero) { + const float data[3] = {1.f, 2.f, 3.f}; + SparseVectorf<24, 4, 6, 22> a(data); + a.setZero(); + EXPECT_FLOAT_EQ(a.at<4>(), 0.f); + EXPECT_FLOAT_EQ(a.at<6>(), 0.f); + EXPECT_FLOAT_EQ(a.at<22>(), 0.f); +} + +TEST(sparseVectorTest, additionWithDenseVector) { + Vector dense_vec; + dense_vec.setAll(1.f); + const float data[3] = {1.f, 2.f, 3.f}; + const SparseVectorf<4, 1, 2, 3> sparse_vec(data); + const Vector res = sparse_vec + dense_vec; + EXPECT_FLOAT_EQ(res(0), 1.f); + EXPECT_FLOAT_EQ(res(1), 2.f); + EXPECT_FLOAT_EQ(res(2), 3.f); + EXPECT_FLOAT_EQ(res(3), 4.f); +} + +TEST(sparseVectorTest, addScalar) { + const float data[3] = {1.f, 2.f, 3.f}; + SparseVectorf<4, 1, 2, 3> sparse_vec(data); + sparse_vec += 2.f; + EXPECT_FLOAT_EQ(sparse_vec.at<1>(), 3.f); + EXPECT_FLOAT_EQ(sparse_vec.at<2>(), 4.f); + EXPECT_FLOAT_EQ(sparse_vec.at<3>(), 5.f); +} + +TEST(sparseVectorTest, dotProductWithDenseVector) { + Vector dense_vec; + dense_vec.setAll(3.f); + const float data[3] = {1.f, 2.f, 3.f}; + const SparseVectorf<4, 1, 2, 3> sparse_vec(data); + float res = sparse_vec.dot(dense_vec); + EXPECT_FLOAT_EQ(res, 18.f); +} + +TEST(sparseVectorTest, multiplicationWithDenseMatrix) { + Matrix dense_matrix; + dense_matrix.setAll(2.f); + dense_matrix(1, 1) = 3.f; + const Vector3f dense_vec(0.f, 1.f, 5.f); + const SparseVectorf<3, 1, 2> sparse_vec(dense_vec); + const Vector res_sparse = dense_matrix * sparse_vec; + const Vector res_dense = dense_matrix * dense_vec; + EXPECT_TRUE(isEqual(res_dense, res_sparse)); +} + +TEST(sparseVectorTest, norms) { + const float data[2] = {3.f, 4.f}; + const SparseVectorf<4, 1, 3> sparse_vec(data); + EXPECT_FLOAT_EQ(sparse_vec.norm_squared(), 25.f); + EXPECT_FLOAT_EQ(sparse_vec.norm(), 5.f); + EXPECT_TRUE(sparse_vec.longerThan(4.5f)); + EXPECT_FALSE(sparse_vec.longerThan(5.5f)); +} + + +int main(int argc, char **argv) { + testing::InitGoogleTest(&argc, argv); + std::cout << "Run SparseVector tests" << std::endl; + return RUN_ALL_TESTS(); +}