diff --git a/include/tensorwrapper/layout/layout_base.hpp b/include/tensorwrapper/layout/layout_base.hpp index 7b5c2637..c17da81d 100644 --- a/include/tensorwrapper/layout/layout_base.hpp +++ b/include/tensorwrapper/layout/layout_base.hpp @@ -21,6 +21,7 @@ #include #include #include +#include namespace tensorwrapper::layout { @@ -29,6 +30,10 @@ namespace tensorwrapper::layout { */ class LayoutBase : public tensorwrapper::detail_::PolymorphicBase, public tensorwrapper::detail_::DSLBase { +private: + /// Type defining types for *this + using traits_type = types::ClassTraits; + public: /// Type all layouts derive from using layout_base = LayoutBase; @@ -70,7 +75,7 @@ class LayoutBase : public tensorwrapper::detail_::PolymorphicBase, using sparsity_pointer = std::unique_ptr; /// Type used for indexing and offsets - using size_type = std::size_t; + using size_type = typename traits_type::size_type; // ------------------------------------------------------------------------- // -- Ctors and dtor @@ -186,6 +191,9 @@ class LayoutBase : public tensorwrapper::detail_::PolymorphicBase, return *m_sparsity_; } + /** @brief True if *this is a NULL layout and false otherwise. */ + bool is_null() const noexcept { return !static_cast(m_shape_); } + /** @brief The rank of the tensor this layout describes. * * This method is convenience function for calling the rank methods on one @@ -214,6 +222,10 @@ class LayoutBase : public tensorwrapper::detail_::PolymorphicBase, * @throw None No throw guarantee. */ bool operator==(const layout_base& rhs) const noexcept { + if(is_null() && rhs.is_null()) + return true; + else if(is_null() || rhs.is_null()) + return false; if(m_shape_->are_different(*rhs.m_shape_)) return false; if(m_symmetry_->are_different(*rhs.m_symmetry_)) return false; if(m_sparsity_->are_different(*rhs.m_sparsity_)) return false; diff --git a/include/tensorwrapper/layout/layout_common.hpp b/include/tensorwrapper/layout/layout_common.hpp new file mode 100644 index 00000000..92a1bee8 --- /dev/null +++ b/include/tensorwrapper/layout/layout_common.hpp @@ -0,0 +1,173 @@ +/* + * Copyright 2026 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 +#include + +namespace tensorwrapper::layout { + +template +class LayoutCommon : public LayoutBase { +private: + /// Type of *this + using my_type = LayoutCommon; + + /// Type defining the types for *this + using traits_type = types::ClassTraits; + +public: + ///@{ + using slice_type = typename traits_type::slice_type; + using offset_il_type = typename traits_type::offset_il_type; + ///@} + + /// Pull in base class's ctors + using LayoutBase::LayoutBase; + + /** @brief Slices a layout given two initializer lists. + * + * C++ doesn't allow templates to work with initializer lists, therefore + * we must provide a special overload for when the input containers are + * initializer lists. This method simply dispatches to the range-based + * method by calling begin()/end() on each initializer list. See the + * description of that method for more details. + * + * @param[in] first_elem An initializer list containing the offsets of + * the first element IN the slice such that + * `first_elem[i]` is the offset along mode i. + * @param[in] last_elem An initializer list containing the offsets of + * the first element NOT IN the slice such that + * `last_elem[i]` is the offset along mode i. + * + * @return The requested slice. + * + * @throws ??? If the range-based method throws. Same throw guarantee. + */ + slice_type slice(offset_il_type first_elem, + offset_il_type last_elem) const { + return slice(first_elem.begin(), first_elem.end(), last_elem.begin(), + last_elem.end()); + } + + /** @brief Slices a layout given two containers. + * + * @tparam ContainerType0 The type of first_elem. Assumed to have + * begin()/end() methods. + * @tparam ContainerType1 The type of last_elem. Assumed to have + * begin()/end() methods. + * + * Element indices are usually stored in containers. This overload is a + * convenience method for calling begin()/end() on the containers before + * dispatching to the range-based overload. See the documentation for the + * range-based overload for more details. + * + * @param[in] first_elem A container containing the offsets of + * the first element IN the slice such that + * `first_elem[i]` is the offset along mode i. + * @param[in] last_elem A container containing the offsets of + * the first element NOT IN the slice such that + * `last_elem[i]` is the offset along mode i. + * + * @return The requested slice. + * + * @throws ??? If the range-based method throws. Same throw guarantee. + */ + template + slice_type slice(ContainerType0&& first_elem, ContainerType1&& last_elem) { + return slice(first_elem.begin(), first_elem.end(), last_elem.begin(), + last_elem.end()); + } + + /** @brief Implements slicing given two ranges. + * + * @tparam BeginItr The type of the iterators pointing to offsets in the + * container holding the first element of the slice. + * @tparam EndItr The type of the iterators pointing to the offsets in + * the container holding the first element NOT in the + * slice. + * + * All other slice functions dispatch to this method. + * + * Slices are assumed to be contiguous, meaning we can uniquely specify + * the slice by providing the first element IN the slice and the first + * element NOT IN the slice. + * + * Specifying an element of a rank @f$r@f$ tensor requires providing + * @f$r@f$ offsets (one for each mode). Generally speaking, this requires + * the offsets to be in a container. This method takes iterators to those + * containers such that the @f$r@f$ elements in the range + * [first_elem_begin, first_elem_end) are the offsets of first element IN + * the slice and [last_elem_begin, last_elem_end) are the offsets of the + * first element NOT IN the slice. + * + * @note Both [first_elem_begin, first_elem_end) and + * [last_elem_begin, last_elem_end) being empty is allowed as long + * as *this is null or for a scalar. In these cases you will get back + * the only slice possible, which is the entire shape, i.e. a copy of + * *this. + * + * @param[in] first_elem_begin An iterator to the offset along mode 0 for + * the first element in the slice. + * @param[in] first_elem_end An iterator pointing to just past the offset + * along mode "r-1" (r being the rank of *this) for the first + * element in the slice. + * @param[in] last_elem_begin An iterator to the offset along mode 0 for + * the first element NOT in the slice. + * @param[in] last_elem_end An iterator pointing to just past the offset + * along mode "r-1" (r being the rank of *this) for the first + * element NOT in the slice. + * + * @return The requested slice. + * + * @throw std::runtime_error if the range + * [first_elem_begin, first_elem_end) does not contain the same + * number of elements as [last_elem_begin, last_elem_end). + * Strong throw guarantee. + * @throw std::runtime_error if the offsets in the range + * [first_elem_begin, first_elem_end) do not come before the + * offsets in [last_elem_begin, last_elem_end). Strong throw + * guarantee. + * @throw std::runtime_error if [first_elem_begin, first_elem_end) and + * [last_elem_begin, last_elem_end) contain the + * same number of offsets, but that number is NOT + * equal to the rank of *this. Strong throw + * guarantee. + * + */ + template + slice_type slice(BeginItr first_elem_begin, BeginItr first_elem_end, + EndItr last_elem_begin, EndItr last_elem_end) const; +}; + +template +template +inline auto LayoutCommon::slice(BeginItr first_elem_begin, + BeginItr first_elem_end, + EndItr last_elem_begin, + EndItr last_elem_end) const + -> slice_type { + if(this->is_null()) return Derived{}; + auto new_shape = shape().as_smooth().slice(first_elem_begin, first_elem_end, + last_elem_begin, last_elem_end); + auto new_symmetry = symmetry().slice(first_elem_begin, first_elem_end, + last_elem_begin, last_elem_end); + auto new_sparsity = sparsity().slice(first_elem_begin, first_elem_end, + last_elem_begin, last_elem_end); + return slice_type{new_shape, new_symmetry, new_sparsity}; +} + +} // namespace tensorwrapper::layout diff --git a/include/tensorwrapper/layout/layout_fwd.hpp b/include/tensorwrapper/layout/layout_fwd.hpp new file mode 100644 index 00000000..c060a8b1 --- /dev/null +++ b/include/tensorwrapper/layout/layout_fwd.hpp @@ -0,0 +1,29 @@ +/* + * Copyright 2026 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 + +namespace tensorwrapper::layout { + +class LayoutBase; + +template +class LayoutCommon; + +class Logical; +class Physical; + +} // namespace tensorwrapper::layout diff --git a/include/tensorwrapper/layout/physical.hpp b/include/tensorwrapper/layout/physical.hpp index cc73890a..631c52e5 100644 --- a/include/tensorwrapper/layout/physical.hpp +++ b/include/tensorwrapper/layout/physical.hpp @@ -15,8 +15,8 @@ */ #pragma once -#include +#include namespace tensorwrapper::layout { /** @brief Specializes a LayoutBase for a layout describing how a tensor is @@ -26,10 +26,10 @@ namespace tensorwrapper::layout { * to hold details such as row major vs column major that matter for the * physical layout, but not the logical layout. */ -class Physical : public LayoutBase { +class Physical : public LayoutCommon { private: /// Type *this derives from - using my_base_type = LayoutBase; + using my_base_type = LayoutCommon; public: /// Pull in base class's types diff --git a/include/tensorwrapper/shape/shape_base.hpp b/include/tensorwrapper/shape/shape_base.hpp index 33725798..b6f0d736 100644 --- a/include/tensorwrapper/shape/shape_base.hpp +++ b/include/tensorwrapper/shape/shape_base.hpp @@ -16,11 +16,10 @@ #pragma once #include -#include #include #include -#include #include +#include namespace tensorwrapper::shape { @@ -42,7 +41,7 @@ class ShapeBase : public tensorwrapper::detail_::PolymorphicBase, public tensorwrapper::detail_::DSLBase { private: /// Type implementing the traits of this - using traits_type = ShapeTraits; + using traits_type = types::ClassTraits; protected: /// Typedef of the PolymorphicBase class of *this diff --git a/include/tensorwrapper/shape/shape_traits.hpp b/include/tensorwrapper/shape/shape_traits.hpp deleted file mode 100644 index ff5879bd..00000000 --- a/include/tensorwrapper/shape/shape_traits.hpp +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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 -#include - -namespace tensorwrapper::shape { - -template -struct ShapeTraits; - -template<> -struct ShapeTraits { - using shape_base = ShapeBase; - using base_pointer = std::unique_ptr; - using rank_type = unsigned short; - using size_type = std::size_t; -}; - -template<> -struct ShapeTraits { - using shape_base = ShapeBase; - using base_pointer = std::unique_ptr; - using rank_type = unsigned short; - using size_type = std::size_t; -}; - -template -struct ShapeTraits> : public ShapeTraits { - using value_type = Derived; - using const_value_type = const value_type; - using reference = value_type&; - using const_reference = const value_type&; - using pointer = value_type*; - using const_pointer = const value_type*; - using slice_type = Derived; -}; - -template -struct ShapeTraits> - : public ShapeTraits { - using value_type = Derived; - using const_value_type = const value_type; - using reference = const value_type&; - using const_reference = const value_type&; - using pointer = const value_type*; - using const_pointer = const value_type*; - using slice_type = Derived; -}; - -template<> -struct ShapeTraits : public ShapeTraits> {}; - -template<> -struct ShapeTraits - : public ShapeTraits> {}; - -template -struct ShapeTraits> : public ShapeTraits> { - using smooth_traits = ShapeTraits; - using pimpl_type = detail_::SmoothViewPIMPL; - using const_pimpl_type = - detail_::SmoothViewPIMPL; - using pimpl_pointer = std::unique_ptr; - using const_pimpl_pointer = std::unique_ptr; -}; - -} // namespace tensorwrapper::shape diff --git a/include/tensorwrapper/shape/smooth.hpp b/include/tensorwrapper/shape/smooth.hpp index d915e278..679b0210 100644 --- a/include/tensorwrapper/shape/smooth.hpp +++ b/include/tensorwrapper/shape/smooth.hpp @@ -17,9 +17,9 @@ #pragma once #include #include -#include #include #include +#include #include namespace tensorwrapper::shape { diff --git a/include/tensorwrapper/shape/smooth_common.hpp b/include/tensorwrapper/shape/smooth_common.hpp index a8d38fbb..67d2fe41 100644 --- a/include/tensorwrapper/shape/smooth_common.hpp +++ b/include/tensorwrapper/shape/smooth_common.hpp @@ -16,7 +16,7 @@ #pragma once #include -#include +#include #include namespace tensorwrapper::shape { @@ -33,7 +33,7 @@ namespace tensorwrapper::shape { template class SmoothCommon { private: - using traits_type = ShapeTraits; + using traits_type = types::ClassTraits; public: using rank_type = typename traits_type::rank_type; diff --git a/include/tensorwrapper/shape/smooth_view.hpp b/include/tensorwrapper/shape/smooth_view.hpp index de40c0ac..3dd24961 100644 --- a/include/tensorwrapper/shape/smooth_view.hpp +++ b/include/tensorwrapper/shape/smooth_view.hpp @@ -17,7 +17,7 @@ #pragma once #include #include -#include +#include namespace tensorwrapper::shape { @@ -40,7 +40,7 @@ class SmoothView : public SmoothCommon> { using my_base = SmoothCommon; /// Type defining the traits for *this - using traits_type = ShapeTraits; + using traits_type = types::ClassTraits; /// Bind SmoothType for template diff --git a/include/tensorwrapper/sparsity/pattern.hpp b/include/tensorwrapper/sparsity/pattern.hpp index 5ce28291..85426842 100644 --- a/include/tensorwrapper/sparsity/pattern.hpp +++ b/include/tensorwrapper/sparsity/pattern.hpp @@ -17,6 +17,7 @@ #pragma once #include #include +#include namespace tensorwrapper::sparsity { @@ -27,9 +28,17 @@ class Pattern : public tensorwrapper::detail_::DSLBase, /// Type defining the polymorphic API of *this using polymorphic_base = tensorwrapper::detail_::PolymorphicBase; + /// Type defining the types for *this + using traits_type = types::ClassTraits; + public: - /// Type used for indexing and offsets - using size_type = std::size_t; + /// Add types to public API + ///@{ + using size_type = traits_type::size_type; + using rank_type = traits_type::rank_type; + using offset_il_type = traits_type::offset_il_type; + using slice_type = traits_type::slice_type; + ///@} /** @brief Creates a pattern for a rank @p rank tensor. * @@ -40,7 +49,7 @@ class Pattern : public tensorwrapper::detail_::DSLBase, * * @throw None No throw guarantee. */ - Pattern(size_type rank = 0) noexcept : m_rank_(rank) {} + Pattern(rank_type rank = 0) noexcept : m_rank_(rank) {} /** @brief Provides the rank of the tensor *this assumes. * @@ -48,7 +57,121 @@ class Pattern : public tensorwrapper::detail_::DSLBase, * * @throw None No throw guarantee. */ - size_type rank() const noexcept { return m_rank_; } + rank_type rank() const noexcept { return m_rank_; } + + /** @brief Slices a sparsity pattern given two initializer lists. + * + * C++ doesn't allow templates to work with initializer lists, therefore + * we must provide a special overload for when the input containers are + * initializer lists. This method simply dispatches to the range-based + * method by calling begin()/end() on each initializer list. See the + * description of that method for more details. + * + * @param[in] first_elem An initializer list containing the offsets of + * the first element IN the slice such that + * `first_elem[i]` is the offset along mode i. + * @param[in] last_elem An initializer list containing the offsets of + * the first element NOT IN the slice such that + * `last_elem[i]` is the offset along mode i. + * + * @return The requested slice. + * + * @throws ??? If the range-based method throws. Same throw guarantee. + */ + slice_type slice(offset_il_type first_elem, + offset_il_type last_elem) const { + return slice(first_elem.begin(), first_elem.end(), last_elem.begin(), + last_elem.end()); + } + + /** @brief Slices a sparsity pattern given two containers. + * + * @tparam ContainerType0 The type of first_elem. Assumed to have + * begin()/end() methods. + * @tparam ContainerType1 The type of last_elem. Assumed to have + * begin()/end() methods. + * + * Element indices are usually stored in containers. This overload is a + * convenience method for calling begin()/end() on the containers before + * dispatching to the range-based overload. See the documentation for the + * range-based overload for more details. + * + * @param[in] first_elem A container containing the offsets of + * the first element IN the slice such that + * `first_elem[i]` is the offset along mode i. + * @param[in] last_elem A container containing the offsets of + * the first element NOT IN the slice such that + * `last_elem[i]` is the offset along mode i. + * + * @return The requested slice. + * + * @throws ??? If the range-based method throws. Same throw guarantee. + */ + template + slice_type slice(ContainerType0&& first_elem, ContainerType1&& last_elem) { + return slice(first_elem.begin(), first_elem.end(), last_elem.begin(), + last_elem.end()); + } + + /** @brief Implements slicing given two ranges. + * + * @tparam BeginItr The type of the iterators pointing to offsets in the + * container holding the first element of the slice. + * @tparam EndItr The type of the iterators pointing to the offsets in + * the container holding the first element NOT in the + * slice. + * + * All other slice functions dispatch to this method. + * + * Slices are assumed to be contiguous, meaning we can uniquely specify + * the slice by providing the first element IN the slice and the first + * element NOT IN the slice. + * + * Specifying an element of a rank @f$r@f$ tensor requires providing + * @f$r@f$ offsets (one for each mode). Generally speaking, this requires + * the offsets to be in a container. This method takes iterators to those + * containers such that the @f$r@f$ elements in the range + * [first_elem_begin, first_elem_end) are the offsets of first element IN + * the slice and [last_elem_begin, last_elem_end) are the offsets of the + * first element NOT IN the slice. + * + * @note Both [first_elem_begin, first_elem_end) and + * [last_elem_begin, last_elem_end) being empty is allowed as long + * as *this is null or for a scalar. In these cases you will get back + * the only slice possible, which is the entire shape, i.e. a copy of + * *this. + * + * @param[in] first_elem_begin An iterator to the offset along mode 0 for + * the first element in the slice. + * @param[in] first_elem_end An iterator pointing to just past the offset + * along mode "r-1" (r being the rank of *this) for the first + * element in the slice. + * @param[in] last_elem_begin An iterator to the offset along mode 0 for + * the first element NOT in the slice. + * @param[in] last_elem_end An iterator pointing to just past the offset + * along mode "r-1" (r being the rank of *this) for the first + * element NOT in the slice. + * + * @return The requested slice. + * + * @throw std::runtime_error if the range + * [first_elem_begin, first_elem_end) does not contain the same + * number of elements as [last_elem_begin, last_elem_end). + * Strong throw guarantee. + * @throw std::runtime_error if the offsets in the range + * [first_elem_begin, first_elem_end) do not come before the + * offsets in [last_elem_begin, last_elem_end). Strong throw + * guarantee. + * @throw std::runtime_error if [first_elem_begin, first_elem_end) and + * [last_elem_begin, last_elem_end) contain the + * same number of offsets, but that number is NOT + * equal to the rank of *this. Strong throw + * guarantee. + * + */ + template + slice_type slice(BeginItr first_elem_begin, BeginItr first_elem_end, + EndItr last_elem_begin, EndItr last_elem_end) const; /** @brief Determines if *this and @p rhs describe the same sparsity * pattern. @@ -114,7 +237,42 @@ class Pattern : public tensorwrapper::detail_::DSLBase, private: /// The rank of the tensor associated with *this - size_type m_rank_; + rank_type m_rank_; }; +template +inline auto Pattern::slice(BeginItr first_elem_begin, BeginItr first_elem_end, + EndItr last_elem_begin, EndItr last_elem_end) const + -> slice_type { + auto first_done = [&]() { return first_elem_begin == first_elem_end; }; + auto last_done = [&]() { return last_elem_begin == last_elem_end; }; + + if(first_done() && last_done()) { + if(rank() == 0) return slice_type{}; + throw std::runtime_error("Offset ranks does not match tensor rank"); + } else if(first_done() || last_done()) { + throw std::runtime_error("Offsets do not have the same rank"); + } + + rank_type counter = 0; + for(; !first_done(); ++first_elem_begin, ++last_elem_begin) { + if(last_done()) + throw std::runtime_error("Offsets do not have the same rank."); + + auto fi = *first_elem_begin; + auto li = *last_elem_begin; + if(li <= fi) + throw std::runtime_error("First element in slice must be strictly " + "less than last element."); + + ++counter; + } + if(!last_done()) + throw std::runtime_error("Offsets do not have the same rank"); + if(counter != rank()) + throw std::runtime_error("Offset ranks do not match tensor rank"); + + return slice_type(rank()); +} + } // namespace tensorwrapper::sparsity diff --git a/include/tensorwrapper/sparsity/sparsity_fwd.hpp b/include/tensorwrapper/sparsity/sparsity_fwd.hpp new file mode 100644 index 00000000..ff029d9c --- /dev/null +++ b/include/tensorwrapper/sparsity/sparsity_fwd.hpp @@ -0,0 +1,23 @@ +/* + * Copyright 2026 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 + +namespace tensorwrapper::sparsity { + +class Pattern; + +} diff --git a/include/tensorwrapper/symmetry/group.hpp b/include/tensorwrapper/symmetry/group.hpp index 591b4eae..823990de 100644 --- a/include/tensorwrapper/symmetry/group.hpp +++ b/include/tensorwrapper/symmetry/group.hpp @@ -20,6 +20,7 @@ #include #include #include +#include #include namespace tensorwrapper::symmetry { @@ -44,6 +45,9 @@ class Group : public utilities::IndexableContainerBase, /// Type of *this using my_type = Group; + /// Type of the traits class defining the types for *this + using traits_type = types::ClassTraits; + /// Type *this derives from to become container-like using container_type = utilities::IndexableContainerBase; @@ -60,11 +64,13 @@ class Group : public utilities::IndexableContainerBase, /// A read-only reference to a symmetry operation using const_reference = value_type::const_base_reference; - /// Unsigned integral type used for indexing and offsets - using size_type = std::size_t; - - /// Type used for mode indices - using mode_index_type = typename value_type::mode_index_type; + /// Pull in types from the traits class + ///@{ + using size_type = typename traits_type::size_type; + using rank_type = typename traits_type::rank_type; + using slice_type = typename traits_type::slice_type; + using offset_il_type = typename traits_type::offset_il_type; + ///@} // ------------------------------------------------------------------------- // -- Ctors and assignment @@ -85,8 +91,7 @@ class Group : public utilities::IndexableContainerBase, * * @throw None No throw guarantee. */ - explicit Group(mode_index_type rank) noexcept : - m_relations_{}, m_rank_(rank) {} + explicit Group(rank_type rank) noexcept : m_relations_{}, m_rank_(rank) {} /** @brief Creates a Group from the provided symmetry operations. * @@ -193,7 +198,121 @@ class Group : public utilities::IndexableContainerBase, * * @throw None No throw guarantee. */ - mode_index_type rank() const noexcept { return m_rank_.value_or(0); } + rank_type rank() const noexcept { return m_rank_.value_or(0); } + + /** @brief Slices a group given two initializer lists. + * + * C++ doesn't allow templates to work with initializer lists, therefore + * we must provide a special overload for when the input containers are + * initializer lists. This method simply dispatches to the range-based + * method by calling begin()/end() on each initializer list. See the + * description of that method for more details. + * + * @param[in] first_elem An initializer list containing the offsets of + * the first element IN the slice such that + * `first_elem[i]` is the offset along mode i. + * @param[in] last_elem An initializer list containing the offsets of + * the first element NOT IN the slice such that + * `last_elem[i]` is the offset along mode i. + * + * @return The requested slice. + * + * @throws ??? If the range-based method throws. Same throw guarantee. + */ + slice_type slice(offset_il_type first_elem, + offset_il_type last_elem) const { + return slice(first_elem.begin(), first_elem.end(), last_elem.begin(), + last_elem.end()); + } + + /** @brief Slices a group given two containers. + * + * @tparam ContainerType0 The type of first_elem. Assumed to have + * begin()/end() methods. + * @tparam ContainerType1 The type of last_elem. Assumed to have + * begin()/end() methods. + * + * Element indices are usually stored in containers. This overload is a + * convenience method for calling begin()/end() on the containers before + * dispatching to the range-based overload. See the documentation for the + * range-based overload for more details. + * + * @param[in] first_elem A container containing the offsets of + * the first element IN the slice such that + * `first_elem[i]` is the offset along mode i. + * @param[in] last_elem A container containing the offsets of + * the first element NOT IN the slice such that + * `last_elem[i]` is the offset along mode i. + * + * @return The requested slice. + * + * @throws ??? If the range-based method throws. Same throw guarantee. + */ + template + slice_type slice(ContainerType0&& first_elem, ContainerType1&& last_elem) { + return slice(first_elem.begin(), first_elem.end(), last_elem.begin(), + last_elem.end()); + } + + /** @brief Implements slicing given two ranges. + * + * @tparam BeginItr The type of the iterators pointing to offsets in the + * container holding the first element of the slice. + * @tparam EndItr The type of the iterators pointing to the offsets in + * the container holding the first element NOT in the + * slice. + * + * All other slice functions dispatch to this method. + * + * Slices are assumed to be contiguous, meaning we can uniquely specify + * the slice by providing the first element IN the slice and the first + * element NOT IN the slice. + * + * Specifying an element of a rank @f$r@f$ tensor requires providing + * @f$r@f$ offsets (one for each mode). Generally speaking, this requires + * the offsets to be in a container. This method takes iterators to those + * containers such that the @f$r@f$ elements in the range + * [first_elem_begin, first_elem_end) are the offsets of first element IN + * the slice and [last_elem_begin, last_elem_end) are the offsets of the + * first element NOT IN the slice. + * + * @note Both [first_elem_begin, first_elem_end) and + * [last_elem_begin, last_elem_end) being empty is allowed as long + * as *this is null or for a scalar. In these cases you will get back + * the only slice possible, which is the entire shape, i.e. a copy of + * *this. + * + * @param[in] first_elem_begin An iterator to the offset along mode 0 for + * the first element in the slice. + * @param[in] first_elem_end An iterator pointing to just past the offset + * along mode "r-1" (r being the rank of *this) for the first + * element in the slice. + * @param[in] last_elem_begin An iterator to the offset along mode 0 for + * the first element NOT in the slice. + * @param[in] last_elem_end An iterator pointing to just past the offset + * along mode "r-1" (r being the rank of *this) for the first + * element NOT in the slice. + * + * @return The requested slice. + * + * @throw std::runtime_error if the range + * [first_elem_begin, first_elem_end) does not contain the same + * number of elements as [last_elem_begin, last_elem_end). + * Strong throw guarantee. + * @throw std::runtime_error if the offsets in the range + * [first_elem_begin, first_elem_end) do not come before the + * offsets in [last_elem_begin, last_elem_end). Strong throw + * guarantee. + * @throw std::runtime_error if [first_elem_begin, first_elem_end) and + * [last_elem_begin, last_elem_end) contain the + * same number of offsets, but that number is NOT + * equal to the rank of *this. Strong throw + * guarantee. + * + */ + template + slice_type slice(BeginItr first_elem_begin, BeginItr first_elem_end, + EndItr last_elem_begin, EndItr last_elem_end) const; // ------------------------------------------------------------------------- // -- Utility methods @@ -291,7 +410,7 @@ class Group : public utilities::IndexableContainerBase, relation_container_type m_relations_; /// The rank of the tensor these symmetries apply to - std::optional m_rank_; + std::optional m_rank_; }; // -- Out of line implementations @@ -303,6 +422,44 @@ inline bool Group::count(const_reference op) const noexcept { return false; } +template +inline auto Group::slice(BeginItr first_elem_begin, BeginItr first_elem_end, + EndItr last_elem_begin, EndItr last_elem_end) const + -> slice_type { + if(size() != 0) + throw std::runtime_error("Slicing of non-trivial symmetry NYI"); + + auto first_done = [&]() { return first_elem_begin == first_elem_end; }; + auto last_done = [&]() { return last_elem_begin == last_elem_end; }; + + if(first_done() && last_done()) { + if(rank() == 0) return slice_type{}; + throw std::runtime_error("Offset ranks does not match tensor rank"); + } else if(first_done() || last_done()) { + throw std::runtime_error("Offsets do not have the same rank"); + } + + rank_type counter = 0; + for(; !first_done(); ++first_elem_begin, ++last_elem_begin) { + if(last_done()) + throw std::runtime_error("Offsets do not have the same rank."); + + auto fi = *first_elem_begin; + auto li = *last_elem_begin; + if(li <= fi) + throw std::runtime_error("First element in slice must be strictly " + "less than last element."); + + ++counter; + } + if(!last_done()) + throw std::runtime_error("Offsets do not have the same rank"); + if(counter != rank()) + throw std::runtime_error("Offset ranks do not match tensor rank"); + + return slice_type(rank()); +} + inline bool Group::operator==(const Group& rhs) const noexcept { if(rank() != rhs.rank()) return false; if(size() != rhs.size()) return false; diff --git a/include/tensorwrapper/symmetry/symmetry_fwd.hpp b/include/tensorwrapper/symmetry/symmetry_fwd.hpp new file mode 100644 index 00000000..7e6a57bb --- /dev/null +++ b/include/tensorwrapper/symmetry/symmetry_fwd.hpp @@ -0,0 +1,25 @@ +/* + * Copyright 2026 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 + +namespace tensorwrapper::symmetry { + +class Group; +class Operation; +class Permutation; + +} // namespace tensorwrapper::symmetry diff --git a/include/tensorwrapper/types/common_types.hpp b/include/tensorwrapper/types/common_types.hpp new file mode 100644 index 00000000..ddd74a16 --- /dev/null +++ b/include/tensorwrapper/types/common_types.hpp @@ -0,0 +1,35 @@ +/* + * Copyright 2026 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 +#include + +namespace tensorwrapper::types { + +/** @brief Types used throughout the entire TensorWrapper library. */ +struct CommonTypes { + /// Type used for indexing and offsets along modes + using size_type = std::size_t; + + /// Type used for describing the rank of a tensor and selecting a mode. + using rank_type = unsigned short; + + /// Type of an initializer list filled with size_type objects + using offset_il_type = std::initializer_list; +}; + +} // namespace tensorwrapper::types diff --git a/include/tensorwrapper/types/layout_traits.hpp b/include/tensorwrapper/types/layout_traits.hpp new file mode 100644 index 00000000..570f2218 --- /dev/null +++ b/include/tensorwrapper/types/layout_traits.hpp @@ -0,0 +1,34 @@ +/* + * Copyright 2026 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 "layout/layout_base.hpp" +#include +#include +#include + +namespace tensorwrapper::types { + +template<> +struct ClassTraits : CommonTypes {}; + +template +struct ClassTraits> + : ClassTraits { + using slice_type = Derived; +}; + +} // namespace tensorwrapper::types diff --git a/include/tensorwrapper/types/shape_traits.hpp b/include/tensorwrapper/types/shape_traits.hpp index 00d7c6dd..acfb76bd 100644 --- a/include/tensorwrapper/types/shape_traits.hpp +++ b/include/tensorwrapper/types/shape_traits.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2025 NWChemEx-Project + * 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. @@ -15,22 +15,66 @@ */ #pragma once -#include -#include +#include +#include #include +#include namespace tensorwrapper::types { -struct ShapeTraitsCommon { - using size_type = std::size_t; - using rank_type = unsigned short; +template<> +struct ClassTraits : public CommonTypes { + using shape_base = shape::ShapeBase; + using base_pointer = std::unique_ptr; +}; + +template<> +struct ClassTraits : public CommonTypes { + using shape_base = shape::ShapeBase; + using base_pointer = std::unique_ptr; +}; + +template +struct ClassTraits> + : public ClassTraits { + using value_type = Derived; + using const_value_type = const value_type; + using reference = value_type&; + using const_reference = const value_type&; + using pointer = value_type*; + using const_pointer = const value_type*; + using slice_type = Derived; +}; + +template +struct ClassTraits> + : public ClassTraits { + using value_type = Derived; + using const_value_type = const value_type; + using reference = const value_type&; + using const_reference = const value_type&; + using pointer = const value_type*; + using const_pointer = const value_type*; + using slice_type = Derived; }; template<> -struct ClassTraits : public ShapeTraitsCommon {}; +struct ClassTraits + : public ClassTraits> {}; template<> -struct ClassTraits - : public ShapeTraitsCommon {}; +struct ClassTraits + : public ClassTraits> {}; + +template +struct ClassTraits> + : public ClassTraits> { + using smooth_traits = ClassTraits; + using pimpl_type = shape::detail_::SmoothViewPIMPL; + using const_pimpl_type = + shape::detail_::SmoothViewPIMPL; + using pimpl_pointer = std::unique_ptr; + using const_pimpl_pointer = std::unique_ptr; +}; } // namespace tensorwrapper::types diff --git a/include/tensorwrapper/types/sparsity_traits.hpp b/include/tensorwrapper/types/sparsity_traits.hpp new file mode 100644 index 00000000..bc1c0fe7 --- /dev/null +++ b/include/tensorwrapper/types/sparsity_traits.hpp @@ -0,0 +1,30 @@ +/* + * Copyright 2026 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 +#include +#include + +namespace tensorwrapper::types { + +template<> +struct ClassTraits : CommonTypes { + using slice_type = sparsity::Pattern; +}; + +} // namespace tensorwrapper::types diff --git a/include/tensorwrapper/types/symmetry_traits.hpp b/include/tensorwrapper/types/symmetry_traits.hpp new file mode 100644 index 00000000..f8183b5f --- /dev/null +++ b/include/tensorwrapper/types/symmetry_traits.hpp @@ -0,0 +1,29 @@ +/* + * Copyright 2026 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 +#include +#include + +namespace tensorwrapper::types { + +template<> +struct ClassTraits : CommonTypes { + using slice_type = symmetry::Group; +}; + +} // namespace tensorwrapper::types diff --git a/include/tensorwrapper/types/types.hpp b/include/tensorwrapper/types/types.hpp index 76f09101..580bf41b 100644 --- a/include/tensorwrapper/types/types.hpp +++ b/include/tensorwrapper/types/types.hpp @@ -19,6 +19,9 @@ #include #include #include +#include +#include +#include /// Namespace for defining types used throughout the TensorWrapper library namespace tensorwrapper::types {} diff --git a/src/tensorwrapper/shape/detail_/smooth_view_pimpl.hpp b/src/tensorwrapper/shape/detail_/smooth_view_pimpl.hpp index a65fce82..a9d3566c 100644 --- a/src/tensorwrapper/shape/detail_/smooth_view_pimpl.hpp +++ b/src/tensorwrapper/shape/detail_/smooth_view_pimpl.hpp @@ -52,7 +52,7 @@ class SmoothViewPIMPL : public tensorwrapper::detail_::PolymorphicBase< /// Type of a SmoothViewPIMPL if it aliases a const Smooth using const_smooth_view_pimpl_pointer = - typename ShapeTraits::const_pimpl_pointer; + typename types::ClassTraits::const_pimpl_pointer; /// Derived class implements by overriding extent_ rank_type extent(size_type i) const { return extent_(i); } diff --git a/tests/cxx/unit_tests/tensorwrapper/layout/layout_base.cpp b/tests/cxx/unit_tests/tensorwrapper/layout/layout_base.cpp index 6e03dd3e..8d1a033b 100644 --- a/tests/cxx/unit_tests/tensorwrapper/layout/layout_base.cpp +++ b/tests/cxx/unit_tests/tensorwrapper/layout/layout_base.cpp @@ -38,13 +38,17 @@ TEST_CASE("LayoutBase") { Physical phys_copy_no_sym(matrix_shape, no_symm, no_sparsity); Physical phys_copy_has_sym(matrix_shape, symm, no_sparsity); Physical phys_copy_just_shape(matrix_shape); + Physical phys_defaulted; // Test via references to the base class LayoutBase& base_copy_no_sym = phys_copy_no_sym; LayoutBase& base_copy_has_sym = phys_copy_has_sym; LayoutBase& base_copy_just_shape = phys_copy_just_shape; + LayoutBase& base_defaulted = phys_defaulted; SECTION("Ctors") { + SECTION("Default") { REQUIRE(base_defaulted.is_null()); } + SECTION("Copy state") { REQUIRE(base_copy_no_sym.shape().are_equal(matrix_shape)); REQUIRE(base_copy_no_sym.symmetry().are_equal(no_symm)); @@ -136,12 +140,25 @@ TEST_CASE("LayoutBase") { REQUIRE(base_copy_has_sym.sparsity() == no_sparsity); } + SECTION("is_null") { + REQUIRE(base_defaulted.is_null()); + REQUIRE_FALSE(base_copy_no_sym.is_null()); + REQUIRE_FALSE(base_copy_has_sym.is_null()); + REQUIRE_FALSE(base_copy_just_shape.is_null()); + } + SECTION("rank") { REQUIRE(base_copy_no_sym.rank() == 2); REQUIRE(base_copy_has_sym.rank() == 2); } SECTION("operator==") { + // Empty vs empty + REQUIRE(base_defaulted == base_defaulted); + + // Empty vs non-empty + REQUIRE_FALSE(base_defaulted == base_copy_no_sym); + // Same REQUIRE(base_copy_no_sym == Physical(matrix_shape, no_symm, no_sparsity)); diff --git a/tests/cxx/unit_tests/tensorwrapper/layout/layout_common.cpp b/tests/cxx/unit_tests/tensorwrapper/layout/layout_common.cpp new file mode 100644 index 00000000..0182badc --- /dev/null +++ b/tests/cxx/unit_tests/tensorwrapper/layout/layout_common.cpp @@ -0,0 +1,92 @@ +/* + * Copyright 2026 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. + */ + +#include "../testing/testing.hpp" +#include +#include + +using namespace tensorwrapper; + +TEST_CASE("LayoutCommon") { + using shape_type = shape::Smooth; + using layout_type = layout::Physical; + using size_type = layout_type::size_type; + using size_vector = std::vector; + + shape_type scalar_shape{}; + shape_type vector_shape{3}; + shape_type matrix_shape{3, 5}; + shape_type tensor_shape{9, 4, 12}; + + layout_type empty; + layout_type scalar(scalar_shape); + layout_type vector(vector_shape); + layout_type matrix(matrix_shape); + layout_type tensor(tensor_shape); + + // Spot check, just calls slice(range) + SECTION("slice(initializer list)") { + REQUIRE(empty.slice({}, {}) == empty); + + layout_type vector_corr(vector_shape.slice({1}, {2})); + REQUIRE(vector.slice({1}, {2}) == vector_corr); + + layout_type matrix_corr(matrix_shape.slice({1, 2}, {3, 5})); + REQUIRE(matrix.slice({1, 2}, {3, 5}) == matrix_corr); + + layout_type tensor_corr(tensor_shape.slice({3, 1, 5}, {7, 4, 10})); + REQUIRE(tensor.slice({3, 1, 5}, {7, 4, 10}) == tensor_corr); + } + + // Spot check, just calls slice(range) + SECTION("slice(initializer list)") { + size_vector i; + REQUIRE(empty.slice(i, i) == empty); + + size_vector i1{1}, i2{2}; + layout_type vector_corr(vector_shape.slice({1}, {2})); + REQUIRE(vector.slice(i1, i2) == vector_corr); + + size_vector i12{1, 2}, i35{3, 5}; + layout_type matrix_corr(matrix_shape.slice({1, 2}, {3, 5})); + REQUIRE(matrix.slice(i12, i35) == matrix_corr); + + size_vector i315{3, 1, 5}, i7410{7, 4, 10}; + layout_type tensor_corr(tensor_shape.slice({3, 1, 5}, {7, 4, 10})); + REQUIRE(tensor.slice(i315, i7410) == tensor_corr); + } + + // Spot check, just dispatches to shape/group/pattern slice method + SECTION("slice(initializer list)") { + size_vector i; + REQUIRE(empty.slice(i.begin(), i.end(), i.begin(), i.end()) == empty); + + size_vector i1{1}, i2{2}; + layout_type vector_corr(vector_shape.slice({1}, {2})); + REQUIRE(vector.slice(i1.begin(), i1.end(), i2.begin(), i2.end()) == + vector_corr); + + size_vector i12{1, 2}, i35{3, 5}; + layout_type matrix_corr(matrix_shape.slice({1, 2}, {3, 5})); + REQUIRE(matrix.slice(i12.begin(), i12.end(), i35.begin(), i35.end()) == + matrix_corr); + + size_vector i315{3, 1, 5}, i7410{7, 4, 10}; + layout_type tensor_corr(tensor_shape.slice({3, 1, 5}, {7, 4, 10})); + REQUIRE(tensor.slice(i315.begin(), i315.end(), i7410.begin(), + i7410.end()) == tensor_corr); + } +} diff --git a/tests/cxx/unit_tests/tensorwrapper/sparsity/pattern.cpp b/tests/cxx/unit_tests/tensorwrapper/sparsity/pattern.cpp index bb3fdb3e..369734b6 100644 --- a/tests/cxx/unit_tests/tensorwrapper/sparsity/pattern.cpp +++ b/tests/cxx/unit_tests/tensorwrapper/sparsity/pattern.cpp @@ -20,8 +20,13 @@ using namespace tensorwrapper::testing; using namespace tensorwrapper::sparsity; TEST_CASE("Pattern") { + using size_type = Pattern::size_type; + Pattern defaulted; + Pattern p0(0); Pattern p1(1); + Pattern p2(2); + Pattern p3(3); SECTION("Ctors, assignment") { SECTION("Default") { REQUIRE(defaulted.rank() == 0); } @@ -37,7 +42,59 @@ TEST_CASE("Pattern") { SECTION("rank") { REQUIRE(defaulted.rank() == 0); + REQUIRE(p0.rank() == 0); REQUIRE(p1.rank() == 1); + REQUIRE(p2.rank() == 2); + REQUIRE(p3.rank() == 3); + } + + SECTION("slice(initializer_lists)") { + using except_t = std::runtime_error; + REQUIRE(defaulted.slice({}, {}) == defaulted); + REQUIRE(p0.slice({}, {}) == p0); + REQUIRE(p1.slice({0}, {1}) == p1); + REQUIRE(p2.slice({0, 0}, {1, 1}) == p2); + REQUIRE(p3.slice({0, 0, 0}, {1, 1, 1}) == p3); + + // Offset ranks don't match: one empty offset + REQUIRE_THROWS_AS(p1.slice({}, {1}), except_t); + + // Offset ranks don't match: catch in loop + REQUIRE_THROWS_AS(p2.slice({0, 0}, {1}), except_t); + + // Offset ranks don't match: catch after loop + REQUIRE_THROWS_AS(p1.slice({0}, {1, 1}), except_t); + + // Offset ranks don't match tensor's rank: empty offsets + REQUIRE_THROWS_AS(p1.slice({}, {}), except_t); + + // Offset ranks don't match tensor's rank: non-empty + REQUIRE_THROWS_AS(p1.slice({0, 0}, {1, 1}), except_t); + + // first_elem == last_elem + REQUIRE_THROWS_AS(p1.slice({1}, {1}), except_t); + + // first_elem > last_elem + REQUIRE_THROWS_AS(p1.slice({1}, {0}), except_t); + } + + SECTION("slice(containers)") { + // This overload dispatches to the ranges overload, so we just spot + // check. + + std::vector empty_idx; + REQUIRE(defaulted.slice(empty_idx, empty_idx) == defaulted); + } + + SECTION("slice(ranges)") { + // For convenience we thoroughly test this function via the il overload. + // All overloads dispatch to this one, thus it's thoroughly tested + // there. + + std::vector empty_idx; + auto eb = empty_idx.begin(); + auto ee = empty_idx.end(); + REQUIRE(defaulted.slice(eb, ee, eb, ee) == defaulted); } SECTION("operator==") { diff --git a/tests/cxx/unit_tests/tensorwrapper/symmetry/group.cpp b/tests/cxx/unit_tests/tensorwrapper/symmetry/group.cpp index 3d611bd9..8a0a1226 100644 --- a/tests/cxx/unit_tests/tensorwrapper/symmetry/group.cpp +++ b/tests/cxx/unit_tests/tensorwrapper/symmetry/group.cpp @@ -15,6 +15,7 @@ */ #include "../testing/testing.hpp" +#include #include #include @@ -24,11 +25,16 @@ using namespace tensorwrapper::symmetry; TEST_CASE("Group") { using cycle_type = typename Permutation::cycle_type; using label_type = typename Group::label_type; + using size_type = typename Group::size_type; Permutation p01(4, cycle_type{0, 1}); Permutation p23(4, cycle_type{2, 3}); Group empty; + Group scalar(0); + Group vector(1); + Group matrix(2); + Group tensor(3); Group g(p01, p23); SECTION("Ctors and assignment") { @@ -74,15 +80,75 @@ TEST_CASE("Group") { SECTION("count") { REQUIRE_FALSE(empty.count(p01)); + REQUIRE_FALSE(scalar.count(p01)); + REQUIRE_FALSE(vector.count(p01)); + REQUIRE_FALSE(matrix.count(p01)); + REQUIRE_FALSE(tensor.count(p01)); REQUIRE(g.count(p01)); REQUIRE(g.count(p23)); } SECTION("rank") { REQUIRE(empty.rank() == 0); + REQUIRE(scalar.rank() == 0); + REQUIRE(vector.rank() == 1); + REQUIRE(matrix.rank() == 2); + REQUIRE(tensor.rank() == 3); REQUIRE(g.rank() == 4); } + SECTION("slice(initializer_lists)") { + using except_t = std::runtime_error; + REQUIRE(empty.slice({}, {}) == empty); + REQUIRE(scalar.slice({}, {}) == scalar); + REQUIRE(vector.slice({0}, {1}) == vector); + REQUIRE(matrix.slice({0, 0}, {1, 1}) == matrix); + REQUIRE(tensor.slice({0, 0, 0}, {1, 1, 1}) == tensor); + + // NYI for actual symmetries + REQUIRE_THROWS_AS(g.slice({0, 0, 0, 0}, {1, 1, 1, 1}), except_t); + + // Offset ranks don't match: one empty offset + REQUIRE_THROWS_AS(vector.slice({}, {1}), except_t); + + // Offset ranks don't match: catch in loop + REQUIRE_THROWS_AS(matrix.slice({0, 0}, {1}), except_t); + + // Offset ranks don't match: catch after loop + REQUIRE_THROWS_AS(vector.slice({0}, {1, 1}), except_t); + + // Offset ranks don't match tensor's rank: empty offsets + REQUIRE_THROWS_AS(vector.slice({}, {}), except_t); + + // Offset ranks don't match tensor's rank: non-empty + REQUIRE_THROWS_AS(vector.slice({0, 0}, {1, 1}), except_t); + + // first_elem == last_elem + REQUIRE_THROWS_AS(vector.slice({1}, {1}), except_t); + + // first_elem > last_elem + REQUIRE_THROWS_AS(vector.slice({1}, {0}), except_t); + } + + SECTION("slice(containers)") { + // This overload dispatches to the ranges overload, so we just spot + // check. + + std::vector empty_idx; + REQUIRE(empty.slice(empty_idx, empty_idx) == empty); + } + + SECTION("slice(ranges)") { + // For convenience we thoroughly test this function via the il overload. + // All overloads dispatch to this one, thus it's thoroughly tested + // there. + + std::vector empty_idx; + auto eb = empty_idx.begin(); + auto ee = empty_idx.end(); + REQUIRE(empty.slice(eb, ee, eb, ee) == empty); + } + SECTION("swap") { Group copy_empty(empty); Group copy_g(g);