Add DenseTensor container based on mdarray and a DenseTensorSpan based of mdspan
Summary
This MR brings the dune interface to mdarray and mdspan, by providing a mixin base class DenseTensorMixin (similar to DenseMatrix and DenseVector) and a derived class DenseTensor. Additionally, the MR provides a DenseTensorSpan, i.e. an mdspan-like class with DenseTensorMixin interface.
Motivation
The Std::mdarray and Std::mdspan classes are based on the corresponding std c++ library proposals and implementations. Those are pure container-adapters and container-views. They can be used like they are, but do not provide the usual Dune interfaces as found in DynamicMatrix and FieldMatrix, for example. This MR provides these datastructures in the regular Dune:: namespace, to be used by Dune algorithms.
Limitations and Design Decisions
The DenseTensorMixin is not (yet) a replacement for DenseMatrix and DenseVector, but goes in this direction. Currently the interface provided by DenseTensorMixin is very minimal. It essentially just provides for rank-2 tensors the rows() and cols() member functions and the exists(i0,i1...) method. Also it provides several constructors, e.g., from single values or initializer lists. But, there are no arithmetic operators implemented yet, see !1447 for a follow-up MR on this topic.
A rank-0 tensor behaves like a scalar, i.e., it can be compared against a scalar, and it can be converted implicitly into a scalar. The other dune-ish scalar-specialization is not implemented (i.e. tensors with all extents 1 are not considered a scalar).
DenseTensor
The DenseTensor is a multi-dimensional container with dynamic or static extents:
template <class Value, std::size_t... extents>
class DenseTensor;
The extents are given either as template parameter (in case of static extents) or the value Dune::dynamic (a special value denoting a dynamic extent). In the constructor any non-static extent must be provided (or all extents) - in form of a list of integers, or a shape container, e.g., a std::array - plus an optional default value for the entries or an initializer list:
DenseTensor<double,2,2> matrix1(42.0); // with initial value 42.0 for all entries
DenseTensor<double,Dune::dynamic,Dune::dynamic> matrix2(2,2, 42.0);
DenseTensor<double,2,2> matrix3{{1.0, 2.0}, {3.0,4.0}}; // with initializer lists
DenseTensor<double,std::dynamic_extend,std::dynamic_extend> matrix4(std::array{2,2}, {{1.0, 2.0}, {3.0,4.0}});
DenseTensorSpan
The implementation of DenseTensorSpan is very minimal. It is essentially just a Std::mdspan equipped with all operations provided by the mixin class. The idea is that it can be used to wrap other data to give it the Dune interface, like std::vector or std::array, similar to !1354 . What you need to provide is a pointer to the contiguous memory and a shape descriptor, i.e., the tensor extents. Those can be again either static or dynamic or a mixture of both.
std::vector<double> values(2*2, 42.0);
DenseTensorSpan matrix_wrapper1(values.data(), std::array{2,2}); // dynamic extents
DenseTensorSpan matrix_wrapper2(values.data(), Std::extents<std::size_t,2,2>{}); // static extents
DenseTensorSpan<const double, Std::extents<std::size_t,2,2>> const_matrix_wrapper(values.data());
Similar to Std::mdspan, the DenseTensorSpan allows to specify a layout and an accessor. These are more advanced features but sometimes necessary, especially in combination with subspans. In !1404 the possibilities to create sub-tensors are explored. This is not yet included in this MR, but might be an extension later.