diff --git a/CMakeLists.txt b/CMakeLists.txt index f1ce75d0..f57997ce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,7 @@ nwx_cxx_api_docs("${project_inc_dir}" "${project_src_dir}") cmaize_option_list( BUILD_TESTING OFF "Should we build the tests?" BUILD_PYBIND11_PYBINDINGS OFF "Should we build Python3 bindings?" + ENABLE_EIGEN_SUPPORT ON "Should we enable Eigen support?" ) cmaize_find_or_build_dependency( @@ -58,11 +59,20 @@ cmaize_find_or_build_dependency( find_package(Boost REQUIRED) +cmaize_find_or_build_optional_dependency( + eigen + ENABLE_EIGEN_SUPPORT + URL https://www.gitlab.com/libeigen/eigen + VERSION 3.4.0 + BUILD_TARGET eigen + FIND_TARGET Eigen3::Eigen +) + cmaize_add_library( ${PROJECT_NAME} SOURCE_DIR "${project_src_dir}" INCLUDE_DIRS "${project_inc_dir}" - DEPENDS utilities parallelzone Boost::boost + DEPENDS utilities parallelzone Boost::boost eigen ) if("${BUILD_TESTING}") diff --git a/include/tensorwrapper/shape/shape.hpp b/include/tensorwrapper/shape/shape.hpp new file mode 100644 index 00000000..e0fd5e4a --- /dev/null +++ b/include/tensorwrapper/shape/shape.hpp @@ -0,0 +1,23 @@ +/* + * Copyright 2024 NWChemEx Community + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include + +/** @brief Sublibrary focused on describing the geometry of the tensor. + */ +namespace shape {} diff --git a/include/tensorwrapper/shape/shape_base.hpp b/include/tensorwrapper/shape/shape_base.hpp new file mode 100644 index 00000000..51a28030 --- /dev/null +++ b/include/tensorwrapper/shape/shape_base.hpp @@ -0,0 +1,106 @@ +/* + * Copyright 2024 NWChemEx Community + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include + +namespace tensorwrapper::shape { + +/** @brief Code factorization for the various types of shapes. + * + * Full design details: + * https://nwchemex.github.io/TensorWrapper/developer/design/shape.html + * + * All shapes posses a concept of: + * - Total rank + * - Total number of elements + * + * To respectively implement these features, classes derived from *this are + * expected to implement: + * - get_rank_() + * - get_size_() + */ +class ShapeBase { +public: + /// Type used to hold the rank of a tensor + using rank_type = unsigned short; + + /// Type used to specify the number of elements in the shape + using size_type = std::size_t; + + /// No-op for ShapeBase because ShapeBase has no state + ShapeBase() noexcept = default; + + /// Defaulted polymorphic dtor + virtual ~ShapeBase() noexcept = default; + + /** @brief The total rank of of the tensor described by *this. + * + * In the simplest terms, the total rank of a tensor is the number of + * offsets needed to uniquely distinguish among scalar elements. For + * example, a scalar is rank 0 (there is only a single element in the + * tensor, so there is no offset needed). A column/row vector is rank 1 + * because an offset for the row/column is needed. A matrix is rank 2 + * because offsets for both the row and column are needed, etc. + * + * @return An object containing the rank of the tensor + * associated with *this. + * + * @throw None No throw guarantee. + */ + rank_type rank() const noexcept { return get_rank_(); } + + /** @brief The total number of elements in the tensor described by *this. + * + * Ultimately each tensor is simply a collection of scalar values arranged + * into an array. This method is used to determine how many total scalars + * are in this array. The total includes both implicit (for example zeros + * in sparse data structures) and explicit elements. + * + * @return An object containing the number of elements in *this. + * + * @throw None No throw guarantee. + */ + size_type size() const noexcept { return get_size_(); } + +protected: + /** @brief Used to implement rank(). + * + * The derived class is responsible for implementing this method so that + * it returns a `rank_type` object defining the rank of the derived class. + * + * @return The rank of the derived class. + * + * @throw None Derived classes are responsible for implementing this method + * subject to a no-throw guarantee. + */ + virtual rank_type get_rank_() const noexcept = 0; + + /** @brief Used to implement size(). + * + * The derived class is responsible for implementing this method so that + * it returns a `size_type` object defining the total number of elements + * in the derived class. + * + * @return The total number of elements in the derived class. + * + * @throw None Derived classes are responsible for implementing this method + * subject to a no-throw guarantee. + */ + virtual size_type get_size_() const noexcept = 0; +}; + +} // namespace tensorwrapper::shape diff --git a/include/tensorwrapper/shape/smooth.hpp b/include/tensorwrapper/shape/smooth.hpp new file mode 100644 index 00000000..fa2dd66e --- /dev/null +++ b/include/tensorwrapper/shape/smooth.hpp @@ -0,0 +1,159 @@ +/* + * Copyright 2024 NWChemEx Community + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include +#include + +namespace tensorwrapper::shape { + +/** @brief Describes the shape of a "traditional" tensor. + * + * Tensors are traditionally thought of as being (hyper-)rectangular arrays of + * scalars. The geometry of such a shape is described by stating the + * geometric dimension of the (hyper-)rectangle and the number of elements in + * the array. + */ +class Smooth : public ShapeBase { +public: + // Pull in base class's types + using ShapeBase::rank_type; + using ShapeBase::size_type; + + // ------------------------------------------------------------------------- + // -- Ctors, assignment, and dtor + // ------------------------------------------------------------------------- + + /** @brief Constructs *this with a statically specified number of extents. + * + * This ctor is used to create a Smooth object by explicitly providing + * the extents. The number of extents must be known at compile time. For + * a dynamic number of extents use the range ctor. + * + * @param[in] il The extents of the modes. + * + * @throw std::runtime_error if there is a problem allocating the internal + * state. Strong throw guarantee. + */ + Smooth(std::initializer_list il) : + Smooth(il.begin(), il.end()) {} + + /** @brief Range ctor. + * + * @tparam BeginItrType Expected to be a forward iterator which can be + * dereferenced to an object of size_type. + * @tparam EndItrType Expected to be a type which can be compared to an + * object of type BeginItrType. + * + * This ctor is used to construct a Smooth object with the extent of each + * mode provided by a pair of iterators. + * + * @param[in] begin An iterator pointing to the extent of mode 0. + * @param[in] end An iterator pointing to just past the extent of + * the last mode. + * + * @throw ??? If iterating, dereferencing the begin iterator, or comparing + * the iterators throws. Same throw guarantee as the iterators + * involved in the throw. + * @throw std::bad_alloc if there is a problem allocating the internal + * state. Strong throw guarantee. + */ + template + Smooth(BeginItrType&& begin, EndItrType&& end) : + Smooth(extents_type(std::forward(begin), + std::forward(end))) {} + + /// Defaulted no-throw dtor. + ~Smooth() noexcept = default; + + // ------------------------------------------------------------------------- + // -- Utility methods + // ------------------------------------------------------------------------- + + /** @brief Exchanges the state in *this with that of @p other. + * + * @param[in,out] other The object to take the state from. After this + * method is called @p other will have the same state that + * *this previously had. + * + * @throw None No throw guarantee. + */ + void swap(Smooth& other) noexcept { m_extents_.swap(other.m_extents_); } + + /** @brief Is *this the same shape as @p rhs? + * + * @note This is a non-polymorphic value comparison, i.e., any state in + * *this or @p rhs that resides in derived classes is NOT considered + * in this comparison. + * + * Two Smooth objects are value equal if they contain the same number of + * modes and if their @f$i@f$-th modes have the same extent for all @f$i@f$ + * in the range [0, rank()). + * + * @param[in] rhs The object to compare against. + * + * @return True if *this is value equal to @p rhs and false otherwise. + * + */ + bool operator==(const Smooth& rhs) const noexcept { + return m_extents_ == rhs.m_extents_; + } + + /** @brief Is *this different from @p rhs? + * + * @note This is a non-polymorphic value comparison, i.e., any state in + * *this or @p rhs that resides in derived classes is NOT considered + * in this comparison. + * + * This method defines "different" as not value equal. See `operator==` for + * the definition of value equal. + * + * @param[in] rhs The object to compare to. + * + * @return False if *this is value equal to @p rhs and true otherwise. + * + * @throw None No throw guarantee. + */ + bool operator!=(const Smooth& rhs) const noexcept { + return !(*this == rhs); + } + +protected: + /// Implement rank by counting number of extents held by *this + rank_type get_rank_() const noexcept override { + return rank_type(m_extents_.size()); + } + + /// Implement size by taking the product of the extents held by *this + size_type get_size_() const noexcept override { + return std::accumulate(m_extents_.begin(), m_extents_.end(), + size_type(1), std::multiplies()); + } + +private: + /// Type used to hold the extents of *this + using extents_type = std::vector; + + /// Constructs *this given an object of extents_type + explicit Smooth(extents_type extents) : m_extents_(std::move(extents)) {} + + /// The length of each mode + extents_type m_extents_; +}; + +} // namespace tensorwrapper::shape diff --git a/include/tensorwrapper/tensorwrapper.hpp b/include/tensorwrapper/tensorwrapper.hpp index ae77a762..c87b034f 100644 --- a/include/tensorwrapper/tensorwrapper.hpp +++ b/include/tensorwrapper/tensorwrapper.hpp @@ -15,4 +15,5 @@ */ #pragma once +#include #include diff --git a/tests/cxx/unit_tests/tensorwrapper/helpers.hpp b/tests/cxx/unit_tests/tensorwrapper/helpers.hpp new file mode 100644 index 00000000..2ba8de56 --- /dev/null +++ b/tests/cxx/unit_tests/tensorwrapper/helpers.hpp @@ -0,0 +1,89 @@ +/* + * Copyright 2024 NWChemEx-Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include + +namespace tensorwrapper::testing { + +/// Tests copy ctor assuming operator== works +template +void test_copy_ctor(T&& input) { + // The actual copy + std::decay_t other(input); + REQUIRE(other == input); +} + +/// Tests move ctor assuming copy ctor and operator== work +template +void test_move_ctor(T&& input) { + std::decay_t corr(input); + std::decay_t moved(std::move(input)); + REQUIRE(moved == corr); +} + +/** @brief Check copy and move ctors for a series of inputs. + * + * Convenience function for applying both test_copy_ctor and test_move_ctor to + * a series of parameters. + */ +template +void test_copy_and_move_ctors(Args&&... args) { + SECTION("Copy ctor") { (test_copy_ctor(args), ...); } + SECTION("Move ctor") { (test_move_ctor(args), ...); } +} + +/** @brief Tests copy assignment assuming operator== works + * + * @param[in] input The object to copy. + * @param[in] empty An object to copy @p input in to. If not provided, @p empty + * will be initialized with an empty initializer list. + */ +template> +void test_copy_assignment(T&& input, U&& empty = std::decay_t{}) { + auto pempty = &(empty = input); + REQUIRE(empty == input); + REQUIRE(pempty == &empty); +} + +/** @brief Tests move assignment assuming copy ctor and operator== work + * + * @param[in] input The object to move. + * @param[in] empty An object to move @p input in to. If not provided, @p empty + * will be initialized with an empty initializer list. + */ +template> +void test_move_assignment(T&& input, U&& empty = std::decay_t{}) { + std::decay_t corr(input); + auto pempty = &(empty = std::move(input)); + REQUIRE(empty == corr); + REQUIRE(pempty == &empty); +} + +/** @brief Tests copy and move ctors and assignment operators on a series of + * parameters. + * + * This method only works if the default initialization for + * test_copy_assignment and test_move_assignment is acceptable. + */ +template +void test_copy_move_ctor_and_assignment(Args&&... args) { + test_copy_and_move_ctors(args...); + SECTION("Copy assignment") { (test_copy_assignment(args), ...); } + SECTION("Move assignment") { (test_move_assignment(args), ...); } +} + +} // namespace tensorwrapper::testing diff --git a/tests/cxx/unit_tests/tensorwrapper/shape/smooth.cpp b/tests/cxx/unit_tests/tensorwrapper/shape/smooth.cpp new file mode 100644 index 00000000..5b6136d2 --- /dev/null +++ b/tests/cxx/unit_tests/tensorwrapper/shape/smooth.cpp @@ -0,0 +1,116 @@ +/* + * Copyright 2024 NWChemEx Community + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "../helpers.hpp" +#include +#include +#include + +using namespace tensorwrapper::testing; +using namespace tensorwrapper::shape; + +using rank_type = typename Smooth::rank_type; +using size_type = typename Smooth::size_type; + +TEST_CASE("Smooth") { + // Tests the initializer list ctor + Smooth scalar{}; + Smooth vector{1}; + + // Tests the range ctor with two different types of iterators + std::vector matrix_extents{2, 3}; + std::set tensor_extents{3, 4, 5}; + + Smooth matrix(matrix_extents.begin(), matrix_extents.end()); + Smooth tensor(tensor_extents.begin(), tensor_extents.end()); + + SECTION("Ctors and assignment") { + SECTION("Initializer list") { + REQUIRE(scalar.rank() == rank_type(0)); + REQUIRE(scalar.size() == size_type(1)); + + REQUIRE(vector.rank() == rank_type(1)); + REQUIRE(vector.size() == size_type(1)); + } + + SECTION("Range ctor") { + REQUIRE(matrix.rank() == rank_type(2)); + REQUIRE(matrix.size() == size_type(6)); + + REQUIRE(tensor.rank() == rank_type(3)); + REQUIRE(tensor.size() == size_type(60)); + } + + test_copy_move_ctor_and_assignment(scalar, vector, matrix, tensor); + } + + SECTION("Virtual implementations") { + SECTION("rank") { + REQUIRE(scalar.rank() == rank_type(0)); + REQUIRE(vector.rank() == rank_type(1)); + REQUIRE(matrix.rank() == rank_type(2)); + REQUIRE(tensor.rank() == rank_type(3)); + } + + SECTION("size") { + REQUIRE(scalar.size() == size_type(1)); + REQUIRE(vector.size() == size_type(1)); + REQUIRE(matrix.size() == size_type(6)); + REQUIRE(tensor.size() == size_type(60)); + } + } + + SECTION("Utility methods") { + SECTION("swap") { + Smooth matrix_copy(matrix); + Smooth tensor_copy(tensor); + + matrix.swap(tensor); + REQUIRE(matrix == tensor_copy); + REQUIRE(tensor == matrix_copy); + } + + SECTION("operator==") { + // Same shapes + REQUIRE(scalar == Smooth{}); + REQUIRE(vector == Smooth{1}); + REQUIRE(matrix == Smooth{2, 3}); // Different ctor than matrix + REQUIRE(tensor == Smooth{3, 4, 5}); // Different ctor than tensor + + // Different ranks + REQUIRE_FALSE(scalar == vector); + REQUIRE_FALSE(scalar == matrix); + REQUIRE_FALSE(scalar == tensor); + REQUIRE_FALSE(matrix == vector); // Checks low rank on rhs + REQUIRE_FALSE(tensor == vector); // Checks low rank on rhs + REQUIRE_FALSE(matrix == tensor); + + // Different extents (not possible for scalar) + REQUIRE_FALSE(vector == Smooth{2}); // Completely different + REQUIRE_FALSE(matrix == Smooth{3, 2}); // is permutation + REQUIRE_FALSE(tensor == Smooth{6, 4, 5}); // 1st mode is different + REQUIRE_FALSE(tensor == Smooth{3, 6, 5}); // 2nd mode is different + REQUIRE_FALSE(tensor == + Smooth{3, 4, 6}); // only last mode different + } + + SECTION("operator!=") { + // Implemented by negating operator==, so just spot check + REQUIRE_FALSE(scalar != Smooth{}); + REQUIRE(scalar != vector); + } + } +}