Repository: xtensor-stack/xtensor (language: python)

Issue:
Add `xt::swapaxes` and `xt::moveaxis`
Relevant NumPy doc
- https://numpy.org/doc/stable/reference/generated/numpy.swapaxes.html
- https://numpy.org/doc/stable/reference/generated/numpy.moveaxis.html


Discussion / hints:

Fix patch:
diff --git a/.azure-pipelines/azure-pipelines-win.yml b/.azure-pipelines/azure-pipelines-win.yml
index cf8fb8f8c..691b8a744 100644
--- a/.azure-pipelines/azure-pipelines-win.yml
+++ b/.azure-pipelines/azure-pipelines-win.yml
@@ -37,7 +37,7 @@ jobs:
                     ninja ^
                     doctest==2.4.7 ^
                     nlohmann_json ^
-                    xtl==0.7.4 ^
+                    xtl==0.7.5 ^
                     xsimd==10.0.0 ^
                     python=3.9
       conda list
diff --git a/README.md b/README.md
index 5e96aa4b4..d087f2e7a 100644
--- a/README.md
+++ b/README.md
@@ -97,7 +97,7 @@ library:
 
 | `xtensor` | `xtl`   |`xsimd` (optional) |
 |-----------|---------|-------------------|
-|  master   | ^0.7.0  |       ^8.0.3      |
+|  master   | ^0.7.5  |       ^8.0.3      |
 |  0.24.x   | ^0.7.0  |       ^8.0.3      |
 |  0.23.x   | ^0.7.0  |       ^7.4.8      |
 |  0.22.0   | ^0.6.23 |       ^7.4.8      |
diff --git a/docs/source/api/xmanipulation.rst b/docs/source/api/xmanipulation.rst
index 18f03f615..784dbe742 100644
--- a/docs/source/api/xmanipulation.rst
+++ b/docs/source/api/xmanipulation.rst
@@ -9,48 +9,8 @@ xmanipulation
 
 Defined in ``xtensor/xmanipulation.hpp``
 
-.. doxygenfunction:: xt::atleast_Nd
+.. cpp:namespace-push:: xt
 
-.. doxygenfunction:: xt::atleast_1d
+.. doxygengroup:: xt_xmanipulation
 
-.. doxygenfunction:: xt::atleast_2d
-
-.. doxygenfunction:: xt::atleast_3d
-
-.. doxygenfunction:: xt::expand_dims
-
-.. doxygenfunction:: xt::flatten
-
-.. doxygenfunction:: xt::flatnonzero
-
-.. doxygenfunction:: xt::flip
-
-.. doxygenfunction:: xt::ravel
-
-.. doxygenfunction:: xt::repeat(E&&, std::size_t, std::size_t)
-
-.. doxygenfunction:: xt::repeat(E&&, const std::vector<std::size_t>&, std::size_t)
-
-.. doxygenfunction:: xt::repeat(E&&, std::vector<std::size_t>&&, std::size_t)
-
-.. doxygenfunction:: xt::roll(E&&, std::ptrdiff_t)
-
-.. doxygenfunction:: xt::roll(E&&, std::ptrdiff_t, std::ptrdiff_t)
-
-.. doxygenfunction:: xt::rot90
-
-.. doxygenfunction:: xt::split
-
-.. doxygenfunction:: xt::hsplit
-
-.. doxygenfunction:: xt::vsplit
-
-.. doxygenfunction:: xt::squeeze(E&&)
-
-.. doxygenfunction:: xt::squeeze(E&&, S&&, Tag)
-
-.. doxygenfunction:: xt::transpose(E&&)
-
-.. doxygenfunction:: xt::transpose(E&&, S&&, Tag)
-
-.. doxygenfunction:: xt::trim_zeros
+.. cpp:namespace-pop::
diff --git a/docs/source/numpy.rst b/docs/source/numpy.rst
index eaf5f8f51..ac2b0bebb 100644
--- a/docs/source/numpy.rst
+++ b/docs/source/numpy.rst
@@ -255,36 +255,42 @@ Rearrange elements
 In the same spirit as concatenation, the following operations do not allocate any memory and do
 not modify the underlying xexpression.
 
-.. table::
+.. list-table::
    :widths: 50 50
+   :header-rows: 1
 
-   +-----------------------------------------------------+-----------------------------------------------------------------------+
-   |            Python 3 - NumPy                         |                C++ 14 - xtensor                                       |
-   +=====================================================+=======================================================================+
-   | :any:`np.diag(a) <numpy.diag>`                      | :cpp:func:`xt::diag(a) <xt::diag>`                                    |
-   +-----------------------------------------------------+-----------------------------------------------------------------------+
-   | :any:`np.diagonal(a) <numpy.diagonal>`              | :cpp:func:`xt::diagonal(a) <xt::diagonal>`                            |
-   +-----------------------------------------------------+-----------------------------------------------------------------------+
-   | :any:`np.triu(a) <numpy.triu>`                      | :cpp:func:`xt::triu(a) <xt::triu>`                                    |
-   +-----------------------------------------------------+-----------------------------------------------------------------------+
-   | :any:`np.tril(a, k=1) <numpy.tril>`                 | :cpp:func:`xt::tril(a, 1) <xt::tril>`                                 |
-   +-----------------------------------------------------+-----------------------------------------------------------------------+
-   | :any:`np.flip(a, axis=3) <numpy.flip>`              | :cpp:func:`xt::flip(a, 3) <xt::flip>`                                 |
-   +-----------------------------------------------------+-----------------------------------------------------------------------+
-   | :any:`np.flipud(a) <numpy.flipud>`                  | :cpp:func:`xt::flip(a, 0) <xt::flip>`                                 |
-   +-----------------------------------------------------+-----------------------------------------------------------------------+
-   | :any:`np.fliplr(a) <numpy.fliplr>`                  | :cpp:func:`xt::flip(a, 1) <xt::flip>`                                 |
-   +-----------------------------------------------------+-----------------------------------------------------------------------+
-   | :any:`np.transpose(a, (1, 0, 2)) <numpy.transpose>` | :cpp:func:`xt::transpose(a, {1, 0, 2}) <xt::transpose>`               |
-   +-----------------------------------------------------+-----------------------------------------------------------------------+
-   | :any:`np.ravel(a, order='F') <numpy.ravel>`         | :cpp:func:`xt::ravel\<xt::layout_type::column_major\>(a) <xt::ravel>` |
-   +-----------------------------------------------------+-----------------------------------------------------------------------+
-   | :any:`np.rot90(a) <numpy.rot90>`                    | :cpp:func:`xt::rot90(a) <xt::rot90>`                                  |
-   +-----------------------------------------------------+-----------------------------------------------------------------------+
-   | :any:`np.rot90(a, 2, (1, 2)) <numpy.rot90>`         | :cpp:func:`xt::rot90\<2\>(a, {1, 2}) <xt::rot90>`                     |
-   +-----------------------------------------------------+-----------------------------------------------------------------------+
-   | :any:`np.roll(a, 2, axis=1) <numpy.roll>`           | :cpp:func:`xt::roll(a, 2, 1) <xt::roll>`                              |
-   +-----------------------------------------------------+-----------------------------------------------------------------------+
+   * - Python3 - NumPy
+     - C++14 - xtensor
+   * - :any:`np.nan_to_num(a) <numpy.nan_to_num>`
+     - :cpp:func:`xt::nan_to_num(a) <xt::nan_to_num>`
+   * - :any:`np.diag(a) <numpy.diag>`
+     - :cpp:func:`xt::diag(a) <xt::diag>`
+   * - :any:`np.diagonal(a) <numpy.diagonal>`
+     - :cpp:func:`xt::diagonal(a) <xt::diagonal>`
+   * - :any:`np.triu(a) <numpy.triu>`
+     - :cpp:func:`xt::triu(a) <xt::triu>`
+   * - :any:`np.tril(a, k=1) <numpy.tril>`
+     - :cpp:func:`xt::tril(a, 1) <xt::tril>`
+   * - :any:`np.flip(a, axis=3) <numpy.flip>`
+     - :cpp:func:`xt::flip(a, 3) <xt::flip>`
+   * - :any:`np.flipud(a) <numpy.flipud>`
+     - :cpp:func:`xt::flip(a, 0) <xt::flip>`
+   * - :any:`np.fliplr(a) <numpy.fliplr>`
+     - :cpp:func:`xt::flip(a, 1) <xt::flip>`
+   * - :any:`np.transpose(a, (1, 0, 2)) <numpy.transpose>`
+     - :cpp:func:`xt::transpose(a, {1, 0, 2}) <xt::transpose>`
+   * - :any:`np.swapaxes(a, 0, -1) <numpy.swapaxes>`
+     - :cpp:func:`xt::swapaxes(a, 0, -1) <xt::swapaxes>`
+   * - :any:`np.moveaxis(a, 0, -1) <numpy.moveaxis>`
+     - :cpp:func:`xt::moveaxis(a, 0, -1) <xt::moveaxis>`
+   * - :any:`np.ravel(a, order='F') <numpy.ravel>`
+     - :cpp:func:`xt::ravel\<xt::layout_type::column_major\>(a) <xt::ravel>`
+   * - :any:`np.rot90(a) <numpy.rot90>`
+     - :cpp:func:`xt::rot90(a) <xt::rot90>`
+   * - :any:`np.rot90(a, 2, (1, 2)) <numpy.rot90>`
+     - :cpp:func:`xt::rot90\<2\>(a, {1, 2}) <xt::rot90>`
+   * - :any:`np.roll(a, 2, axis=1) <numpy.roll>`
+     - :cpp:func:`xt::roll(a, 2, 1) <xt::roll>`
 
 Iteration
 ---------
diff --git a/environment-dev.yml b/environment-dev.yml
index 76fd110aa..efb744249 100644
--- a/environment-dev.yml
+++ b/environment-dev.yml
@@ -3,7 +3,7 @@ channels:
 - conda-forge
 dependencies:
 - cmake
-- xtl=0.7.4
+- xtl=0.7.5
 - xsimd=10.0.0
 - nlohmann_json
 - doctest=2.4.7
diff --git a/include/xtensor/xexception.hpp b/include/xtensor/xexception.hpp
index 996b25727..30e1287af 100644
--- a/include/xtensor/xexception.hpp
+++ b/include/xtensor/xexception.hpp
@@ -15,7 +15,9 @@
 #include <sstream>
 #include <stdexcept>
 #include <string>
+#include <type_traits>
 
+#include <xtl/xcompare.hpp>
 #include <xtl/xsequence.hpp>
 #include <xtl/xspan_impl.hpp>
 
@@ -278,6 +280,24 @@ namespace xt
         }
     }
 
+    /*******************************
+     *  check_axis implementation  *
+     *******************************/
+
+    template <class A, class D>
+    inline void check_axis_in_dim(A axis, D dim, const char* subject = "Axis")
+    {
+        auto const sdim = static_cast<std::make_signed_t<D>>(dim);
+        if (xtl::cmp_greater_equal(axis, dim) || xtl::cmp_less(axis, -sdim))
+        {
+            XTENSOR_THROW(
+                std::out_of_range,
+                std::string(subject) + " (" + std::to_string(axis)
+                    + ") is not within the number of dimensions (" + std::to_string(dim) + ')'
+            );
+        }
+    }
+
     /****************
      * check_access *
      ****************/
diff --git a/include/xtensor/xmanipulation.hpp b/include/xtensor/xmanipulation.hpp
index 818ea421f..c967ff69c 100644
--- a/include/xtensor/xmanipulation.hpp
+++ b/include/xtensor/xmanipulation.hpp
@@ -10,7 +10,14 @@
 #ifndef XTENSOR_MANIPULATION_HPP
 #define XTENSOR_MANIPULATION_HPP
 
+#include <algorithm>
+#include <utility>
+
+#include <xtl/xcompare.hpp>
+#include <xtl/xsequence.hpp>
+
 #include "xbuilder.hpp"
+#include "xexception.hpp"
 #include "xrepeat.hpp"
 #include "xstrided_view.hpp"
 #include "xtensor_config.hpp"
@@ -18,6 +25,10 @@
 
 namespace xt
 {
+    /**
+     * @defgroup xt_xmanipulation
+     */
+
     namespace check_policy
     {
         struct none
@@ -35,6 +46,9 @@ namespace xt
     template <class E, class S, class Tag = check_policy::none>
     auto transpose(E&& e, S&& permutation, Tag check_policy = Tag());
 
+    template <class E>
+    auto swapaxes(E&& e, std::ptrdiff_t axis1, std::ptrdiff_t axis2);
+
     template <layout_type L = XTENSOR_DEFAULT_TRAVERSAL, class E>
     auto ravel(E&& e);
 
@@ -215,6 +229,8 @@ namespace xt
 
     /**
      * Returns a transpose view by reversing the dimensions of xexpression e
+     *
+     * @ingroup xt_xmanipulation
      * @param e the input expression
      */
     template <class E>
@@ -242,6 +258,8 @@ namespace xt
 
     /**
      * Returns a transpose view by permuting the xexpression e with @p permutation.
+     *
+     * @ingroup xt_xmanipulation
      * @param e the input expression
      * @param permutation the sequence containing permutation
      * @param check_policy the check level (check_policy::full() or check_policy::none())
@@ -262,6 +280,101 @@ namespace xt
 
     /// @endcond
 
+    /*****************************
+     *  swapaxes implementation  *
+     *****************************/
+
+    namespace detail
+    {
+        template <class S>
+        inline S swapaxes_perm(std::size_t dim, std::ptrdiff_t axis1, std::ptrdiff_t axis2)
+        {
+            const std::size_t ax1 = normalize_axis(dim, axis1);
+            const std::size_t ax2 = normalize_axis(dim, axis2);
+            auto perm = xtl::make_sequence<S>(dim, 0);
+            using id_t = typename S::value_type;
+            std::iota(perm.begin(), perm.end(), id_t(0));
+            perm[ax1] = ax2;
+            perm[ax2] = ax1;
+            return perm;
+        }
+    }
+
+    /**
+     * Return a new expression with two axes interchanged.
+     *
+     * The two axis parameter @p axis and @p axis2 are interchangable.
+     *
+     * @ingroup xt_xmanipulation
+     * @param e The input expression
+     * @param axis1 First axis to swap
+     * @param axis2 Second axis to swap
+     */
+    template <class E>
+    inline auto swapaxes(E&& e, std::ptrdiff_t axis1, std::ptrdiff_t axis2)
+    {
+        const auto dim = e.dimension();
+        check_axis_in_dim(axis1, dim, "Parameter axis1");
+        check_axis_in_dim(axis2, dim, "Parameter axis2");
+
+        using strides_t = get_strides_t<typename std::decay_t<E>::shape_type>;
+        return transpose(std::forward<E>(e), detail::swapaxes_perm<strides_t>(dim, axis1, axis2));
+    }
+
+    /*****************************
+     *  moveaxis implementation  *
+     *****************************/
+
+    namespace detail
+    {
+        template <class S>
+        inline S moveaxis_perm(std::size_t dim, std::ptrdiff_t src, std::ptrdiff_t dest)
+        {
+            using id_t = typename S::value_type;
+
+            const std::size_t src_norm = normalize_axis(dim, src);
+            const std::size_t dest_norm = normalize_axis(dim, dest);
+
+            // Initializing to src_norm handles case where `dest == -1` and the loop
+            // does not go check `perm_idx == dest_norm` a `dim+1`th time.
+            auto perm = xtl::make_sequence<S>(dim, src_norm);
+            id_t perm_idx = 0;
+            for (id_t i = 0; xtl::cmp_less(i, dim); ++i)
+            {
+                if (xtl::cmp_equal(perm_idx, dest_norm))
+                {
+                    perm[perm_idx] = src_norm;
+                    ++perm_idx;
+                }
+                if (xtl::cmp_not_equal(i, src_norm))
+                {
+                    perm[perm_idx] = i;
+                    ++perm_idx;
+                }
+            }
+            return perm;
+        }
+    }
+
+    /**
+     * Return a new expression with an axis move to a new position.
+     *
+     * @ingroup xt_xmanipulation
+     * @param e The input expression
+     * @param src Original position of the axis to move
+     * @param dest Destination position for the original axis.
+     */
+    template <class E>
+    inline auto moveaxis(E&& e, std::ptrdiff_t src, std::ptrdiff_t dest)
+    {
+        auto const dim = e.dimension();
+        check_axis_in_dim(src, dim, "Parameter src");
+        check_axis_in_dim(dest, dim, "Parameter dest");
+
+        using strides_t = get_strides_t<typename std::decay_t<E>::shape_type>;
+        return xt::transpose(std::forward<E>(e), detail::moveaxis_perm<strides_t>(e.dimension(), src, dest));
+    }
+
     /************************************
      * ravel and flatten implementation *
      ************************************/
@@ -292,10 +405,12 @@ namespace xt
     }
 
     /**
-     * Returns a flatten view of the given expression. No copy is made.
+     * Return a flatten view of the given expression. No copy is made.
+     *
+     * @ingroup xt_xmanipulation
      * @param e the input expression
-     * @tparam L the layout used to read the elements of e. If no parameter
-     * is specified, XTENSOR_DEFAULT_TRAVERSAL is used.
+     * @tparam L the layout used to read the elements of e.
+     *     If no parameter is specified, XTENSOR_DEFAULT_TRAVERSAL is used.
      * @tparam E the type of the expression
      */
     template <layout_type L, class E>
@@ -311,12 +426,15 @@ namespace xt
     }
 
     /**
-     * Returns a flatten view of the given expression. No copy is made. This
-     * method is equivalent to ravel and is provided for API sameness with
-     * Numpy.
+     * Return a flatten view of the given expression.
+     *
+     * No copy is made.
+     * This method is equivalent to ravel and is provided for API sameness with Numpy.
+     *
+     * @ingroup xt_xmanipulation
      * @param e the input expression
-     * @tparam L the layout used to read the elements of e. If no parameter
-     * is specified, XTENSOR_DEFAULT_TRAVERSAL is used.
+     * @tparam L the layout used to read the elements of e.
+     *     If no parameter is specified, XTENSOR_DEFAULT_TRAVERSAL is used.
      * @tparam E the type of the expression
      * @sa ravel
      */
@@ -327,8 +445,9 @@ namespace xt
     }
 
     /**
-     * @brief return indices that are non-zero in the flattened version of arr,
-     * equivalent to nonzero(ravel<layout_type>(arr))[0];
+     * Return indices that are non-zero in the flattened version of arr.
+     *
+     * Equivalent to ``nonzero(ravel<layout_type>(arr))[0];``
      *
      * @param arr input array
      * @return indices that are non-zero in the flattened version of arr
@@ -346,6 +465,7 @@ namespace xt
     /**
      * Trim zeros at beginning, end or both of 1D sequence.
      *
+     * @ingroup xt_xmanipulation
      * @param e input xexpression
      * @param direction string of either 'f' for trim from beginning, 'b' for trim from end
      *                  or 'fb' (default) for both.
@@ -381,9 +501,11 @@ namespace xt
      **************************/
 
     /**
-     * Returns a squeeze view of the given expression. No copy is made.
-     * Squeezing an expression removes dimensions of extent 1.
+     * Returns a squeeze view of the given expression.
+     *
+     * No copy is made. Squeezing an expression removes dimensions of extent 1.
      *
+     * @ingroup xt_xmanipulation
      * @param e the input expression
      * @tparam E the type of the expression
      */
@@ -457,8 +579,9 @@ namespace xt
     }
 
     /**
-     * @brief Remove single-dimensional entries from the shape of an xexpression
+     * Remove single-dimensional entries from the shape of an xexpression
      *
+     * @ingroup xt_xmanipulation
      * @param e input xexpression
      * @param axis integer or container of integers, select a subset of single-dimensional
      *        entries of the shape.
@@ -496,11 +619,12 @@ namespace xt
      ******************************/
 
     /**
-     * @brief Expand the shape of an xexpression.
+     * Expand the shape of an xexpression.
      *
      * Insert a new axis that will appear at the axis position in the expanded array shape.
      * This will return a ``strided_view`` with a ``xt::newaxis()`` at the indicated axis.
      *
+     * @ingroup xt_xmanipulation
      * @param e input xexpression
      * @param axis axis to expand
      * @return returns a ``strided_view`` with expanded dimension
@@ -525,6 +649,7 @@ namespace xt
      * Note: dimensions are added equally at the beginning and the end.
      * For example, a 1-D array of shape (N,) becomes a view of shape (1, N, 1).
      *
+     * @ingroup xt_xmanipulation
      * @param e input xexpression
      * @tparam N the number of requested dimensions
      * @return ``strided_view`` with expanded dimensions
@@ -552,6 +677,8 @@ namespace xt
 
     /**
      * Expand to at least 1D
+     *
+     * @ingroup xt_xmanipulation
      * @sa atleast_Nd
      */
     template <class E>
@@ -562,6 +689,8 @@ namespace xt
 
     /**
      * Expand to at least 2D
+     *
+     * @ingroup xt_xmanipulation
      * @sa atleast_Nd
      */
     template <class E>
@@ -572,6 +701,8 @@ namespace xt
 
     /**
      * Expand to at least 3D
+     *
+     * @ingroup xt_xmanipulation
      * @sa atleast_Nd
      */
     template <class E>
@@ -585,13 +716,14 @@ namespace xt
      ************************/
 
     /**
-     * @brief Split xexpression along axis into subexpressions
+     * Split xexpression along axis into subexpressions
      *
      * This splits an xexpression along the axis in `n` equal parts and
      * returns a vector of ``strided_view``.
      * Calling split with axis > dimension of e or a `n` that does not result in
      * an equal division of the xexpression will throw a runtime_error.
      *
+     * @ingroup xt_xmanipulation
      * @param e input xexpression
      * @param n number of elements to return
      * @param axis axis along which to split the expression
@@ -624,10 +756,11 @@ namespace xt
     }
 
     /**
-     * @brief Split an xexpression into subexpressions horizontally (column-wise)
+     * Split an xexpression into subexpressions horizontally (column-wise)
      *
      * This method is equivalent to ``split(e, n, 1)``.
      *
+     * @ingroup xt_xmanipulation
      * @param e input xexpression
      * @param n number of elements to return
      */
@@ -638,10 +771,11 @@ namespace xt
     }
 
     /**
-     * @brief Split an xexpression into subexpressions vertically (row-wise)
+     * Split an xexpression into subexpressions vertically (row-wise)
      *
      * This method is equivalent to ``split(e, n, 0)``.
      *
+     * @ingroup xt_xmanipulation
      * @param e input xexpression
      * @param n number of elements to return
      */
@@ -656,10 +790,10 @@ namespace xt
      ***********************/
 
     /**
-     * @brief Reverse the order of elements in an xexpression along every axis.
+     * Reverse the order of elements in an xexpression along every axis.
      *
+     * @ingroup xt_xmanipulation
      * @param e the input xexpression
-     *
      * @return returns a view with the result of the flip.
      */
     template <class E>
@@ -675,13 +809,14 @@ namespace xt
     }
 
     /**
-     * @brief Reverse the order of elements in an xexpression along the given axis.
+     * Reverse the order of elements in an xexpression along the given axis.
+     *
      * Note: A NumPy/Matlab style `flipud(arr)` is equivalent to `xt::flip(arr, 0)`,
      * `fliplr(arr)` to `xt::flip(arr, 1)`.
      *
+     * @ingroup xt_xmanipulation
      * @param e the input xexpression
      * @param axis the axis along which elements should be reversed
-     *
      * @return returns a view with the result of the flip
      */
     template <class E>
@@ -711,69 +846,73 @@ namespace xt
      * rot90 implementation *
      ************************/
 
-    template <std::ptrdiff_t N>
-    struct rot90_impl;
-
-    template <>
-    struct rot90_impl<0>
+    namespace detail
     {
-        template <class E>
-        inline auto operator()(E&& e, const std::array<std::size_t, 2>& /*axes*/)
+        template <std::ptrdiff_t N>
+        struct rot90_impl;
+
+        template <>
+        struct rot90_impl<0>
         {
-            return std::forward<E>(e);
-        }
-    };
+            template <class E>
+            inline auto operator()(E&& e, const std::array<std::size_t, 2>& /*axes*/)
+            {
+                return std::forward<E>(e);
+            }
+        };
 
-    template <>
-    struct rot90_impl<1>
-    {
-        template <class E>
-        inline auto operator()(E&& e, const std::array<std::size_t, 2>& axes)
+        template <>
+        struct rot90_impl<1>
         {
-            using std::swap;
+            template <class E>
+            inline auto operator()(E&& e, const std::array<std::size_t, 2>& axes)
+            {
+                using std::swap;
 
-            dynamic_shape<std::ptrdiff_t> axes_list(e.shape().size());
-            std::iota(axes_list.begin(), axes_list.end(), 0);
-            swap(axes_list[axes[0]], axes_list[axes[1]]);
+                dynamic_shape<std::ptrdiff_t> axes_list(e.shape().size());
+                std::iota(axes_list.begin(), axes_list.end(), 0);
+                swap(axes_list[axes[0]], axes_list[axes[1]]);
 
-            return transpose(flip(std::forward<E>(e), axes[1]), std::move(axes_list));
-        }
-    };
+                return transpose(flip(std::forward<E>(e), axes[1]), std::move(axes_list));
+            }
+        };
 
-    template <>
-    struct rot90_impl<2>
-    {
-        template <class E>
-        inline auto operator()(E&& e, const std::array<std::size_t, 2>& axes)
+        template <>
+        struct rot90_impl<2>
         {
-            return flip(flip(std::forward<E>(e), axes[0]), axes[1]);
-        }
-    };
+            template <class E>
+            inline auto operator()(E&& e, const std::array<std::size_t, 2>& axes)
+            {
+                return flip(flip(std::forward<E>(e), axes[0]), axes[1]);
+            }
+        };
 
-    template <>
-    struct rot90_impl<3>
-    {
-        template <class E>
-        inline auto operator()(E&& e, const std::array<std::size_t, 2>& axes)
+        template <>
+        struct rot90_impl<3>
         {
-            using std::swap;
+            template <class E>
+            inline auto operator()(E&& e, const std::array<std::size_t, 2>& axes)
+            {
+                using std::swap;
 
-            dynamic_shape<std::ptrdiff_t> axes_list(e.shape().size());
-            std::iota(axes_list.begin(), axes_list.end(), 0);
-            swap(axes_list[axes[0]], axes_list[axes[1]]);
+                dynamic_shape<std::ptrdiff_t> axes_list(e.shape().size());
+                std::iota(axes_list.begin(), axes_list.end(), 0);
+                swap(axes_list[axes[0]], axes_list[axes[1]]);
 
-            return flip(transpose(std::forward<E>(e), std::move(axes_list)), axes[1]);
-        }
-    };
+                return flip(transpose(std::forward<E>(e), std::move(axes_list)), axes[1]);
+            }
+        };
+    }
 
     /**
-     * @brief Rotate an array by 90 degrees in the plane specified by axes.
+     * Rotate an array by 90 degrees in the plane specified by axes.
+     *
      * Rotation direction is from the first towards the second axis.
      *
+     * @ingroup xt_xmanipulation
      * @param e the input xexpression
      * @param axes the array is rotated in the plane defined by the axes. Axes must be different.
      * @tparam N number of times the array is rotated by 90 degrees. Default is 1.
-     *
      * @return returns a view with the result of the rotation
      */
     template <std::ptrdiff_t N, class E>
@@ -789,7 +928,7 @@ namespace xt
         auto norm_axes = forward_normalize<std::array<std::size_t, 2>>(e, axes);
         constexpr std::ptrdiff_t n = (4 + (N % 4)) % 4;
 
-        return rot90_impl<n>()(std::forward<E>(e), norm_axes);
+        return detail::rot90_impl<n>()(std::forward<E>(e), norm_axes);
     }
 
     /***********************
@@ -797,15 +936,16 @@ namespace xt
      ***********************/
 
     /**
-     * @brief Roll an expression.
+     * Roll an expression.
+     *
      * The expression is flatten before shifting, after which the original
      * shape is restore. Elements that roll beyond the last position are
      * re-introduced at the first. This function does not change the input
      * expression.
      *
+     * @ingroup xt_xmanipulation
      * @param e the input xexpression
      * @param shift the number of places by which elements are shifted
-     *
      * @return a roll of the input expression
      */
     template <class E>
@@ -894,14 +1034,15 @@ namespace xt
     }
 
     /**
-     * @brief Roll an expression along a given axis.
+     * Roll an expression along a given axis.
+     *
      * Elements that roll beyond the last position are re-introduced at the first.
      * This function does not change the input expression.
      *
+     * @ingroup xt_xmanipulation
      * @param e the input xexpression
      * @param shift the number of places by which elements are shifted
      * @param axis the axis along which elements are shifted.
-     *
      * @return a roll of the input expression
      */
     template <class E>
@@ -933,6 +1074,7 @@ namespace xt
     /****************************
      * repeat implementation    *
      ****************************/
+
     namespace detail
     {
         template <class E, class R>
@@ -948,13 +1090,13 @@ namespace xt
     }
 
     /**
-     * @brief Repeats elements of an expression along a given axis.
+     * Repeat elements of an expression along a given axis.
      *
+     * @ingroup xt_xmanipulation
      * @param e the input xexpression
-     * @param repeats The number of repetition of each elements. \ref repeats is broadcasted to
-     * fit the shape of the given \ref axis.
+     * @param repeats The number of repetition of each elements.
+     *     @p repeats is broadcasted to fit the shape of the given @p axis.
      * @param axis the axis along which to repeat the value
-     *
      * @return an expression which as the same shape as \ref e, except along the given \ref axis
      */
     template <class E>
@@ -967,11 +1109,12 @@ namespace xt
     }
 
     /**
-     * @brief Repeats elements of an expression along a given axis.
+     * Repeat elements of an expression along a given axis.
      *
+     * @ingroup xt_xmanipulation
      * @param e the input xexpression
-     * @param repeats The number of repetition of each elements. The size of \ref repeats
-     * must match the shape of the given \ref axis.
+     * @param repeats The number of repetition of each elements.
+     *     The size of @p repeats must match the shape of the given @p axis.
      * @param axis the axis along which to repeat the value
      *
      * @return an expression which as the same shape as \ref e, except along the given \ref axis
@@ -983,13 +1126,13 @@ namespace xt
     }
 
     /**
-     * @brief Repeats elements of an expression along a given axis.
+     * Repeat elements of an expression along a given axis.
      *
+     * @ingroup xt_xmanipulation
      * @param e the input xexpression
-     * @param repeats The number of repetition of each elements. The size of \ref repeats
-     * must match the shape of the given \ref axis.
+     * @param repeats The number of repetition of each elements.
+     *     The size of @p repeats must match the shape of the given @p axis.
      * @param axis the axis along which to repeat the value
-     *
      * @return an expression which as the same shape as \ref e, except along the given \ref axis
      */
     template <class E>


Test patch:
diff --git a/test/test_xmanipulation.cpp b/test/test_xmanipulation.cpp
index 6cd6eab91..fba13870e 100644
--- a/test/test_xmanipulation.cpp
+++ b/test/test_xmanipulation.cpp
@@ -80,6 +80,39 @@ namespace xt
         EXPECT_EQ(fun2(1, 2), tr2(2, 1));
     }
 
+    TEST(xmanipulation, swapaxes)
+    {
+        auto const a = xarray<double>::from_shape({1, 2, 3, 4, 5});
+        using shape_type = decltype(a)::shape_type;
+
+        EXPECT_EQ(swapaxes(a, 0, 1).shape(), (shape_type{2, 1, 3, 4, 5}));
+        EXPECT_EQ(swapaxes(a, 1, 3).shape(), (shape_type{1, 4, 3, 2, 5}));
+        EXPECT_EQ(swapaxes(a, 1, -1).shape(), (shape_type{1, 5, 3, 4, 2}));
+        // Symetry
+        EXPECT_EQ(swapaxes(a, 0, 1).shape(), swapaxes(a, 1, 0).shape());
+        EXPECT_EQ(swapaxes(a, 1, 4).shape(), swapaxes(a, 4, 1).shape());
+        EXPECT_EQ(swapaxes(a, -1, 0).shape(), swapaxes(a, 0, -1).shape());
+        // No Op
+        EXPECT_EQ(swapaxes(a, 0, 0).shape(), a.shape());
+        EXPECT_EQ(swapaxes(a, -1, -1).shape(), a.shape());
+    }
+
+    TEST(xmanipulation, moveaxis)
+    {
+        auto const a = xarray<double>::from_shape({1, 2, 3, 4, 5});
+        using shape_type = decltype(a)::shape_type;
+
+        EXPECT_EQ(moveaxis(a, 0, 1).shape(), (shape_type{2, 1, 3, 4, 5}));
+        EXPECT_EQ(moveaxis(a, 1, 0).shape(), (shape_type{2, 1, 3, 4, 5}));
+        EXPECT_EQ(moveaxis(a, 1, 3).shape(), (shape_type{1, 3, 4, 2, 5}));
+        EXPECT_EQ(moveaxis(a, 3, 1).shape(), (shape_type{1, 4, 2, 3, 5}));
+        EXPECT_EQ(moveaxis(a, 1, -1).shape(), (shape_type{1, 3, 4, 5, 2}));
+        EXPECT_EQ(moveaxis(a, -1, 1).shape(), (shape_type{1, 5, 2, 3, 4}));
+        // No Op
+        EXPECT_EQ(moveaxis(a, 0, 0).shape(), a.shape());
+        EXPECT_EQ(moveaxis(a, -1, -1).shape(), a.shape());
+    }
+
     TEST(xmanipulation, ravel)
     {
         xarray<int, layout_type::row_major> a = {{0, 1, 2}, {3, 4, 5}};