An expression such as x + y * sin(z)
does not hold the result. Values are only computed upon access or when the expression is assigned to a container. This
allows to operate symbolically on very large arrays and only compute the result for the indices of interest:
// Assume x and y are xarrays each containing 1 000 000 objects
auto f = cos(x) + sin(y);
double first_res = f(1200);
double second_res = f(2500);
// Only two values have been computed
That means if you use the same expression in two assign statements, the computation of the expression will be done twice. It might be convenient to store the result of the expression in a temporary variable:
xt::xarray<double> tmp = cos(x) + sin(y);
xt::xarray<double> res1 = tmp + 2 * x;
xt::xarray<double> res2 = tmp - 2 * x;
If you have to force the evaluation of an xexpression then you can use xt::eval
on an xexpression.
Evaluating will either return a rvalue to a newly allocated container in the case of an xexpression, or a reference to a container in case you are evaluating a xarray
or xtensor
. Note that, in order to avoid copies, you should use a universal reference on the lefthand side (auto&&
). For example:
xt::xarray<double> a = {1, 2, 3};
xt::xarray<double> b = {3, 2, 1};
auto calc = a + b; // unevaluated xexpression!
auto&& e = xt::eval(calc); // a rvalue container xarray!
auto&& a_ref = xt::eval(a); // a reference to the existing container
In an operation involving two arrays of different dimensions, the array
with the lesser dimensions is broadcast across the leading dimensions of
the other. For example, if A
has shape (2, 3)
, and B
has shape
(4, 2, 3)
, the result of a broadcast operation with A
and B
has
shape (4, 2, 3)
.
(2, 3) # A
(4, 2, 3) # B
---------
(4, 2, 3) # Result
The same rule holds for scalars, which are handled as 0-D expressions. If A is a scalar, the equation becomes:
() # A
(4, 2, 3) # B
---------
(4, 2, 3) # Result
If matched up dimensions of two input arrays are different, and one of
them has size 1
, it is broadcast to match the size of the other. Let's
say B has the shape (4, 2, 1)
in the previous example, so the
broadcasting happens as follows:
(2, 3) # A
(4, 2, 1) # B
---------
(4, 2, 3) # Result
xtensor
provides a layout_type
enum that helps to specify the layout
used by multi-dimensional arrays:
layout_type::row_major
,layout_type::column_major
: fixes the strided index scheme.layout_type::dynamic
:resize
and constructor overloads allow specifying a set of strides.
Examples:
std::vector<size_t> shape = { 3, 2, 4 };
std::vector<size_t> strides = { 8, 4, 1 };
xt::xarray<double, xt::layout_type::dynamic> a(shape, strides);
xt::xarray<double, xt::layout_type::dynamic> b(shape, xt::layout_type::row_major);
xt::xarray<double, xt::layout_type::row_major> c(shape);
// same:
xt::xarray<double> c(shape);
Runtime vs Compile-time dimensionality:
xarray
can be reshaped dynamically to any number of dimensions. It is the container that is the most similar to numpy arrays.xtensor
has a dimension set at compilation time, which enables many optimizations. For example, shapes and strides ofxtensor
instances are allocated on the stack instead of the heap.xtensor_fixed
has a shape fixed at compile time. This allows even more optimizations, such as allocating the storage for the container on the stack, as well as computing strides and backstrides at compile time, making the allocation of this container extremely cheap.
Example:
#include "xtensor/xarray.hpp"
#include "xtensor/xtensor.hpp"
#include "xtensor/xfixed.hpp"
xt::xarray<double> a({3, 2, 4});
xt::xtensor<double, 3> a({3, 2, 4});
xt::xtensor_fixed<double, xt::xshape<3, 2, 4>> a();
xarray
, xtensor
and xtensor_fixed
containers are all xexpression
.
Additional interface they provide:
- Each method exposed in
xexpression
interface has its non-const counterpart exposed byxarray
,xtensor
andxtensor_fixed
. reshape()
: the new shape has to have same number of elements as the original container, keeps old elements.resize()
: resizes the container, doesn't preserve the container elements.strides()
: returns the strides of the container.
// Tip: The shape argument can have one of its value equal to -1,
// in this case the value is inferred from the number of elements
// in the container and the remaining values in the shape
xt::xarray<int> a = { 1, 2, 3, 4, 5, 6, 7, 8 };
a.reshape({-1, 4});
//a.shape() is {2, 4}
In the following example, temporary variable will be created to store a + b + c
before assigning to b, because b can be required to resize:
b = a + c + b;
If the left-hand side is not involved in the expression being assigned, to prevent the usage of temporary variable use xt::noalias()
:
xt::noalias(b) = a + c;
// Even if b has to be resized, a+c will be assigned directly to it
In xtensor scalars are assumed to be 0-D expressions, so a = 1.5
will lead to resizing a
to 0 dimensions and assigning 1.5
.
The correct way to assign scalar is using .fill()
method:
a = 1.5; // 0-D xarray
a.fill(1.5); // N-D xarray, all values equal 1.5
For the reasoning behind such approach read documentation.
+, -, *, /, %, !, ||, &&, <, <=, >, >=
are element-wise operators and apply the lazy broadcasting rules.==, !=
return singletrue
orfalse
.
From namespace xt
:
- math:
pow
,sin
,ceil
... - casting:
cast<>
, performsstatic_cast<>
; - reducers:
sum
,reduce
... - accumulators:
cumsum
,accumulate
...
Slices can be specified in the following ways:
- selection in a dimension by specifying an index (unsigned integer)
range(min, max)
, a slice representing the interval [min, max)range(min, max, step)
, a slice representing the stepped interval [min, max)all()
, a slice representing all the elements of a dimensionnewaxis()
, a slice representing an additional dimension of length onekeep(i0, i1, i2, ...)
a slice selecting non-contiguous indices to keep on the underlying expressiondrop(i0, i1, i2, ...)
a slice selecting non-contiguous indices to drop on the underlying expression
xt::view(a, xt::range(1, 3), xt::all(), xt::range(1, 3));
xt::view(a, 1, xt::all(), xt::range(0, 4, 2));
xt::view(a, xt::all(), xt::all(), xt::newaxis(), xt::all());
xt::view(a, xt::drop(0), xt::all(), xt::keep(0, 3));
The range function supports the placeholder _
syntax:
using namespace xt::placeholders; // required for `_` to work
xt::view(a, xt::range(_, 2), xt::all(), xt::range(1, _));
From namespace xt
:
strided_view
, slice vector built at run-time (base for views listed below)transpose
, lazy trasposed viewravel<layout_type>, flatten
, one-dimensional viewsreshape_view
, view as if new shape givendynamic_view
, same as, but slower thanstrided_view
, with support ofkeep
anddrop
functionsindex_view
, view of elements with specified indicesfilter
, view of elements that verify given conditionfiltration
, optimizedfilter
for scalar assignmentsmasked_view
, apply mask onxexpression
broadcast
, to specified shapereal
,imag
, views on real and imaginary parts of complex expression
xt::strided_view(a, { xt::range(0, 1), xt::newaxis(), 1, xt::all() });
xt::transpose(a);
xt::ravel<layout_type::column_major>(a);
xt::flatten(a);
xt::reshape_view(a, { 4, 2, 3 });
xt::dynamic_view(a, { xt::range(0, 1), 1, xt::all(), xt::keep(0, 2, 3), xt::drop(1, 2, 4) });
xt::index_view(a, {{0,0}, {1, 0}, {0, 1}});
xt::filter(a, a >= 5);
xt::filtration(a, a >= 5) += 100;
xt::masked_view(a, {{true, false, false}, {false, true, false}});
xt::broadcast(a, { 3, 2, 3 });
xt::real(e) = xt::zeros<double>({2, 2});
Since views cannot be resized, when assigning an expression to a view, broadcasting rules are applied:
view(a, all(), all()) = 4.5;
// => a = {{4.5, 4.5, 4.5}, {4.5, 4.5, 4.5}}
The expressions returned by these functions implement the
laziness of xtensor
, that is, they don’t hold any value. Values are
computed upon request.
zeros
, ones
, eye
arange
, linspace
, logspace
concatenate
, stack
rand
,randint
,randn
meshgrid