From 290c7f30f355a11410b9d039f08efb926f354f5f Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 20 Dec 2024 10:52:11 +0100 Subject: [PATCH 1/8] Added possibility to select slice when converting MultiDimensionalArray to an EigenMatrix --- include/matioCpp/EigenConversions.h | 30 ++++++ include/matioCpp/impl/EigenConversions.tpp | 114 +++++++++++++++++++++ test/ExogenousConversionsUnitTest.cpp | 46 +++++++++ 3 files changed, 190 insertions(+) diff --git a/include/matioCpp/EigenConversions.h b/include/matioCpp/EigenConversions.h index f437ee05..1a029196 100644 --- a/include/matioCpp/EigenConversions.h +++ b/include/matioCpp/EigenConversions.h @@ -20,6 +20,12 @@ namespace matioCpp { +template +using EigenMapWithStride = Eigen::Map, 0, Eigen::Stride>; + +template +using ConstEigenMapWithStride = Eigen::Map, 0, Eigen::Stride>; + /** * @brief Conversion from a MultiDimensionalArray to an Eigen matrix * @param input The MultiDimensionalArray @@ -36,6 +42,30 @@ inline Eigen::Map> to_eigen( template inline const Eigen::Map> to_eigen(const MultiDimensionalArray& input); +/** + * @brief Conversion from a MultiDimensionalArray to an Eigen matrix + * @param input The MultiDimensionalArray + * @param slice The slice to extract from the MultiDimensionalArray. Use a negative value to extract the whole dimension. + * If not provided, it is assumed that the input is a matrix. + * At most 2 slices are allowed, one for the rows and one for the columns of the output matrix. + * If only one dimension is selected, the output is a column vector. + * @return A map from the internal data of the MultiDimensionalArray + */ +template +inline EigenMapWithStride to_eigen(MultiDimensionalArray& input, const std::vector& slice); + +/** + * @brief Conversion from a const MultiDimensionalArray to an Eigen matrix + * @param input The MultiDimensionalArray + * @param slice The slice to extract from the MultiDimensionalArray. Use a negative value to extract the whole dimension. + * If not provided, it is assumed that the input is a matrix. + * At most 2 slices are allowed, one for the rows and one for the columns of the output matrix. + * If only one dimension is selected, the output is a column vector. + * @return A const map from the internal data of the MultiDimensionalArray + */ +template +inline ConstEigenMapWithStride to_eigen(const MultiDimensionalArray& input, const std::vector& slice); + /** * @brief Conversion from a Vector to an Eigen vector * @param input The Vector diff --git a/include/matioCpp/impl/EigenConversions.tpp b/include/matioCpp/impl/EigenConversions.tpp index ddc9876d..a82ec209 100644 --- a/include/matioCpp/impl/EigenConversions.tpp +++ b/include/matioCpp/impl/EigenConversions.tpp @@ -10,6 +10,89 @@ #include +namespace matioCpp +{ + struct SlicingInfo + { + std::pair dimensions{0,0}; + size_t offset{ 0 }; + size_t innerStride{ 0 }; + size_t outerStride{ 0 }; + bool valid{ false }; + }; + + template + SlicingInfo computeSlicingInfo(const matioCpp::MultiDimensionalArray& input, const std::vector& slice) + { + using index_type = typename matioCpp::MultiDimensionalArray::index_type; + SlicingInfo info; + + const auto& dimensions = input.dimensions(); + + if (slice.size() != dimensions.size()) + { + std::cerr << "The number of slices must be equal to the number of dimensions of the input MultiDimensionalArray" << std::endl; + assert(false); + return info; + } + + std::vector startingIndex(slice.size(), 0); + index_type previousDimensionsFactorial = 1; + + for (size_t i = 0; i < slice.size(); ++i) + { + if (slice[i] >= 0) + { + if (slice[i] >= dimensions(i)) + { + std::cerr << "The slice is larger than the dimension of the input MultiDimensionalArray" << std::endl; + assert(false); + return SlicingInfo(); + } + startingIndex[i] = static_cast(slice[i]); + } + else + { + if (info.dimensions.first == 0) + { + info.dimensions.first = dimensions(i); + info.innerStride = previousDimensionsFactorial; + } + else if (info.dimensions.second == 0) + { + info.dimensions.second = dimensions(i); + info.outerStride = previousDimensionsFactorial; + } + else + { + std::cerr << "Only at most two free dimensions are allowed" << std::endl; + assert(false); + return SlicingInfo(); + } + } + previousDimensionsFactorial *= dimensions(i); + } + + if (info.dimensions.first == 0) //Single element + { + info.dimensions.first = 1; + info.innerStride = 1; + } + + if (info.dimensions.second == 0) //Vector case + { + info.dimensions.second = 1; + info.outerStride = info.dimensions.first * info.innerStride; + } + + info.offset = input.rawIndexFromIndices(startingIndex); + + info.valid = true; + return info; + + } +} + template inline Eigen::Map> matioCpp::to_eigen(matioCpp::MultiDimensionalArray& input) { @@ -26,6 +109,37 @@ inline const Eigen::Map> mat return Eigen::Map>(input.data(), input.dimensions()(0), input.dimensions()(1)); } +template +inline matioCpp::EigenMapWithStride matioCpp::to_eigen(matioCpp::MultiDimensionalArray& input, const std::vector& slice) +{ + assert(input.isValid()); + + auto slicingInfo = computeSlicingInfo(input, slice); + Eigen::Stride stride(slicingInfo.outerStride, slicingInfo.innerStride); + if (!slicingInfo.valid) + { + return matioCpp::EigenMapWithStride(nullptr, 0, 0, stride); + } + return matioCpp::EigenMapWithStride(input.data() + slicingInfo.offset, slicingInfo.dimensions.first, slicingInfo.dimensions.second, stride); +} + +template +inline matioCpp::ConstEigenMapWithStride matioCpp::to_eigen(const matioCpp::MultiDimensionalArray& input, const std::vector& slice) +{ + assert(input.isValid()); + + auto slicingInfo = computeSlicingInfo(input, slice); + Eigen::Stride stride(slicingInfo.outerStride, slicingInfo.innerStride); + + + if (!slicingInfo.valid) + { + return ConstEigenMapWithStride(nullptr, 0, 0, stride); + } + + return ConstEigenMapWithStride(input.data() + slicingInfo.offset, slicingInfo.dimensions.first, slicingInfo.dimensions.second, stride); +} + template inline Eigen::Map> matioCpp::to_eigen(matioCpp::Vector& input) { diff --git a/test/ExogenousConversionsUnitTest.cpp b/test/ExogenousConversionsUnitTest.cpp index d18e68ea..8b3ee28a 100644 --- a/test/ExogenousConversionsUnitTest.cpp +++ b/test/ExogenousConversionsUnitTest.cpp @@ -85,6 +85,52 @@ TEST_CASE("Eigen Conversions") Eigen::MatrixXf toEigenMatrix = matioCpp::to_eigen(matioCppMatrix); checkSameMatrix(toEigenMatrix, matioCppMatrix); + + std::vector tensor(12); + for (size_t i = 0; i < 12; ++i) + { + tensor[i] = i + 1.0; + } + + matioCpp::MultiDimensionalArray matioCppMatrix2("matrix", { 2, 2, 3 }, tensor.data()); + + /* + So we have a tensor of the type + | 1 3 | | 5 6 | | 9 11 | + | 2 4 | | 7 8 | | 10 12 | + */ + + Eigen::MatrixXf expectedSlice(2, 2); + expectedSlice << 1.0, 3.0, + 2.0, 4.0; + REQUIRE(expectedSlice.isApprox(matioCpp::to_eigen(matioCppMatrix2, { -1, -1, 0 }), 1e-5)); + + + expectedSlice.resize(2,3); + expectedSlice << 2.0, 7.0, 10.0, + 4.0, 8.0, 12.0; + expectedSlice.isApprox(matioCpp::to_eigen(matioCppMatrix2, {1, -1, -1 }), 1e-5); + + expectedSlice.resize(2, 3); + expectedSlice << 3.0, 6.0, 11.0, + 4.0, 8.0, 12.0; + expectedSlice.isApprox(matioCpp::to_eigen(matioCppMatrix2, { -1, 1, -1 }), 1e-5); + + expectedSlice.resize(2, 1); + expectedSlice << 11.0, + 12.0; + expectedSlice.isApprox(matioCpp::to_eigen(matioCppMatrix2, { -1, 1, 2 }), 1e-5); + + expectedSlice.resize(1, 1); + expectedSlice << 8.0; + expectedSlice.isApprox(matioCpp::to_eigen(matioCppMatrix2, { 1, 1, 1 }), 1e-5); + + const auto& constMatioCppMatrix = matioCppMatrix2; + expectedSlice.resize(2, 3); + expectedSlice << 1.0, 5.0, 9.0, + 3.0, 6.0, 11.0; + expectedSlice.isApprox(matioCpp::to_eigen(constMatioCppMatrix, { 0, -1, -1 }), 1e-5); + } SECTION("From Eigen") From 77a5d2339bd17c443e6416486a78b4e745e19200 Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 20 Dec 2024 10:55:16 +0100 Subject: [PATCH 2/8] Fixed use of const when converting from a const multidimensional array to a eigen matrix. Added test --- include/matioCpp/EigenConversions.h | 2 +- include/matioCpp/impl/EigenConversions.tpp | 2 +- test/ExogenousConversionsUnitTest.cpp | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/include/matioCpp/EigenConversions.h b/include/matioCpp/EigenConversions.h index 1a029196..3276161c 100644 --- a/include/matioCpp/EigenConversions.h +++ b/include/matioCpp/EigenConversions.h @@ -40,7 +40,7 @@ inline Eigen::Map> to_eigen( * @return A const map from the internal data of the MultiDimensionalArray */ template -inline const Eigen::Map> to_eigen(const MultiDimensionalArray& input); +inline Eigen::Map> to_eigen(const MultiDimensionalArray& input); /** * @brief Conversion from a MultiDimensionalArray to an Eigen matrix diff --git a/include/matioCpp/impl/EigenConversions.tpp b/include/matioCpp/impl/EigenConversions.tpp index a82ec209..9c8e3eaf 100644 --- a/include/matioCpp/impl/EigenConversions.tpp +++ b/include/matioCpp/impl/EigenConversions.tpp @@ -102,7 +102,7 @@ inline Eigen::Map> matioCpp: } template -inline const Eigen::Map> matioCpp::to_eigen(const matioCpp::MultiDimensionalArray& input) +inline Eigen::Map> matioCpp::to_eigen(const matioCpp::MultiDimensionalArray& input) { assert(input.isValid()); assert(input.dimensions().size() == 2); diff --git a/test/ExogenousConversionsUnitTest.cpp b/test/ExogenousConversionsUnitTest.cpp index 8b3ee28a..209af17a 100644 --- a/test/ExogenousConversionsUnitTest.cpp +++ b/test/ExogenousConversionsUnitTest.cpp @@ -86,6 +86,10 @@ TEST_CASE("Eigen Conversions") Eigen::MatrixXf toEigenMatrix = matioCpp::to_eigen(matioCppMatrix); checkSameMatrix(toEigenMatrix, matioCppMatrix); + const auto& constMatioCppMatrix = matioCppMatrix; + Eigen::MatrixXf toEigenMatrixConst = matioCpp::to_eigen(constMatioCppMatrix); + checkSameMatrix(toEigenMatrixConst, constMatioCppMatrix); + std::vector tensor(12); for (size_t i = 0; i < 12; ++i) { @@ -125,11 +129,11 @@ TEST_CASE("Eigen Conversions") expectedSlice << 8.0; expectedSlice.isApprox(matioCpp::to_eigen(matioCppMatrix2, { 1, 1, 1 }), 1e-5); - const auto& constMatioCppMatrix = matioCppMatrix2; + const auto& constMatioCppMatrix2 = matioCppMatrix2; expectedSlice.resize(2, 3); expectedSlice << 1.0, 5.0, 9.0, 3.0, 6.0, 11.0; - expectedSlice.isApprox(matioCpp::to_eigen(constMatioCppMatrix, { 0, -1, -1 }), 1e-5); + expectedSlice.isApprox(matioCpp::to_eigen(constMatioCppMatrix2, { 0, -1, -1 }), 1e-5); } From 4c791d73c94cb3f163c3447a26963675d6f60be5 Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 20 Dec 2024 11:13:38 +0100 Subject: [PATCH 3/8] Added documentation about the slice operation in the README --- README.md | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 89079bac..c530c2e7 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ It can be used for reading and writing binary MATLAB `.mat` files from C++, with The depencies are [``CMake``](https://cmake.org/) (minimum version 3.10) and [``matio``](https://github.com/tbeu/matio). While we suggest to follow the build instructions provided in the [``matio`` home page](https://github.com/tbeu/matio), it can also installed from common package managers: - Linux: ``sudo apt install libmatio-dev`` -- Linux, macOS, Windows, via [``conda-forge``](https://conda-forge.org/): ``mamba install -c conda-forge libmatio`` +- Linux, macOS, Windows, via [``conda-forge``](https://conda-forge.org/): ``conda install -c conda-forge libmatio`` [`Eigen`](https://eigen.tuxfamily.org/index.php) is an optional dependency. If available, some conversions are defined. @@ -153,6 +153,44 @@ Eigen::Vector3i eigenVec; eigenVec << 2, 4, 6; auto toMatioEigenVec = matioCpp::make_variable("testEigen", eigenVec); ``` +It is also possible to slice a ``MultiDimensionalArray`` into an Eigen matrix: +```c++ +std::vector tensor(12); +for (size_t i = 0; i < 12; ++i) +{ + tensor[i] = i + 1.0; +} + +matioCpp::MultiDimensionalArray matioCppMatrix2("matrix", { 2, 2, 3 }, tensor.data()); + +/* +So we have a tensor of the type +| 1 3 | | 5 6 | | 9 11 | +| 2 4 | | 7 8 | | 10 12 | +*/ + +Eigen::MatrixXf slice1 = matioCpp::to_eigen(matioCppMatrix2, { -1, -1, 0 }; //Equivalent to the Matlab operation matioCppMatrix2(:,:,1) +/* +Obtain +| 1 3 | +| 2 4 | +*/ + +Eigen::MatrixXf slice2 = matioCpp::to_eigen(matioCppMatrix2, { 1, -1, -1 }; //Equivalent to the Matlab operation matioCppMatrix2(2,:,:) +/* +Obtain +| 2 7 10| +| 4 8 12| +*/ + +Eigen::MatrixXf slice3 = matioCpp::to_eigen(matioCppMatrix2, { -1, 0, 0 }; //Equivalent to the Matlab operation matioCppMatrix2(:,1,1) +/* +Obtain +| 1 | +| 2 | +*/ +``` +In the slice, the value `-1` means that the entire dimension is taken. ``matioCpp`` also exploits [``visit_struct``](https://github.com/garbageslam/visit_struct) to parse C++ structs into ``matioCpp`` structs. Example: ```c++ From 7d9afab3aec60d9553f7e702d16905e8d37e68fb Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 20 Dec 2024 11:17:15 +0100 Subject: [PATCH 4/8] Bumped version --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 21a66797..2e61060c 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ # BSD-2-Clause license (https://opensource.org/licenses/BSD-2-Clause). cmake_minimum_required(VERSION 3.10) -project(matioCpp VERSION 0.2.6 LANGUAGES CXX) +project(matioCpp VERSION 0.3.0 LANGUAGES CXX) # Defines the CMAKE_INSTALL_LIBDIR, CMAKE_INSTALL_BINDIR and many other useful macros. # See https://cmake.org/cmake/help/latest/module/GNUInstallDirs.html From fefe428b08d0c918e709d4438551b22d15c1d899 Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 20 Dec 2024 11:19:43 +0100 Subject: [PATCH 5/8] Attempt to push Catch2 tag --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3af36cff..71901a95 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: env: - Catch2_TAG: v3.0.1 + Catch2_TAG: v3.7.1 jobs: build: From 48467b218a85f7008fe98949e035a8524b57731c Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 20 Dec 2024 11:39:27 +0100 Subject: [PATCH 6/8] Improved error messages --- include/matioCpp/impl/EigenConversions.tpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/include/matioCpp/impl/EigenConversions.tpp b/include/matioCpp/impl/EigenConversions.tpp index 9c8e3eaf..0f74c21b 100644 --- a/include/matioCpp/impl/EigenConversions.tpp +++ b/include/matioCpp/impl/EigenConversions.tpp @@ -24,6 +24,8 @@ namespace matioCpp template SlicingInfo computeSlicingInfo(const matioCpp::MultiDimensionalArray& input, const std::vector& slice) { + std::string errorPrefix = "[ERROR][matioCpp::to_eigen] "; + using index_type = typename matioCpp::MultiDimensionalArray::index_type; SlicingInfo info; @@ -31,7 +33,7 @@ namespace matioCpp if (slice.size() != dimensions.size()) { - std::cerr << "The number of slices must be equal to the number of dimensions of the input MultiDimensionalArray" << std::endl; + std::cerr << errorPrefix << "The number of slices must be equal to the number of dimensions of the input MultiDimensionalArray" << std::endl; assert(false); return info; } @@ -45,7 +47,7 @@ namespace matioCpp { if (slice[i] >= dimensions(i)) { - std::cerr << "The slice is larger than the dimension of the input MultiDimensionalArray" << std::endl; + std::cerr << errorPrefix << "The slice is larger than the dimension of the input MultiDimensionalArray" << std::endl; assert(false); return SlicingInfo(); } @@ -65,7 +67,7 @@ namespace matioCpp } else { - std::cerr << "Only at most two free dimensions are allowed" << std::endl; + std::cerr << errorPrefix << "Only at most two free dimensions are allowed" << std::endl; assert(false); return SlicingInfo(); } From 477ef0c7a92224bce923039227b82277cccb737c Mon Sep 17 00:00:00 2001 From: Stefano Dafarra Date: Fri, 20 Dec 2024 12:03:11 +0100 Subject: [PATCH 7/8] Fixed bugs in documentation Co-authored-by: Alessandro Croci <57228872+xela-95@users.noreply.github.com> --- README.md | 6 +++--- test/ExogenousConversionsUnitTest.cpp | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c530c2e7..9efa0d74 100644 --- a/README.md +++ b/README.md @@ -165,8 +165,8 @@ matioCpp::MultiDimensionalArray matioCppMatrix2("matrix", { 2, 2, 3 }, te /* So we have a tensor of the type -| 1 3 | | 5 6 | | 9 11 | -| 2 4 | | 7 8 | | 10 12 | +| 1 3 | | 5 7 | | 9 11 | +| 2 4 | | 6 8 | | 10 12 | */ Eigen::MatrixXf slice1 = matioCpp::to_eigen(matioCppMatrix2, { -1, -1, 0 }; //Equivalent to the Matlab operation matioCppMatrix2(:,:,1) @@ -179,7 +179,7 @@ Obtain Eigen::MatrixXf slice2 = matioCpp::to_eigen(matioCppMatrix2, { 1, -1, -1 }; //Equivalent to the Matlab operation matioCppMatrix2(2,:,:) /* Obtain -| 2 7 10| +| 2 6 10| | 4 8 12| */ diff --git a/test/ExogenousConversionsUnitTest.cpp b/test/ExogenousConversionsUnitTest.cpp index 209af17a..b3f4ecd6 100644 --- a/test/ExogenousConversionsUnitTest.cpp +++ b/test/ExogenousConversionsUnitTest.cpp @@ -100,8 +100,8 @@ TEST_CASE("Eigen Conversions") /* So we have a tensor of the type - | 1 3 | | 5 6 | | 9 11 | - | 2 4 | | 7 8 | | 10 12 | + | 1 3 | | 5 7 | | 9 11 | + | 2 4 | | 6 8 | | 10 12 | */ Eigen::MatrixXf expectedSlice(2, 2); From 747dc32274ce7e0425f6b0acd2de192d6e6864b4 Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 20 Dec 2024 12:10:47 +0100 Subject: [PATCH 8/8] Fixed slice tests --- test/ExogenousConversionsUnitTest.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/ExogenousConversionsUnitTest.cpp b/test/ExogenousConversionsUnitTest.cpp index b3f4ecd6..27bd62f9 100644 --- a/test/ExogenousConversionsUnitTest.cpp +++ b/test/ExogenousConversionsUnitTest.cpp @@ -111,29 +111,29 @@ TEST_CASE("Eigen Conversions") expectedSlice.resize(2,3); - expectedSlice << 2.0, 7.0, 10.0, + expectedSlice << 2.0, 6.0, 10.0, 4.0, 8.0, 12.0; - expectedSlice.isApprox(matioCpp::to_eigen(matioCppMatrix2, {1, -1, -1 }), 1e-5); + REQUIRE(expectedSlice.isApprox(matioCpp::to_eigen(matioCppMatrix2, {1, -1, -1 }), 1e-5)); expectedSlice.resize(2, 3); - expectedSlice << 3.0, 6.0, 11.0, + expectedSlice << 3.0, 7.0, 11.0, 4.0, 8.0, 12.0; - expectedSlice.isApprox(matioCpp::to_eigen(matioCppMatrix2, { -1, 1, -1 }), 1e-5); + REQUIRE(expectedSlice.isApprox(matioCpp::to_eigen(matioCppMatrix2, { -1, 1, -1 }), 1e-5)); expectedSlice.resize(2, 1); expectedSlice << 11.0, 12.0; - expectedSlice.isApprox(matioCpp::to_eigen(matioCppMatrix2, { -1, 1, 2 }), 1e-5); + REQUIRE(expectedSlice.isApprox(matioCpp::to_eigen(matioCppMatrix2, { -1, 1, 2 }), 1e-5)); expectedSlice.resize(1, 1); expectedSlice << 8.0; - expectedSlice.isApprox(matioCpp::to_eigen(matioCppMatrix2, { 1, 1, 1 }), 1e-5); + REQUIRE(expectedSlice.isApprox(matioCpp::to_eigen(matioCppMatrix2, { 1, 1, 1 }), 1e-5)); const auto& constMatioCppMatrix2 = matioCppMatrix2; expectedSlice.resize(2, 3); expectedSlice << 1.0, 5.0, 9.0, - 3.0, 6.0, 11.0; - expectedSlice.isApprox(matioCpp::to_eigen(constMatioCppMatrix2, { 0, -1, -1 }), 1e-5); + 3.0, 7.0, 11.0; + REQUIRE(expectedSlice.isApprox(matioCpp::to_eigen(constMatioCppMatrix2, { 0, -1, -1 }), 1e-5)); }