Skip to content

Expressions

In KFR, all data is represented through Expression concept. Expression is a virtual multidimensional array consisting of elements that can be read (Input expression) or written to (Output expression). Expression can have specific size or have infinite size in any dimension. Its size may be known at compile time or at runtime only.

The number of dimensions must be known at compile time.

Normally, expressions do not own any data and can be seen as data generators with any algorithm under the hood. But classes owning data (tensor and univector) provide expression interface as well. Since KFR5 you can make expression from any user defined or std type.

Expressions can refer to other expressions as its arguments. prvalue expressions are captured by value and moved to expression storage. lvalue expressions are captured by reference. The latter may cause dangling references if the resulting expression is used outside of its arguments scope. As always, std::move forces variable to be captured by value.

The following function creates Expression that represents a virtual 3-dimensional array with elements starting from 0 at \((0,0,0)\) index and incremented by \(1\), \(10\) and \(100\) along each axis.

counter(0, 1, 10, 100)

To get a single element from Expression, call get_element function. It takes the expression itself and the index.

get_element(counter(0, 1, 10, 100), {1, 2, 3}) == 321

Calling functions or operators on expressions or arrays of data is performed lazily in KFR. This allows better optimization and does not require saving temporary data.

Internally a C++ technique called Expression templates is used but expressions processing is explicitly vectorized in KFR. You can control some aspects of vectorization.

Functions and operators

For example, subtracting one univector from another produces expression, not univector:

univector<int, 5> x{1, 2, 3, 4, 5};
univector<int, 5> y{0, 0, 1, 10, -5};

auto z = x - y; // z is of type expression_function<...>, not univector. 
                // This only constructs an expression and does not perform any calculation

You should assign expression to a univector (or tensor) to get the data:

univector<int, 5> x{1, 2, 3, 4, 5};
univector<int, 5> y{0, 0, 1, 10, -5};    

univector<int, 5> z = x - y;

Note

When an expression is assigned to a univector variable, expression is evaluated in process function and values are being written to the target storage.

Same applies to calling KFR functions on univectors, this doesn't calculate values immediately. Instead, new expression will be created.

univector<float, 5> x{1, 2, 3, 4, 5};
sqrt(x);                                // only constructs an expression
univector<float, 5> values = sqrt(x);   // constructs an expression and writes data to univector

Element type of an input expressions can be determined by using expression_value_type<Expr> (In KFR4 it was value_type_of<Expr>). Since KFR5 all expressions have their type specified.

Writing custom expressions

To turn a user-defined class into expression, you should define kfr::expression_traits<Class> and provide typedefs and function to work with it.

Input expressions must implement get_elements function to retrieve elements at given index.

Output expressions must implement set_elements function to set its elements.

Example of input expression:

#include <kfr/all.hpp>

namespace kfr
{

template <typename T, index_t Size>
struct identity_matrix
{
};

template <typename T, index_t Size>
struct expression_traits<identity_matrix<T, Size>> : expression_traits_defaults
{
    // element type for expression
    using value_type             = T;

    // number of dimensions, must be known at compile time
    constexpr static size_t dims = 2;

    // function to retrieve shape (size) of matrix, runtime version
    constexpr static shape<2> get_shape(const identity_matrix<T, Size>& self) { return { Size, Size }; }

    // function to retrieve shape (size) of matrix, compile time version
    // if the size is unknown at compile time the function must be still defined
    // but return undefined_size for every axis with unknown size
    constexpr static shape<2> get_shape() { return { Size, Size }; }
};

template <typename T, index_t Size, index_t Axis, size_t N>
vec<T, N> get_elements(const identity_matrix<T, Size>& self, const shape<2>& index,
                       const axis_params<Axis, N>& sh)
{
    // identity matrix expression returns 1 if col==row, 0 otherwise
    // indices<A> returns indices for given axis taking vectorization into account
    // select is a SIMD-enabled ternary operator function
    return select(indices<0>(index, sh) == indices<1>(index, sh), 1, 0);
}

}

int main()
{
    using namespace kfr;
    // Create identity_matrix expression with 9 cols/rows,
    // Render it to tensor class (trender)
    // Convert to string with maximum 16 columns of values
    // And 0 dimensions collapsed (to_string).
    println(trender(identity_matrix<float, 9>{}).to_string(16, 0));
}

Output:

{{1, 0, 0, 0, 0, 0, 0, 0, 0},
 {0, 1, 0, 0, 0, 0, 0, 0, 0},
 {0, 0, 1, 0, 0, 0, 0, 0, 0},
 {0, 0, 0, 1, 0, 0, 0, 0, 0},
 {0, 0, 0, 0, 1, 0, 0, 0, 0},
 {0, 0, 0, 0, 0, 1, 0, 0, 0},
 {0, 0, 0, 0, 0, 0, 1, 0, 0},
 {0, 0, 0, 0, 0, 0, 0, 1, 0},
 {0, 0, 0, 0, 0, 0, 0, 0, 1}}

Expression may be defined in a compact form using only a single class:

template <typename T, index_t Size>
struct identity_matrix : expression_traits_defaults
{
    using value_type             = T;

    constexpr static size_t dims = 2;
    constexpr static shape<2> get_shape(const identity_matrix& self) { return { Size, Size }; }
    constexpr static shape<2> get_shape() { return { Size, Size }; }

    template <index_t Axis, size_t N>
    friend vec<T, N> get_elements(const identity_matrix& self, const shape<2>& index,
                                  const axis_params<Axis, N>& sh)
    {
        return select(indices<0>(index, sh) == indices<1>(index, sh), 1, 0);
    }
};

The same class with the size defined at runtime.

template <typename T>
struct identity_matrix : expression_traits_defaults
{
    using value_type             = T;

    constexpr static size_t dims = 2;
    constexpr static shape<2> get_shape(const identity_matrix& self) { return { self.size, self.size }; }
    // undefined_size means size is not known at compile time
    constexpr static shape<2> get_shape() { return { undefined_size, undefined_size }; }

    template <index_t Axis, size_t N>
    friend vec<T, N> get_elements(const identity_matrix& self, const shape<2>& index,
                                  const axis_params<Axis, N>& sh)
    {
        return select(indices<0>(index, sh) == indices<1>(index, sh), 1, 0);
    }

    index_t size;
};

Reducing functions

Reducing functions accept 1D Expression and produce scalar.

Some of the reducing functions are: sum, rms, mean, dotproduct, product, sumsqr.

Some of reducing functions have the same names as corresponding regular functions but with of suffix to distinguish them: minof, maxof, absmaxof, absminof