/cpp-generic-tensor-template-library

Generic Tensor Template Library

Primary LanguageC++MIT LicenseMIT

Generic Tensor Template Library

Target

  • highly flexible and math-orientated tensor (representation) library allowing:
    • arbitrary scalar types
    • arbitrary scalar operations (type traits)
    • arbitrary ranks for tensors
    • arbitrary dimensions per rank
  • with the goal to be as efficiency as possible under these constraints

Features

  • scalar type traits
  • customizable tensor shapes
  • scalar operations +, -, *, /
  • tensor operations +, -, +=, -=, *, *= (scalar multiplication), outer_product, contract
  • element and subtensor access
  • (in-place) elementwise operations of arbitrary arity
  • static memory by default: tensors are represented as (fixed-size) arrays of scalars
  • strongly typed: compile-time checks for contractions etc.

Non-Features

These things are not implemented in this library:

  • reshaping tensors
  • change of index order (e.g. transposition)
  • dot, cross, length, trace or any other additional structure beyond the bare minimum
  • solving linear equations

note: In the furture some of these might be provided as an extension. Forcing any specific interpretation of the tensors may contradict some of the use cases and is not desired.

Extenions

The following items are supported via extensions of this library:

  • solving differential equations via 'boost::numeric::odeint`

Disclaimer

As of now, the internal representation of tensors relies on anonymous unions. In terms of the current C+ standard (C++20) their usage is undefined behaviour (UB). For all tested combinations of hardware (X86-64 and ARM64) and compilers (gcc and clang up to and including -O3 and -Ofast) no UB was observed. To ensure correct behaviour on a specific platform it is strongly recommended to run the tests included here first and (if not already covered) custome tests involving the usage of tensors of desired shape and with the desired scalar type for all levels of optimization used. All common tensor operations must be verified first to guarentee correct results.

Usage

This is a header-only library. It is located inside the include folder. To import everything use

#include <gttl.hpp>

To import parts of the library use

#include <gttl/path-to-part.hpp>

For support of boost::numeric::odeint import

#include <gttl/extensions/boost/numeric/odeint.hpp>

Documentation

To generate the documentation in the folder doc run

make doc

(see requirements)

Tests

To generate and execute all tests run

make test

(see requirements)

note: Some tests have additional requirements like boost::numeric::odeint and may need to be disabled manually.

Basic Example

#include <iostream>
#include <gttl.hpp>

using namespace gttl::literals; // for _D

// custom scalar type
using Scalar = float; 
// custom tensor rank
constexpr std::size_t RANK{2};
// custom (non-homogenious) dimensions
constexpr gttl::Dimensions<RANK> DIMENSIONS{2_D, 3_D};
using Mat2x3 = gttl::Tensor<Scalar, RANK, DIMENSIONS>;

int main()
{
    using std::cout;

    const Mat2x3 mat1{1, 2, 3, 4, 5, 6};
    const Mat2x3 mat2{6, 5, 4, 3, 2, 1};

    cout << "basic vector operations:" << '\n';
    cout << "-mat1" << '\n';
    cout << (-mat1) << '\n';
    cout << "mat1 + mat2" << '\n';
    cout << (mat1 + mat2) << '\n';
    cout << "mat1 - mat2" << '\n';
    cout << (mat1 - mat2) << '\n';
    cout << "mat1 * 3" << '\n';
    cout << (mat1 * 3) << '\n';

    cout << '\n';
    cout << "element access:" << '\n';
    cout << "mat1.at(1)" << '\n';
    cout << (mat1.at(1)) << '\n';
    cout << "mat1.at(1, 2)" << '\n';
    cout << (mat1.at(1, 2)) << '\n';

    constexpr auto square = [](const Scalar& x) { return x * x; };
    cout << '\n';
    cout << "elementwise operations:" << '\n';
    cout << "mat1.elementwise(square)" << '\n';
    cout << (mat1.elementwise(square)) << '\n';
}

output:

basic vector operations:
-mat1
{{-1,-2,-3},{-4,-5,-6}}
mat1 + mat2
{{7,7,7},{7,7,7}}
mat1 - mat2
{{-5,-3,-1},{1,3,5}}
mat1 * 3
{{3,6,9},{12,15,18}}

element access:
mat1.at(1)
{4,5,6}
mat1.at(1, 2)
6

elementwise operations:
mat1.elementwise(square)
{{1,4,9},{16,25,36}}

'Extreme' Example

// clang-format off
#include <iostream>
#include <numeric>
#include <gttl.hpp>

using namespace gttl::literals; // for _D

using Scalar = float;
// unusual rank
constexpr std::size_t RANK{6};
// (non-homogenious) dimensions
constexpr gttl::Dimensions<RANK> DIMENSIONS{2_D, 3_D, 4_D, 5_D, 6_D, 7_D};
using TensorR6 = gttl::Tensor<Scalar, RANK, DIMENSIONS>;

int main()
{
    using std::cout;

    TensorR6 tensor;

    std::iota(std::begin(tensor), std::end(tensor), 0);

    cout << '\n';
    cout << "tensor.at(0, 1, 2, 3, 4)" << '\n';
    cout << (tensor.at(0, 1, 2, 3, 4)) << '\n';

}

output:

tensor.at(0, 1, 2, 3, 4)
{1414,1415,1416,1417,1418,1419,1420}

Outer Product / Tensor Product Example

#include <iostream>
#include <numeric>
#include <gttl.hpp>

using namespace gttl::literals; // for _D

// custom scalar type
using Scalar = float; 
// custom tensor ranks
constexpr std::size_t RANK1{1};
constexpr std::size_t RANK2{2};
// custom (non-homogenious) dimensions
constexpr gttl::Dimensions<RANK1> DIMENSIONS1{3_D};
constexpr gttl::Dimensions<RANK2> DIMENSIONS2{4_D, 2_D};
using Ten3 = gttl::Tensor<Scalar, RANK1, DIMENSIONS1>;
using Ten4x2 = gttl::Tensor<Scalar, RANK2, DIMENSIONS2>;

int main()
{
    using std::cout;

    Ten3 tensor1{};
    std::iota(std::begin(tensor1), std::end(tensor1), 1);

    Ten4x2 tensor2{};
    std::iota(std::begin(tensor2), std::end(tensor2), Ten3::size + 1);
    
    cout << "tensor1" << '\n';
    cout << (tensor1) << '\n';
    cout << "tensor2" << '\n';
    cout << (tensor2) << '\n';

    cout << '\n';
    cout << "outer_product(tensor1, tensor2)" << '\n';
    cout << (outer_product(tensor1, tensor2)) << '\n';
    cout << "outer_product(tensor2, tensor1)" << '\n';
    cout << (outer_product(tensor2, tensor1)) << '\n';
}

output:

tensor1
{1,2,3}
tensor2
{{4,5},{6,7},{8,9},{10,11}}

outer_product(tensor1, tensor2)
{{{4,5},{6,7},{8,9},{10,11}},{{8,10},{12,14},{16,18},{20,22}},{{12,15},{18,21},{24,27},{30,33}}}
outer_product(tensor2, tensor1)
{{{4,8,12},{5,10,15}},{{6,12,18},{7,14,21}},{{8,16,24},{9,18,27}},{{10,20,30},{11,22,33}}}

Contraction Example

#include <iostream>
#include <numeric>
#include <gttl.hpp>

using namespace gttl::literals; // for _D

// custom scalar type
using Scalar = float; 
// custom tensor rank
constexpr std::size_t RANK1{3};
constexpr std::size_t RANK2{2};
// custom (non-homogenious) dimensions
constexpr gttl::Dimensions<RANK1> DIMENSIONS1{3_D, 2_D, 3_D};
constexpr gttl::Dimensions<RANK2> DIMENSIONS2{3_D, 2_D};
using Ten3x2 = gttl::Tensor<Scalar, RANK1, DIMENSIONS1>;
using Ten3x2x4 = gttl::Tensor<Scalar, RANK2, DIMENSIONS2>;

int main()
{
    using std::cout;

    Ten3x2 tensor1{};
    std::iota(std::begin(tensor1), std::end(tensor1), 1);

    Ten3x2x4 tensor2{};
    std::iota(std::begin(tensor2), std::end(tensor2), Ten3x2::size + 1);
    
    cout << "tensors:" << '\n';
    cout << "tensor1" << '\n';
    cout << (tensor1) << '\n';
    cout << "tensor2" << '\n';
    cout << (tensor2) << '\n';

    cout << '\n' << '\n';
    cout << "contraction:" << '\n';
    cout << "contraction<0, 2>(tensor1)" << '\n';
    cout << (contraction<0, 2>(tensor1)) << '\n';
    cout << '\n';
    cout << "note: oder of indices is irrelevant" << '\n';
    cout << "contraction<2, 0>(tensor1)" << '\n';
    cout << (contraction<2, 0>(tensor1)) << '\n';
    // note: contraction<0, 1>, contraction<1, 0> etc. are illegal
    //       contractions and consequently refuse to compile

    cout << '\n' << '\n';
    cout << "contraction of outer product:" << '\n';
    cout << "contraction<0, 3>(tensor1, tensor2)" << '\n';
    cout << (contraction<0, 3>(tensor1, tensor2)) << '\n';
    cout << '\n';
    cout << "note: indices may belong to the same tensor" << '\n';
    cout << "contraction<0, 2>(tensor1, tensor2)" << '\n';
    cout << (contraction<0, 2>(tensor1, tensor2)) << '\n';
    // note: illegal contractions are refused again
}

output:

tensors:
tensor1
{{{1,2,3},{4,5,6}},{{7,8,9},{10,11,12}},{{13,14,15},{16,17,18}}}
tensor2
{{19,20},{21,22},{23,24}}


contraction:
contraction<0, 2>(tensor1)
{24,33}

note: oder of indices is irrelevant
contraction<2, 0>(tensor1)
{24,33}


contraction of outer product:
contraction<0, 3>(tensor1, tensor2)
{{{465,486},{528,552},{591,618}},{{654,684},{717,750},{780,816}}}

note: indices may belong to the same tensor
contraction<0, 2>(tensor1, tensor2)
{{{456,480},{504,528},{552,576}},{{627,660},{693,726},{759,792}}}

Namespaces

  • library: gttl
  • literal suffixes: gttl::literals

Requirements

  • C++20 (for using)
  • C++23 (for testing)
  • make 4.3+ (for documentation and testing)
  • doxygen 1.9+ (for documentation)
  • Boost 1.80+ (for testing)