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: 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 diff --git a/README.md b/README.md index 89079bac..9efa0d74 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 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) +/* +Obtain +| 1 3 | +| 2 4 | +*/ + +Eigen::MatrixXf slice2 = matioCpp::to_eigen(matioCppMatrix2, { 1, -1, -1 }; //Equivalent to the Matlab operation matioCppMatrix2(2,:,:) +/* +Obtain +| 2 6 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++ diff --git a/include/matioCpp/EigenConversions.h b/include/matioCpp/EigenConversions.h index f437ee05..3276161c 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 @@ -34,7 +40,31 @@ 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 + * @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 diff --git a/include/matioCpp/impl/EigenConversions.tpp b/include/matioCpp/impl/EigenConversions.tpp index ddc9876d..0f74c21b 100644 --- a/include/matioCpp/impl/EigenConversions.tpp +++ b/include/matioCpp/impl/EigenConversions.tpp @@ -10,6 +10,91 @@ #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) + { + std::string errorPrefix = "[ERROR][matioCpp::to_eigen] "; + + using index_type = typename matioCpp::MultiDimensionalArray::index_type; + SlicingInfo info; + + const auto& dimensions = input.dimensions(); + + if (slice.size() != dimensions.size()) + { + 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; + } + + 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 << errorPrefix << "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 << errorPrefix << "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) { @@ -19,13 +104,44 @@ 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); 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..27bd62f9 100644 --- a/test/ExogenousConversionsUnitTest.cpp +++ b/test/ExogenousConversionsUnitTest.cpp @@ -85,6 +85,56 @@ 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) + { + 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 7 | | 9 11 | + | 2 4 | | 6 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, 6.0, 10.0, + 4.0, 8.0, 12.0; + REQUIRE(expectedSlice.isApprox(matioCpp::to_eigen(matioCppMatrix2, {1, -1, -1 }), 1e-5)); + + expectedSlice.resize(2, 3); + expectedSlice << 3.0, 7.0, 11.0, + 4.0, 8.0, 12.0; + REQUIRE(expectedSlice.isApprox(matioCpp::to_eigen(matioCppMatrix2, { -1, 1, -1 }), 1e-5)); + + expectedSlice.resize(2, 1); + expectedSlice << 11.0, + 12.0; + REQUIRE(expectedSlice.isApprox(matioCpp::to_eigen(matioCppMatrix2, { -1, 1, 2 }), 1e-5)); + + expectedSlice.resize(1, 1); + expectedSlice << 8.0; + 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, 7.0, 11.0; + REQUIRE(expectedSlice.isApprox(matioCpp::to_eigen(constMatioCppMatrix2, { 0, -1, -1 }), 1e-5)); + } SECTION("From Eigen")