Examples¶
Each example is a complete program from the examples/ directory. They are built and run with the tests, so the code below is code that compiles.
01. First steps
Tensors with shapes known at compile time or at run time, element-wise expressions that compute nothing until they are assigned, and a result made in one pass.
// SPDX-License-Identifier: Apache-2.0
// First steps: tensors with shapes known at compile time or at run time, element-wise expressions
// that compute nothing until assigned, and a result made in one pass.
import strata;
import std;
namespace {
using strata::Dyn;
using strata::Matrix;
using strata::Shape;
using strata::Tensor;
template <typename L, typename R>
concept addable = requires(const L& lhs, const R& rhs) { lhs + rhs; };
} // namespace
auto main() -> int {
// A static shape, 2 x 3, is part of the type: a small tensor keeps its elements inside itself.
// Nested lists are written as the matrix is read, row by row.
const Matrix<float, 2, 3> a{{0, 1, 2}, {10, 11, 12}};
// A dynamic shape is given at run time; the elements are on the heap.
const Matrix<float, Dyn, Dyn> b{Shape<Dyn, Dyn>{2, 3}, 0.5F};
// An expression only records what to compute, over views of `a` and `b`...
const auto twice_a_plus_b = (a * 2.0F) + b;
// ... until a tensor is made of it, in one pass over memory. The type follows the expression:
// float values, and the static shape both operands agree on.
Matrix c = twice_a_plus_b;
static_assert(std::same_as<decltype(c), Tensor<float, Shape<2, 3>>>);
// Tensors, views and expressions print with std::format, as NumPy prints arrays: over lines,
// columns aligned; the spec after a second colon is the elements', as for a range.
std::println("a:\n{}", a);
std::println("b:\n{::.1f}", b);
std::println("c = 2a + b:\n{::.1f}", c);
// An expression prints too, read where it is printed: on one line with `l`, with its kind,
// element type and shape first with `?`.
std::println("{:?l}", twice_a_plus_b);
// Assigning to a tensor of the same extents writes in place, no allocation. Every operator of the
// elements is there: `c -= b` is `c = c - b`.
c -= b;
std::println("c -= b:\n{::.1f}", c);
// Shapes are checked: static extents that differ do not compile, dynamic ones are checked when
// the expression is made.
static_assert(!addable<Tensor<float, Shape<2, 3>>, Tensor<float, Shape<3, 2>>>);
static_assert(addable<Tensor<float, Shape<2, 3>>, Tensor<float, Shape<Dyn, Dyn>>>);
}
02. Generators
Tensors with values but no memory: a constant, the identity, a run of numbers, any function of the index. They mix into expressions like views, and become tensors when assigned.
// SPDX-License-Identifier: Apache-2.0
// Generators: tensors with values but no memory. A constant, the identity, a run of numbers, any
// function of the index. They mix into expressions like views, are sliced like views, and become
// tensors when assigned; nothing is stored until then.
import strata;
import std;
namespace {
using strata::block;
using strata::Dyn;
using strata::permute;
using strata::seq;
using strata::Shape;
using strata::slice;
using strata::Tensor;
// A generator prints like any tensor, its values computed as they are printed.
auto print(std::string_view title, const auto& matrix) -> void { std::println("{}:\n{::.2f}", title, matrix); }
auto print_vector(std::string_view title, const auto& vector) -> void {
std::println("{:>18}: {:l:.2f}", title, vector);
}
} // namespace
auto main() -> int {
// Constants: the same value everywhere, of any shape.
print("zeros<float>(2 x 3)", strata::zeros<float>(Shape<2, 3>{}));
print("full(2 x 3, 7.5)", strata::full(Shape<Dyn, Dyn>{2, 3}, 7.5));
// Runs of numbers: iota by a step, linspace between two ends, both ends included.
print_vector("iota<float>(5)", strata::iota<float>(5));
print_vector("iota(5, 1, 0.25)", strata::iota<double>(5, 1.0, 0.25));
print_vector("linspace(0, 1, 5)", strata::linspace(0.0, 1.0, 5));
// The identity, in any rank, and a function of the index for anything else.
print("eye<float>(4 x 4)", strata::eye<float>(Shape<4, 4>{}));
const auto distance = strata::generate(Shape<Dyn, Dyn>{4, 5}, [](std::size_t i, std::size_t j) {
const auto di = static_cast<double>(static_cast<std::ptrdiff_t>(i));
const auto dj = static_cast<double>(static_cast<std::ptrdiff_t>(j));
return std::hypot(di, dj);
});
print("distance from (0, 0)", distance);
// Slicing a generator gives a generator: the function is fed other indices, nothing is computed
// or stored. A row of the identity, and a block of it off the diagonal: a `seq<3>{1}` is three
// indices from 1, a `seq{1, 4}` the indices 1 to 3 when the size is known only at run time.
const auto eye4 = strata::eye<float>(Shape<4, 4>{});
print_vector("eye(4 x 4) row 2", eye4 | slice<0>(2));
print("eye(4 x 4) 3 x 3 from (0, 1)", eye4 | block(seq<3>{0}, seq<3>{1}));
print("distance transposed, cut", distance | permute<1, 0> | block(seq{1, 4}, seq{0, 3}));
// In expressions, like any operand: a matrix weighted column by column, 0 to 1, without a tensor
// of weights. The weights are a generator too, read by another.
const auto data = strata::full(Shape<Dyn, Dyn>{3, 4}, 2.0);
const auto column_weight = strata::generate(
Shape<Dyn, Dyn>{3, 4},
[weights = strata::linspace(0.0, 1.0, 4)](std::size_t /*i*/, std::size_t j) { return weights[j]; });
const Tensor weighted = data * column_weight;
print("2 weighted by column, 0 .. 1", weighted);
// Shaped like something at hand, a tensor here, whatever its type.
const Tensor identity = strata::eye<double>(Shape<3, 3>{});
const Tensor shifted = identity + strata::full_like(identity, 0.5);
print("identity + full_like(identity, 0.5)", shifted);
}
03. Views
Memory someone else owns, seen as a tensor. Slices, blocks and transposes are views of the same memory: nothing is copied, and writing through them writes the original.
// SPDX-License-Identifier: Apache-2.0
// Views: memory someone else owns, seen as a tensor. Slices, blocks and transposes are views too,
// of the same memory: nothing is copied, and writing through them writes the original.
import strata;
import std;
namespace {
using strata::block;
using strata::Dense;
using strata::Dyn;
using strata::permute;
using strata::RowMajor;
using strata::seq;
using strata::Shape;
using strata::slice;
using strata::Strided;
using strata::Tensor;
using strata::TensorView;
// A function over any matrix of doubles takes a strided view: every layout converts to it, a
// tensor, a row-major view, a block, a transpose.
using AnyMatrix = TensorView<const double, Shape<Dyn, Dyn>, Strided<Dyn, Dyn>>;
auto print(std::string_view title, AnyMatrix matrix) -> void {
std::println("{} {}:\n{}", title, matrix.shape(), matrix);
}
// The sum of the diagonal, of a square matrix or not.
auto trace(AnyMatrix matrix) -> double {
double total = 0;
for (std::size_t i = 0; i < std::min(matrix.extent(0), matrix.extent(1)); ++i) {
total += matrix[i, i];
}
return total;
}
} // namespace
auto main() -> int {
// Twelve numbers in a std::vector, filled through a flat view of it, then seen as a row-major
// 3 x 4 matrix, as C and NumPy lay it out.
std::vector<double> numbers(12);
strata::assign(TensorView{numbers.data(), Shape<Dyn>{numbers.size()}}, strata::iota<double>(12));
const TensorView<double, Shape<Dyn, Dyn>, Dense<RowMajor>> matrix{numbers.data(), Shape<Dyn, Dyn>{3, 4}};
print("matrix", matrix);
// Views are made with a pipe: `matrix | slice<0>(1)` is row 1, an axis less. The column steps over
// the rows, so its view is strided.
const auto row = matrix | slice<0>(1);
const auto column = matrix | slice<1>(2);
std::println("row 1: {:l}", row);
std::println("column 2: {:l}", column);
// A block is a `seq` for each axis: `seq<2>{1}` is two indices from 1, so the size is known at compile
// time; `seq{0, 3}` is the indices 0 to 2, a size known at run time.
const auto middle = matrix | block(seq<2>{1}, seq<2>{1});
print("2 x 2 from (1, 1)", middle);
print("rows 0 to 2, two columns from 2", matrix | block(seq{0, 3}, seq<2>{2}));
// A transpose reorders the axes; the memory stays where it is.
print("transposed", matrix | permute<1, 0>);
// Assigning to a view writes the memory it views, here the vector: the top-left block becomes
// ten times the bottom-right one. The operators of the elements are all there, `+=` to `>>=`.
const auto top_left = matrix | block(seq<2>{0}, seq<2>{0});
const auto bottom_right = matrix | block(seq<2>{1}, seq<2>{2});
top_left = bottom_right * 10.0;
row += 100.0; // row 1 was a view of `numbers` before the write, and still is
std::println("numbers[0] = {}, numbers[4] = {}, numbers[5] = {}", numbers[0], numbers[4], numbers[5]);
print("after the writes", matrix);
// Expressions over views of different layouts mix freely.
const auto corner = matrix | block(seq<3>{0}, seq<3>{0});
const Tensor<double, Shape<Dyn, Dyn>> symmetric = corner + (corner | permute<1, 0>);
print("block + its transpose", symmetric);
// One function of our own for all of them.
std::println("traces: matrix {}, block {}, transpose {}, tensor {}", trace(matrix), trace(middle),
trace(matrix | permute<1, 0>), trace(symmetric));
// strata's reductions take them all as well, and read each in the order of its own memory: the
// row-major matrix front to back, its transpose too, the block row by row.
std::println("sums: matrix {}, block {}, transpose {}, tensor {}", strata::sum(matrix), strata::sum(middle),
strata::sum(matrix | permute<1, 0>), strata::sum(symmetric));
std::println("matrix: min {}, max {}, mean {}", strata::min(matrix), strata::max(matrix), strata::mean(matrix));
// A std::array works the same; static extents make a static view.
std::array<double, 6> raw{1, 2, 3, 4, 5, 6};
const TensorView fixed{raw.data(), Shape<2, 3>{}};
print("std::array as 2 x 3", fixed);
}
04. Memory
Where a tensor's elements live, and how to choose: the default policy, a stronger alignment, an arena, and an allocator of your own.
// SPDX-License-Identifier: Apache-2.0
// Where a tensor's elements live, and how to choose: the default policy, a stronger alignment, an
// arena, and an allocator of one's own. Under a policy strata places the memory itself; an
// allocator given is always used.
import strata;
import std;
namespace {
using strata::Aligned;
using strata::ColumnMajor;
using strata::Dyn;
using strata::Shape;
using strata::Tensor;
// Whether `pointer` is on a `boundary`-byte boundary: what an alignment promises, and no more.
auto aligned_to(const void* pointer, std::size_t boundary) -> bool {
return std::bit_cast<std::uintptr_t>(pointer) % boundary == 0;
}
// An allocator of one's own: std::allocator underneath, counting what it is asked for. It declares
// the alignment it gives, which is all strata needs to know of it.
std::size_t allocations = 0;
template <typename T>
struct CountingAllocator {
using value_type = T;
static constexpr std::size_t alignment = __STDCPP_DEFAULT_NEW_ALIGNMENT__;
CountingAllocator() = default;
template <typename U>
CountingAllocator(const CountingAllocator<U>& /*other*/) noexcept {}
auto allocate(std::size_t count) -> T* {
++allocations;
return std::allocator<T>{}.allocate(count);
}
auto deallocate(T* pointer, std::size_t count) noexcept -> void { std::allocator<T>{}.deallocate(pointer, count); }
friend auto operator==(const CountingAllocator&, const CountingAllocator&) -> bool = default;
};
} // namespace
auto main() -> int {
// The default policy, Aligned<64>: a small static tensor is its elements, a cache line of them;
// a dynamic one is on the heap, 64-byte aligned for SIMD and cache lines.
const Tensor<float, Shape<4, 4>> small;
const Tensor<float, Shape<Dyn, Dyn>> large{Shape<Dyn, Dyn>{100, 100}};
std::println("Tensor<float, Shape<4, 4>>: {} bytes, inside the object", sizeof(small));
std::println("Tensor<float, Shape<Dyn, Dyn>>: {} bytes of handle, elements 64-byte aligned: {}", sizeof(large),
aligned_to(large.data(), 64));
// A stronger alignment is a policy too, for inline and heap elements alike.
const Tensor<float, Shape<8, 8>, ColumnMajor, Aligned<128>> aligned;
std::println("Aligned<128>: elements 128-byte aligned: {}", aligned_to(aligned.data(), 128));
// Elements about to be overwritten need no zeros first. A tensor made of an expression skips
// them by itself; written by hand, it is asked for.
Tensor<float, Shape<Dyn>> buffer{Shape<Dyn>{1000}, strata::uninitialized};
strata::assign(buffer, strata::iota<float>(1000));
std::println("uninitialized, then written: buffer[999] = {}", buffer[999]);
// An arena: every tensor takes its elements from the buffer below, static ones too, and all of
// it goes at once when the arena does. A std::pmr::polymorphic_allocator<> serves any element
// type: strata rebinds it.
std::array<std::byte, 16 * 1024> stack{};
{
std::pmr::monotonic_buffer_resource arena{stack.data(), stack.size(), std::pmr::null_memory_resource()};
const strata::pmr::Tensor<float, Shape<Dyn, Dyn>> x{Shape<Dyn, Dyn>{16, 16}, 1.0F, &arena};
const strata::pmr::Tensor<float, Shape<4, 4>> y{strata::eye<float>(Shape<4, 4>{}), &arena};
const Tensor<double, Shape<Dyn, Dyn>, ColumnMajor, std::pmr::polymorphic_allocator<>> z{x * 3.0F, &arena};
const auto inside = [&](const void* pointer) {
const auto* byte = static_cast<const std::byte*>(pointer);
return byte >= stack.data() && byte < stack.data() + stack.size();
};
std::println("arena: x {}, y {}, z {}; y[2, 2] = {}, z[15, 15] = {}", inside(x.data()), inside(y.data()),
inside(z.data()), y[2, 2], z[15, 15]);
}
// An allocator of one's own is used as std::vector would use it, for every tensor of it.
Tensor<float, Shape<Dyn>, ColumnMajor, CountingAllocator<float>> counted{Shape<Dyn>{10}, 1.0F};
counted = counted * 2.0F; // the same extents: in place
std::println("counting allocator: {} allocation(s), counted[9] = {}", allocations, counted[9]);
std::println("strata assumes {}-byte alignment of it", strata::allocation_alignment<CountingAllocator<float>>);
}
05. Units
Tensors of quantities with units. strata knows nothing about units: it takes any value type with the operations an expression uses.
// SPDX-License-Identifier: Apache-2.0
// Tensors of quantities with units. strata knows nothing about units: it takes any value type with
// the operations an expression uses, works out the type of each result from them, and has no
// operator where they have none. A units library such as mp-units fits in the same way; a small
// one of our own here keeps the example free of dependencies.
import strata;
import std;
namespace {
using strata::block;
using strata::Dyn;
using strata::seq;
using strata::Shape;
using strata::Tensor;
// A quantity of length^L x time^T, in metres and seconds. The dimension is in the type, so the
// compiler checks it: lengths add to lengths only, and multiplying or dividing makes new ones.
template <int L, int T>
struct Quantity {
double value;
friend constexpr auto operator+(Quantity lhs, Quantity rhs) -> Quantity { return {lhs.value + rhs.value}; }
friend constexpr auto operator-(Quantity lhs, Quantity rhs) -> Quantity { return {lhs.value - rhs.value}; }
friend constexpr auto operator*(Quantity lhs, double factor) -> Quantity { return {lhs.value * factor}; }
template <int L2, int T2>
friend constexpr auto operator*(Quantity lhs, Quantity<L2, T2> rhs) -> Quantity<L + L2, T + T2> {
return {lhs.value * rhs.value};
}
template <int L2, int T2>
friend constexpr auto operator/(Quantity lhs, Quantity<L2, T2> rhs) -> Quantity<L - L2, T - T2> {
return {lhs.value / rhs.value};
}
// Quantities of one dimension compare; of different ones they do not.
friend constexpr auto operator<=>(Quantity lhs, Quantity rhs) = default;
// The root of an even power: of an area a length, of a time squared a time. strata's sqrt calls
// it unqualified, and argument-dependent lookup finds it here.
friend auto sqrt(Quantity quantity) -> Quantity<L / 2, T / 2>
requires(L % 2 == 0 && T % 2 == 0)
{
return {std::sqrt(quantity.value)};
}
};
using Length = Quantity<1, 0>;
using Time = Quantity<0, 1>;
using Speed = Quantity<1, -1>;
using Acceleration = Quantity<1, -2>;
template <int L, int T>
auto unit(Quantity<L, T> /*quantity*/) -> std::string {
const auto power = [](std::string_view symbol, int exponent) -> std::string {
if (exponent == 0) {
return "";
}
return exponent == 1 ? std::string{symbol} : std::format("{}^{}", symbol, exponent);
};
return power("m", L) + (L != 0 && T != 0 ? "·" : "") + power("s", T);
}
// A tensor of quantities prints once its element does: one line, two decimals each.
auto print(std::string_view title, const auto& quantities) -> void {
std::println("{:>14}: {:l:.2f}", title, quantities);
}
template <typename L, typename R>
concept addable = requires(const L& lhs, const R& rhs) { lhs + rhs; };
template <typename L, typename R>
concept comparable = requires(const L& lhs, const R& rhs) { lhs < rhs; };
template <typename E>
concept sqrtable = requires(const E& operand) { strata::sqrt(operand); };
} // namespace
// A quantity prints as its value, by the spec given for a double, and its unit: `{:.2f}` is
// `1.23 m`. With this, a tensor of quantities prints like a tensor of numbers.
template <int L, int T>
struct std::formatter<Quantity<L, T>> : std::formatter<double> { // NOLINT(bugprone-std-namespace-modification)
// Any context, not std::format_context only: std::formattable asks with one of its own.
template <typename Ctx>
auto format(Quantity<L, T> quantity, Ctx& ctx) const -> Ctx::iterator {
auto out = std::formatter<double>::format(quantity.value, ctx);
return std::format_to(out, " {}", unit(quantity));
}
};
auto main() -> int {
// Positions of a body falling from rest, every half second: x = g t² / 2. A generator of
// lengths, made into a tensor.
constexpr std::size_t samples = 7;
constexpr Time step{0.5};
const Tensor<Length, Shape<Dyn>> position = strata::generate(Shape<Dyn>{samples}, [](std::size_t i) {
const double t = step.value * static_cast<double>(static_cast<std::ptrdiff_t>(i));
return Length{9.81 * t * t / 2.0};
});
// Finite differences over blocks of the same tensor, one step apart. Length / Time is Speed,
// Speed / Time is Acceleration: the types say so, and so would the compiler if they did not.
const auto later = position | block(seq{1, samples});
const auto earlier = position | block(seq{0, samples - 1});
const Tensor velocity = (later - earlier) / step;
const Tensor acceleration =
((velocity | block(seq{1, samples - 1})) - (velocity | block(seq{0, samples - 2}))) / step;
static_assert(std::same_as<decltype(velocity)::value_type, Speed>);
static_assert(std::same_as<decltype(acceleration)::value_type, Acceleration>);
print("position", position);
print("velocity", velocity);
print("acceleration", acceleration);
// A quantity is a scalar like any number: how far each average speed carries in two seconds.
const Tensor<Length, Shape<Dyn>> distance = velocity * Time{2.0};
print("in 2 s", distance);
// Functions and comparisons take quantities too, with the dimensions checked as for operators:
// t = sqrt(2 x / g) is a time again, and only speeds compare with a speed.
constexpr Acceleration g{9.81};
const Tensor<Time, Shape<Dyn>> time = ((position * 2.0) / g) | strata::sqrt;
print("sqrt(2 x / g)", time);
std::println("{:>14}: {} of {}", "faster than 10", strata::count(velocity > Speed{10.0}), velocity.extent(0));
static_assert(!sqrtable<Tensor<Length, Shape<Dyn>>>); // a length has no root in metres
static_assert(!comparable<Tensor<Speed, Shape<Dyn>>, Length>);
// And a constant of quantities is a generator like any other.
print("start + 1 m", position + strata::full(Shape<Dyn>{samples}, Length{1.0}));
// Wrong dimensions are no expression at all.
static_assert(addable<Tensor<Length, Shape<Dyn>>, Tensor<Length, Shape<Dyn>>>);
static_assert(!addable<Tensor<Length, Shape<Dyn>>, Tensor<Time, Shape<Dyn>>>);
static_assert(!addable<Tensor<Speed, Shape<Dyn>>, Length>);
}
06. An image
An image as a tensor, height by width by channels. Channels are slices, a crop is a block, a grey version is an expression, a threshold is a mask.
// SPDX-License-Identifier: Apache-2.0
// An image as a tensor: height x width x channels, row-major, as image files and libraries lay it
// out. Channels are slices, a crop is a block, a grey version is an expression over the channels,
// a threshold is a mask, and converting it back to bytes is an explicit conversion. The pictures
// are drawn in the terminal in 24-bit colour; with NO_COLOR set (https://no-color.org), as
// characters from dark to light.
import strata;
import std;
namespace {
using strata::block;
using strata::Dyn;
using strata::permute;
using strata::RowMajor;
using strata::seq;
using strata::Shape;
using strata::slice;
using strata::Tensor;
using Image = Tensor<std::uint8_t, Shape<Dyn, Dyn, 3>, RowMajor>;
using Grey = Tensor<float, Shape<Dyn, Dyn>, RowMajor>;
using Colour = std::array<std::uint8_t, 3>;
auto colour_wanted() -> bool {
static const bool wanted = std::getenv("NO_COLOR") == nullptr; // NOLINT(concurrency-mt-unsafe): read once
return wanted;
}
auto to_byte(double value) -> std::uint8_t { return static_cast<std::uint8_t>(std::clamp(value, 0.0, 255.0)); }
// Draws `height` x `width` pixels, `pixel(y, x)` giving each colour. In colour, two pixels to a
// character: the upper half block takes the top one as its colour and the bottom one as its
// background. Otherwise one character a pixel, by its luma.
auto draw(std::string_view title, std::size_t height, std::size_t width, const auto& pixel) -> void {
std::println("{} ({} x {}):", title, height, width);
if (!colour_wanted()) {
constexpr std::string_view ramp = " .:-=+*#%@";
for (std::size_t y = 0; y < height; ++y) {
for (std::size_t x = 0; x < width; ++x) {
const Colour c = pixel(y, x);
const double luma = (0.299 * c[0]) + (0.587 * c[1]) + (0.114 * c[2]);
std::print("{}", ramp[static_cast<std::size_t>(luma) * (ramp.size() - 1) / 255]);
}
std::println("");
}
return;
}
for (std::size_t y = 0; y < height; y += 2) {
for (std::size_t x = 0; x < width; ++x) {
const Colour top = pixel(y, x);
std::print("\x1b[38;2;{};{};{}m", top[0], top[1], top[2]);
if (y + 1 < height) {
const Colour bottom = pixel(y + 1, x);
std::print("\x1b[48;2;{};{};{}m", bottom[0], bottom[1], bottom[2]);
} else {
std::print("\x1b[49m"); // an odd last row: the terminal's own background below
}
std::print("▀");
}
std::println("\x1b[0m");
}
}
// An image, its channels, and grey matrices of 0..255, drawn alike.
auto draw_rgb(std::string_view title, const auto& image) -> void {
draw(title, image.extent(0), image.extent(1),
[&](std::size_t y, std::size_t x) { return Colour{image[y, x, 0], image[y, x, 1], image[y, x, 2]}; });
}
auto draw_channel(std::string_view title, const auto& channel, std::size_t which) -> void {
draw(title, channel.extent(0), channel.extent(1), [&](std::size_t y, std::size_t x) {
Colour colour{};
colour[which] = static_cast<std::uint8_t>(channel[y, x]);
return colour;
});
}
auto draw_grey(std::string_view title, const auto& grey) -> void {
draw(title, grey.extent(0), grey.extent(1), [&](std::size_t y, std::size_t x) {
const std::uint8_t level = to_byte(static_cast<double>(grey[y, x]));
return Colour{level, level, level};
});
}
// A sunset over the sea, made by one function of the pixel and the channel: a sky from deep blue
// to orange, a sun on the horizon, and the sea below with the sun's light on it.
auto sunset(std::size_t height, std::size_t width) -> Image {
const auto pixel = [=](std::size_t y, std::size_t x, std::size_t channel) -> std::uint8_t {
const double u = static_cast<double>(x) / static_cast<double>(width - 1); // 0 .. 1, left to right
const double v = static_cast<double>(y) / static_cast<double>(height - 1); // 0 .. 1, top to bottom
constexpr double horizon = 0.62;
const double sun_x = 0.5;
const double sun_y = horizon - 0.04;
const double dx = (u - sun_x) * static_cast<double>(width) / static_cast<double>(height);
const double dy = v - sun_y;
const double sun = std::exp(-((dx * dx) + (dy * dy)) / 0.006); // a soft disc
std::array<double, 3> colour{};
if (v < horizon) {
const double t = v / horizon; // 0 at the top of the sky, 1 at the horizon
colour = {20 + (235 * t * t), 30 + (110 * t * t), 90 + (20 * t) - (60 * t * t)};
const double glow = sun + (0.35 * std::exp(-((dx * dx) + (dy * dy)) / 0.05));
colour = {colour[0] + (255 * glow), colour[1] + (200 * glow), colour[2] + (90 * sun)};
} else {
const double depth = (v - horizon) / (1 - horizon); // 0 at the horizon, 1 at the bottom
colour = {15 + (40 * (1 - depth)), 40 + (40 * (1 - depth)), 70 + (60 * (1 - depth))};
const double path = std::exp(-(dx * dx) / (0.004 + (0.05 * depth))) * (1 - (0.6 * depth));
const double ripple = 0.5 + (0.5 * std::sin((140.0 * v) + (6.0 * u)));
colour = {colour[0] + (230 * path * ripple), colour[1] + (140 * path * ripple), colour[2] + (40 * path)};
}
return to_byte(colour[channel]);
};
return Image{strata::generate(Shape<Dyn, Dyn, 3>{height, width}, pixel)};
}
} // namespace
auto main() -> int {
constexpr std::size_t height = 40;
constexpr std::size_t width = 72;
const Image image = sunset(height, width);
draw_rgb("a sunset, 40 x 72 x 3 bytes", image);
// Each channel is a slice along the last axis: a height x width view, stepping 3 bytes at a time.
const auto red = image | slice<2>(0);
const auto green = image | slice<2>(1);
const auto blue = image | slice<2>(2);
draw_channel("red channel", red, 0);
draw_channel("blue channel", blue, 2);
// Luma as a weighted sum of the channels: bytes times floats are floats.
const Grey grey = (red * 0.299F) + (green * 0.587F) + (blue * 0.114F);
draw_grey("grey", grey);
// A crop is a block, of the colour image or of the grey one; a transpose turns it on its side.
draw_rgb("the sun, cropped", image | block(seq{14, 34}, seq{20, 52}, seq<3>{0}));
draw_grey("grey crop, transposed", grey | block(seq{14, 34}, seq{20, 52}) | permute<1, 0>);
// A threshold is a mask, and where draws it: white where the grey is bright, black elsewhere.
const auto bright = grey > 200.0F;
draw_grey("grey > 200", strata::where(bright, 255.0F, 0.0F));
std::println("bright pixels: {} of {}", strata::count(bright), height * width);
// A vignette: darker towards the corners, from a generator of weights, on each channel alike.
const auto vignette = strata::generate(Shape<Dyn, Dyn>{height, width}, [](std::size_t y, std::size_t x) {
const double dy = (static_cast<double>(static_cast<std::ptrdiff_t>(y)) - 19.5) / 22.0;
const double dx = (static_cast<double>(static_cast<std::ptrdiff_t>(x)) - 35.5) / 38.0;
return static_cast<float>(std::max(0.0, 1.0 - (0.9 * ((dx * dx) + (dy * dy)))));
});
const Tensor<std::uint8_t, Shape<Dyn, Dyn, 3>, RowMajor> vignetted =
strata::generate(Shape<Dyn, Dyn, 3>{height, width}, [&](std::size_t y, std::size_t x, std::size_t channel) {
return to_byte(static_cast<double>(image[y, x, channel]) * static_cast<double>(vignette[y, x]));
});
draw_rgb("vignetted", vignetted);
// Back to bytes: another value type, so the conversion is spelt out.
const Tensor<std::uint8_t, Shape<Dyn, Dyn>, RowMajor> bytes{grey};
std::println("grey bytes at the sun and at a corner: {} {}", bytes[22, 36], bytes[0, 0]);
}
07. Math and masks
Functions of <cmath> take expressions and give expressions; comparisons give masks, which combine and choose values with where.
// SPDX-License-Identifier: Apache-2.0
// Math and masks on a sampled signal. The functions of <cmath> take expressions and give
// expressions; comparisons give masks of bool, which combine with & | ! and choose values with
// where; all, any and count answer questions about them. Signals are drawn as one line of bars.
// Every function takes its operand also from the left of a pipe: `x | abs | sqrt`; a call without its
// first argument, `clamp(-1, 1)`, waits for it there. Comparisons bind tighter than |, so they are
// parenthesised: `(x | abs) > 1.5`.
import strata;
import std;
namespace {
using strata::Dyn;
using strata::Shape;
using strata::Tensor;
using Signal = Tensor<double, Shape<Dyn>>;
using Mask = Tensor<bool, Shape<Dyn>>;
constexpr std::size_t samples = 64;
// A line of bars, the lowest for `low` and the highest for `high`, clamped between.
auto draw(std::string_view title, const auto& signal, double low = -3.0, double high = 3.0) -> void {
constexpr std::array<std::string_view, 8> bars{"▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"};
std::print("{:>26} ", title);
for (std::size_t i = 0; i < signal.extent(0); ++i) {
const double level = std::clamp((signal[i] - low) / (high - low), 0.0, 1.0);
std::print("{}", bars[static_cast<std::size_t>(std::lround(level * 7.0))]);
}
std::println("");
}
// A mask as a line of marks: `|` where true.
auto draw_mask(std::string_view title, const auto& mask) -> void {
std::print("{:>26} ", title);
for (std::size_t i = 0; i < mask.extent(0); ++i) {
std::print("{}", mask[i] ? "|" : "·");
}
std::println("");
}
} // namespace
auto main() -> int {
// A slow wave with a fast ripple on it, and a few spikes: a sensor that glitches now and then.
const auto t = strata::linspace(0.0, 2.0 * std::numbers::pi, samples);
const auto spikes = strata::generate(Shape<Dyn>{samples},
[](std::size_t i) { return i % 17 == 5 ? 3.0 : (i % 23 == 11 ? -3.0 : 0.0); });
const Signal signal = strata::sin(t) + (strata::sin(t * 7.0) * 0.3) + spikes;
draw("signal", signal);
// Element-wise math is lazy like any expression: nothing is computed until assigned or read.
draw("abs(signal)", signal | strata::abs);
draw("clamp(signal, -1, 1)", signal | strata::clamp(-1.0, 1.0));
draw("maximum(signal, 0)", signal | strata::maximum(0.0));
// A comparison is a mask; masks combine element by element. && and || are left as they are:
// overloaded, they would no longer short-circuit.
const auto glitch = (signal | strata::abs) > 1.5;
const auto rising = strata::cos(t) > 0.0;
draw_mask("abs(signal) > 1.5", glitch);
draw_mask("cos(t) > 0", rising);
draw_mask("glitch & cos(t) > 0", glitch & rising);
draw_mask("!glitch", !glitch);
// Questions about a mask, all at once over every element.
std::println("glitches: {} of {}; any: {}; all clean: {}", strata::count(glitch), samples, strata::any(glitch),
strata::all(!glitch));
// where chooses, element by element: the glitches replaced by the wave they hide.
const Signal cleaned = strata::where(glitch, strata::sin(t), signal);
draw("where(glitch, sin(t), x)", cleaned);
std::println("glitches left: {}", ((cleaned | strata::abs) > 1.5) | strata::count);
// A mask kept as a tensor of bool, and a function of our own over several operands.
const Mask positive = cleaned > 0.0;
const auto smoothstep = strata::map(
[](double x, double edge) {
const double s = std::clamp(x / edge, 0.0, 1.0);
return s * s * (3.0 - (2.0 * s));
},
cleaned, 0.8);
draw_mask("cleaned > 0", positive);
draw("smoothstep(cleaned, 0.8)", smoothstep, 0.0, 1.0);
// A logarithm of a signal that touches zero is no longer finite; isfinite says where. A pipeline
// needs no data to be written down: `floored` is a value, to apply to any signal.
const auto decibels = (cleaned | strata::abs | strata::log10) * 20.0;
constexpr auto floored = strata::abs | strata::maximum(1e-6) | strata::log10 | strata::isfinite;
std::println("finite decibels: {} of {}; after a floor: {}", strata::isfinite(decibels) | strata::count, samples,
cleaned | floored | strata::all);
// == stays one bool, as std::equality_comparable expects; equal() is the element-wise one.
const Signal rounded = strata::round(cleaned);
std::println("rounded to zero: {}; to one: {}", strata::count(strata::equal(rounded, 0.0)),
strata::count(strata::equal(rounded, 1.0)));
}
08. Reductions
Many values into one, of a whole expression or along axes, on a week of hourly temperatures from three sensors.
// SPDX-License-Identifier: Apache-2.0
// Reductions: many values into one. Of a whole expression, `sum(x)`, a value at once; along axes,
// `sum.along<1>(x)`, an expression of the axes left, computed when assigned like any other. A week of
// hourly temperatures from three sensors, and the questions one asks of it.
import strata;
import std;
namespace {
using strata::Dyn;
using strata::Shape;
using strata::slice;
using strata::Tensor;
constexpr std::size_t days = 7;
constexpr std::size_t hours = 24;
constexpr std::size_t sensors = 3;
constexpr std::array<std::string_view, days> day_names{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
auto print_by_day(std::string_view title, const auto& per_day) -> void {
std::print("{:>26}:", title);
for (std::size_t d = 0; d < per_day.extent(0); ++d) {
if constexpr (std::integral<std::remove_cvref_t<decltype(per_day[d])>>) {
std::print(" {}={:5}", day_names[d], per_day[d]);
} else {
std::print(" {}={:5.1f}", day_names[d], per_day[d]);
}
}
std::println("");
}
} // namespace
auto main() -> int {
// days x hours x sensors: a daily swing around a mean that warms through the week, the swing
// widening with the weather clearing, each sensor off by a little of its own. Sensor 2 sits in
// the sun and reads high in the afternoon.
const Tensor<double, Shape<days, hours, sensors>> readings =
strata::generate(Shape<days, hours, sensors>{}, [](std::size_t d, std::size_t h, std::size_t s) {
const auto day = static_cast<double>(static_cast<std::ptrdiff_t>(d));
const auto hour = static_cast<double>(static_cast<std::ptrdiff_t>(h));
const auto sensor = static_cast<double>(static_cast<std::ptrdiff_t>(s));
const double swing = (3.0 + (0.8 * day)) * std::sin((hour - 9.0) * std::numbers::pi / 12.0);
const double sun = s == 2 && h >= 12 && h <= 16 ? 4.0 : 0.0;
return 14.0 + day + swing + (0.3 * sensor) + sun;
});
// A whole tensor into one value, at once: sum, mean, the extremes.
std::println("{} readings: mean {:.2f}, min {:.2f}, max {:.2f}", readings.shape().size(), strata::mean(readings),
strata::min(readings), strata::max(readings));
// Along axes: an expression of what is left. Over hours and sensors, one value a day.
print_by_day("daily mean", Tensor<double, Shape<days>>{strata::mean.along<1, 2>(readings)});
print_by_day("daily max", Tensor<double, Shape<days>>{strata::max.along<1, 2>(readings)});
// Reductions pipe like any function: `x | square | mean.along<1, 2> | sqrt`, the daily rms, has a
// pipeline of its own to be reused.
constexpr auto rms = strata::square | strata::mean.along<1, 2> | strata::sqrt;
print_by_day("daily rms", Tensor<double, Shape<days>>{readings | rms});
// Each sensor over the whole week: a mean per sensor, and how far it sits from the others.
const Tensor<double, Shape<sensors>> per_sensor = strata::mean.along<0, 1>(readings);
std::println("{:>26}: {:.2f} {:.2f} {:.2f}", "mean by sensor", per_sensor[0], per_sensor[1], per_sensor[2]);
// A reduction is an expression like any other: sliced before it is computed, or an operand.
// The hottest hour of Wednesday, by sensor; the spread of each day, max minus min.
const auto hottest = strata::max.along<0>((readings | slice<0>(2))); // Wednesday: hours x sensors -> sensors
std::println("{:>26}: {:.2f} {:.2f} {:.2f}", "Wednesday's max by sensor", hottest[0], hottest[1], hottest[2]);
print_by_day("daily spread",
Tensor<double, Shape<days>>{strata::max.along<1, 2>(readings) - strata::min.along<1, 2>(readings)});
// `expand` is the pair of a reduction: it puts the axes back. The deviation of each reading from its day's
// mean takes the mean away along hours and sensors, and `expand<1, 2>` puts it at every hour and sensor
// again. The mean is computed first, into a tensor: an expansion reads its operand at every position, and
// a mean left lazy under it would be computed again at each.
const Tensor<double, Shape<days>> daily = strata::mean.along<1, 2>(readings);
const Tensor<double, Shape<days, hours, sensors>> deviation =
readings - strata::expand<1, 2>(daily, readings.shape());
std::println("{:>26}: {:.2f}, left in a day's mean: {:.1e}", "largest deviation",
strata::max(strata::abs(deviation)), strata::max(strata::abs(strata::mean.along<1, 2>(deviation))));
// Masks reduce too: how many hours above 20 degrees each day, and whether any sensor ever froze.
const Tensor<std::size_t, Shape<days>> warm_hours = strata::count.along<1, 2>(readings > 20.0);
print_by_day("readings above 20", warm_hours);
std::println("{:>26}: {}", "any below zero", strata::any(readings < 0.0));
// The accumulator follows the values, or is chosen: integers sum in their own type, and a mean
// of integers is a double, as in NumPy.
const Tensor<std::uint8_t, Shape<Dyn>> votes{strata::full(Shape<Dyn>{1000}, std::uint8_t{200})};
std::println("{:>26}: {} (as {}), mean {}", "1000 bytes of 200", strata::sum(votes),
std::same_as<decltype(strata::sum(votes)), int> ? "int" : "?", strata::mean(votes));
// A float sum is pairwise: a million times 0.1 stays 0.1 million, where one by one it drifts.
const Tensor<float, Shape<Dyn>> tenths{strata::full(Shape<Dyn>{1'000'000}, 0.1F)};
float naive = 0.0F;
for (std::size_t i = 0; i < tenths.extent(0); ++i) {
naive += tenths[i];
}
std::println("{:>26}: strata {:.1f}, one by one {:.1f}", "a million times 0.1F", strata::sum(tenths), naive);
}
09. Pipes
x | abs | sqrt is sqrt(abs(x)), read in the order things happen. A pipeline is a value of its own, to apply to any tensor.
// SPDX-License-Identifier: Apache-2.0
// Pipes: `x | abs | sqrt` is `sqrt(abs(x))`, read in the order things happen. Every function of strata
// takes its operand from the left of a pipe; a call without its first argument, `pow(2.0)`, waits
// for it there; and a pipeline needs no data to be written down: `square | sum | sqrt` is a value of
// its own, to apply to any tensor, or to pass on. Pipes are lazy as every expression is.
import strata;
import std;
namespace {
using strata::Dyn;
using strata::Shape;
using strata::Tensor;
// A pipeline is an argument like any function: applied to whatever it is given.
auto show(std::string_view title, const auto& pipeline, const auto& data) -> void {
std::println("{:>22}: {::.3f}", title, data | pipeline);
}
} // namespace
auto main() -> int {
const Tensor<double, Shape<Dyn>> x = strata::linspace(-2.0, 2.0, 5);
std::println("x: {::.2f}", x);
// A chain reads left to right. Nothing is computed until the result is read or assigned.
show("x | abs | sqrt", strata::abs | strata::sqrt, x);
// A call short of its first argument keeps the rest: `pow(2.0)` is `pow(x, 2.0)` once x comes,
// `clamp(-1, 1)` is `clamp(x, -1, 1)`. A full call stays a call: `pow(x, 2.0)`.
show("x | pow(2) | sqrt", strata::pow(2.0) | strata::sqrt, x);
show("x | clamp(-1, 1)", strata::clamp(-1.0, 1.0), x);
show("x | maximum(0)", strata::maximum(0.0), x);
// Your own function joins with map: `x | map(f)` is `map(f, x)`.
show("x | map(f)", strata::map([](double v) { return v * v * v; }), x);
// A pipeline written down once, a norm and a root of the mean square, for any tensor. A
// reduction of a whole tensor is a value, so the pipeline ends in a number.
constexpr auto norm = strata::square | strata::sum | strata::sqrt;
constexpr auto rms = strata::square | strata::mean | strata::sqrt;
std::println("{:>22}: {:.3f}, rms {:.3f}", "norm(x)", x | norm, x | rms);
// So `x` scaled to a unit length is a plain expression with a pipeline in it.
const Tensor<double, Shape<Dyn>> unit = x * (1.0 / (x | norm));
std::println("{:>22}: {::.3f} (norm {:.3f})", "unit", unit, unit | norm);
// Along axes the pipeline goes on with what is left: the length of every row of a matrix.
const Tensor<double, Shape<3, 4>> grid = strata::generate(Shape<3, 4>{}, [](std::size_t i, std::size_t j) {
return static_cast<double>(static_cast<std::ptrdiff_t>(i)) -
static_cast<double>(static_cast<std::ptrdiff_t>(j));
});
constexpr auto row_length = strata::square | strata::sum.along<1> | strata::sqrt;
// `eval` ends it with a tensor of the values, computed at once.
const auto lengths = grid | row_length | strata::eval;
std::println("{:>22}: {::.3f}", "row lengths", lengths);
// Masks pipe to the questions about them. `|` binds looser than the comparisons: parentheses
// around them, `(x | abs) > 1`.
std::println("{:>22}: {}", "|x| > 1", ((x | strata::abs) > 1.0) | strata::count);
constexpr auto in_range = strata::abs | strata::isfinite;
std::println("{:>22}: {}", "all finite", x | in_range | strata::all);
// Between two masks `|` is still the bitwise or: only a function on its right makes a pipe.
const auto outside = (x < -1.0) | (x > 1.0);
std::println("{:>22}: {}", "outside [-1, 1]", outside | strata::count);
}
10. einsum
One notation for the products of linear algebra: "ij,jk->ik" is the matrix product, "ii->" the trace.
// SPDX-License-Identifier: Apache-2.0
// einsum: one notation for the products of linear algebra. Each operand names its axes with letters;
// the letters after `->` are the result's, and every other one is summed over. `"ij,jk->ik"` is the
// matrix product, `"ii->"` the trace, `"i,j->ij"` the outer product. The result is lazy, as every
// expression is: reading one element computes one sum.
import strata;
import std;
namespace {
using strata::einsum;
using strata::Shape;
using strata::slice;
using strata::Tensor;
using strata::TensorView;
using Matrix = TensorView<const double, Shape<2, 2>>;
using Vector = TensorView<const double, Shape<3>>;
// Column-major, as strata's default: [[1, 2], [3, 4]] and [[5, 6], [7, 8]].
constexpr std::array a_values{1.0, 3.0, 2.0, 4.0};
constexpr std::array b_values{5.0, 7.0, 6.0, 8.0};
} // namespace
auto main() -> int {
const Matrix a{a_values.data(), Shape<2, 2>{}};
const Matrix b{b_values.data(), Shape<2, 2>{}};
// The matrix product: j is in both operands and not in the result, so it is summed.
std::println("a b:\n{}", Tensor{einsum<"ij,jk->ik">(a, b)}); // [[19, 22], [43, 50]]
// One operand: a transpose, row sums, the diagonal, the trace.
std::println("a transposed:\n{}", Tensor{einsum<"ij->ji">(a)});
std::println("row sums of a: {}", Tensor{einsum<"ij->i">(a)}); // [3, 7]
std::println("diagonal of a: {}", Tensor{einsum<"ii->i">(a)}); // [1, 4]
std::println("trace of a: {}", einsum<"ii->">(a)[]); // 5
// Two vectors: the dot product, and the outer one, of higher rank than either.
constexpr std::array u_values{1.0, 2.0, 3.0};
constexpr std::array v_values{4.0, 5.0, 6.0};
const Vector u{u_values.data(), Shape<3>{}};
const Vector v{v_values.data(), Shape<3>{}};
std::println("u . v: {}", einsum<"i,i->">(u, v)[]); // 32
std::println("u v^T:\n{}", Tensor{einsum<"i,j->ij">(u, v)});
// Three operands: the cross product through the Levi-Civita symbol, (a x b)_k = eps_ijk a_i b_j.
Tensor<double, Shape<3, 3, 3>> eps{};
eps[0, 1, 2] = eps[1, 2, 0] = eps[2, 0, 1] = 1.0;
eps[0, 2, 1] = eps[2, 1, 0] = eps[1, 0, 2] = -1.0;
std::println("u x v: {}", Tensor{einsum<"ijk,i,j->k">(eps, u, v)}); // [-3, 6, -3]
// Lazy: an element, or a row, computes only what it needs. Row 1 of a b is row 1 of a times b.
const auto product = einsum<"ij,jk->ik">(a, b);
std::println("(a b)[1, 0]: {}, row 1: {}", product[1, 0], Tensor{product | slice<0>(1)});
// Any expression is an operand, and an einsum is one too: (a + a) b, and (a b)^T = b^T a^T.
std::println("(a + a) b:\n{}", Tensor{einsum<"ij,jk->ik">(a + a, b)});
std::println("b^T a^T:\n{}", Tensor{einsum<"ij,jk->ik">(einsum<"ij->ji">(b), einsum<"ij->ji">(a))});
// What it costs: one loop over every letter at once. An operand that is an expression is read once
// for each value of the letters it lacks: above, a + a once per k. Where it is dear, the assignment
// computes it into a buffer first, and reads that instead (see 11_scratch); a tensor of its values,
// made by hand, does the same. A chain of three or more matrices is still cheaper as products two
// at a time, `"ij,jk->ik"` twice, than in one einsum.
const Tensor twice_a = a + a;
std::println("(a + a) b, made first:\n{}", Tensor{einsum<"ij,jk->ik">(twice_a, b)});
}
11. Scratch buffers
The buffers of an evaluation and where their memory comes from: an allocator, a memory resource, or a policy.
// SPDX-License-Identifier: Apache-2.0
// Buffers of an evaluation, and where their memory comes from. An assignment, `Tensor t = e`, or a
// reduction to a value, `sum(e)`, first computes into a buffer each part of `e` that is read many
// times and dear to compute again, then reads the buffer; nothing in the code says so, the cost
// model decides at compile time. `scratch` says where those buffers take their memory: an allocator,
// a memory resource, or a policy, as a tensor takes. Without it, they come from the heap, aligned.
import strata;
import std;
namespace {
using strata::Dyn;
using strata::einsum;
using strata::exp;
using strata::materializes;
using strata::scratch;
using strata::Shape;
using strata::sum;
using strata::Tensor;
using Matrix = Tensor<double, Shape<Dyn, Dyn>>;
// i / 10 + j, an n × m matrix.
auto matrix(std::size_t n, std::size_t m) -> Matrix {
return Matrix{strata::generate(Shape<Dyn, Dyn>{n, m}, [](std::size_t i, std::size_t j) {
return (static_cast<double>(i) / 10) + static_cast<double>(j);
})};
}
} // namespace
auto main() -> int {
const Matrix a = matrix(3, 4);
const Matrix b = matrix(4, 5);
// exp(a / 10) is read once for every column of b: computed into a buffer first, 12 calls of exp
// rather than 60. `materializes` says so, for a static_assert where a buffer is expected; a + a,
// read once, never needs one.
const auto product = einsum<"ij,jk->ik">(exp(a * 0.1), b);
static_assert(materializes<decltype(product)>);
static_assert(!materializes<decltype(a + a)>);
std::println("exp(a / 10) b:\n{::.2f}", Tensor{product});
// The buffers from an arena: a stack buffer, nothing from the heap. Its upstream is the null
// resource: past its 4096 bytes the arena throws rather than go to the heap.
std::array<std::byte, 4096> stack{};
std::pmr::monotonic_buffer_resource arena{stack.data(), stack.size(), std::pmr::null_memory_resource()};
const Tensor in_arena = product | scratch(&arena);
std::println("the same, its buffer in the arena:\n{::.2f}", in_arena);
// A scratch is a value: one for many evaluations, as in this loop. The arena gives
// its memory back all at once, between them.
const auto temporaries = scratch(&arena);
for (int round = 0; round < 3; ++round) {
arena.release();
std::println("round {}: sum {:.3f}", round, sum(einsum<"ij,jk->ik">(exp(a * (0.1 * round)), b) | temporaries));
}
// Where nothing is worth a buffer, a scratch is not used, and that is no error: whether a part is
// buffered is the cost model's call, which may change as it is tuned.
arena.release();
std::println("a + a, nothing to buffer:\n{::.1f}", Tensor{(a + a) | temporaries});
// An assignment that reads its own destination other than where it writes, a transpose in place,
// goes through a buffer too, and a scratch at its root gives that one. `noalias` promises there is
// nothing to look for: c * 2 reads each element where it writes it.
arena.release();
Matrix c = matrix(3, 3);
c = (c | strata::permute<1, 0>) | temporaries;
c = c * 2.0 | strata::noalias;
std::println("2 c^T, in place:\n{::.1f}", c);
// The nearest scratch above a buffer gives its memory: exp(a)'s from the arena, none from the
// outer one, a policy, which asks for 128-byte alignment.
arena.release();
const auto nested = einsum<"ij,jk->ik">(exp(a * 0.1) | temporaries, b) | scratch(strata::Aligned<128>{});
std::println("nested scratch:\n{::.2f}", Tensor{nested});
}
12. Tensors and views
Making a tensor, its order in memory, and views of memory with their slices, blocks and transposes; the program behind the Tensors and views guide.
// SPDX-License-Identifier: Apache-2.0
// Tensors and views, the way the guide shows them: making a tensor, its order in memory, and views
// of memory with their slices, blocks and transposes.
import strata;
import std;
namespace {
using strata::block;
using strata::Dense;
using strata::Dyn;
using strata::Matrix;
using strata::MatrixX;
using strata::permute;
using strata::RowMajor;
using strata::seq;
using strata::Shape;
using strata::slice;
using strata::Strided;
using strata::Tensor;
using strata::TensorView;
// Any matrix of doubles, whatever its layout: a tensor, a block, a transpose.
using AnyMatrix = TensorView<const double, Shape<Dyn, Dyn>, Strided<Dyn, Dyn>>;
auto trace(AnyMatrix matrix) -> double {
double total = 0;
for (std::size_t i = 0; i < std::min(matrix.extent(0), matrix.extent(1)); ++i) {
total += matrix[i, i];
}
return total;
}
// A slice of a tensor that is named is a view of its memory: reading gives a reference, writing writes.
// A slice of a temporary cannot be: its elements would be gone, so the slice holds the tensor and only
// reads.
using Square = Tensor<double, Shape<2, 2>>;
static_assert(std::is_lvalue_reference_v<decltype(slice<0>(std::declval<Square&>(), 0)[0])>);
static_assert(!std::is_lvalue_reference_v<decltype(slice<0>(std::declval<Square>(), 0)[0])>);
} // namespace
auto main() -> int {
// 2 x 3, every element value-initialized: 0.
Matrix<float, 2, 3> a;
// A shape known at run time, and every element 0.5.
const MatrixX<float> b{Shape<Dyn, Dyn>{2, 3}, 0.5F};
// Row 1, column 2.
a[1, 2] = 5.0F;
std::println("a:\n{}", a);
std::println("b: {:l}", b);
// Memory that is written before it is read.
Tensor<float, Shape<Dyn>> buffer{Shape<Dyn>{1000}, strata::uninitialized};
strata::assign(buffer, strata::linspace(0.0F, 1.0F, 1000));
std::println("buffer: {} elements, the last {}", buffer.size(), buffer[999]);
// The element at (i, j) is 10 i + j.
const auto value = [](std::size_t i, std::size_t j) { return static_cast<int>((10 * i) + j); };
const Tensor<int, Shape<2, 3>> column_major = strata::generate(Shape<2, 3>{}, value);
const Tensor<int, Shape<2, 3>, RowMajor> row_major = strata::generate(Shape<2, 3>{}, value);
std::println("column-major memory: {}", std::span{column_major.data(), 6});
std::println("row-major memory: {}", std::span{row_major.data(), 6});
std::println("element (1, 2) of both: {} and {}", column_major[1, 2], row_major[1, 2]);
// Twelve numbers in a vector, filled through a flat view of it...
std::vector<double> numbers(12);
strata::assign(TensorView{numbers.data(), Shape<Dyn>{numbers.size()}}, strata::iota<double>(12));
// ... and seen as a row-major 3 x 4 matrix.
const TensorView<double, Shape<Dyn, Dyn>, Dense<RowMajor>> matrix{numbers.data(), Shape<Dyn, Dyn>{3, 4}};
std::println("{}", matrix);
// Row 1 and column 2: one axis less. The column steps over the rows.
const auto row = matrix | slice<0>(1);
const auto column = matrix | slice<1>(2);
// A block of 2 x 2 from (1, 1); and one with a size known only at run time: rows 0, 1 and 2.
const auto square = matrix | block(seq<2>{1}, seq<2>{1});
const auto tall = matrix | block(seq{0, 3}, seq<2>{2});
// The axes swapped: 4 x 3.
const auto transposed = matrix | permute<1, 0>;
std::println("row 1: {:l}", row);
std::println("column 2: {:l}", column);
std::println("block: {:l}", square);
std::println("tall: {:l}", tall);
std::println("transposed: {}", transposed.shape());
// Assigning to a view writes its memory: the top-left block becomes ten times the one at (1, 2),
// and the vector changes. `+=` and the other operators of the elements work the same way.
const auto top_left = matrix | block(seq<2>{0}, seq<2>{0});
top_left = (matrix | block(seq<2>{1}, seq<2>{2})) * 10.0;
top_left += 1.0;
std::println("numbers[0] = {}, numbers[5] = {}", numbers[0], numbers[5]);
const Tensor<double, Shape<Dyn, Dyn>> owned{Shape<Dyn, Dyn>{2, 2}, 1.5};
std::println("traces: view {}, block {}, transpose {}, tensor {}", trace(matrix), trace(square), trace(transposed),
trace(owned));
}
13. Expressions
Lazy arithmetic, masks, pipes, reductions, generators, einsum and aliasing; the program behind the Expressions guide.
// SPDX-License-Identifier: Apache-2.0
// Expressions, the way the guide shows them: lazy arithmetic, masks, pipes, reductions, generators,
// einsum, and what happens when an expression reads the tensor it is assigned to.
import strata;
import std;
namespace {
using strata::Dyn;
using strata::Matrix;
using strata::MatrixX;
using strata::Shape;
using strata::Tensor;
template <typename L, typename R>
concept addable = requires(const L& lhs, const R& rhs) { lhs + rhs; };
static_assert(addable<Tensor<float, Shape<2, 3>>, Tensor<float, Shape<Dyn, Dyn>>>);
static_assert(!addable<Tensor<float, Shape<2, 3>>, Tensor<float, Shape<3, 2>>>); // static extents differ
} // namespace
auto main() -> int {
const Matrix<float, 2, 3> a =
strata::generate(Shape<2, 3>{}, [](std::size_t i, std::size_t j) { return static_cast<float>(i + (2 * j)); });
const MatrixX<float> b{Shape<Dyn, Dyn>{2, 3}, 0.5F};
// An expression records what to compute, and computes nothing.
const auto e = (a * 2.0F) + b;
// A tensor is made of it: computed now, in one pass over memory. The static shape wins.
Matrix c = e;
static_assert(std::same_as<decltype(c), Tensor<float, Shape<2, 3>>>);
std::println("c:\n{::.1f}", c);
// Assigned: the extents agree, so it is written in place.
c = c - b;
std::println("c - b:\n{::.1f}", c);
// A mask: a bool for every element.
const auto big = a > 2.0F;
// 2 where the mask is true, else the element of a.
const auto capped = strata::where(big, 2.0F, a);
std::println("big: {:l}", big);
std::println("capped: {:l}", capped);
std::println("{} of {} are big, any: {}", strata::count(big), big.size(), strata::any(big));
const Tensor<double, Shape<Dyn>> x = strata::linspace(-2.0, 2.0, 5);
// sqrt(abs(x)), read in the order things happen.
std::println("{::.3f}", x | strata::abs | strata::sqrt);
// `pow(2.0)` waits for its first argument.
std::println("{::.3f}", x | strata::pow(2.0));
// A pipeline is a value of its own.
constexpr auto norm = strata::square | strata::sum | strata::sqrt;
std::println("norm(x) = {:.3f}", x | norm);
const Tensor<double, Shape<2, 3>> m =
strata::generate(Shape<2, 3>{}, [](std::size_t i, std::size_t j) { return static_cast<double>(i + (2 * j)); });
// Of the whole tensor: a value, at once.
std::println("sum {}, mean {}, max {}", strata::sum(m), strata::mean(m), strata::max(m));
// Along an axis: an expression of the axes that are left.
const Tensor<double, Shape<2>> per_row = strata::sum.along<1>(m);
const Tensor<double, Shape<3>> per_column = strata::sum.along<0>(m);
std::println("per row {:l}, per column {:l}", per_row, per_column);
std::println("iota: {:l}", strata::iota<int>(5));
std::println("linspace: {:l:.2f}", strata::linspace(0.0, 1.0, 5));
std::println("eye:\n{}", strata::eye<int>(Shape<3, 3>{}));
// No memory until the tensor is made.
const Tensor<int, Shape<3, 3>> scaled = strata::eye<int>(Shape<3, 3>{}) * 7;
std::println("7 I: {:l}", scaled);
// [[1, 2], [3, 4]].
const Matrix<double, 2, 2> p = strata::generate(
Shape<2, 2>{}, [](std::size_t i, std::size_t j) { return static_cast<double>((2 * i) + j + 1); });
// The matrix product.
std::println("p p:\n{}", Tensor{strata::einsum<"ij,jk->ik">(p, p)});
// No letters left after the arrow: rank 0, read with `[]`.
std::println("trace: {}", strata::einsum<"ii->">(p)[]);
Matrix<double, 2, 2> t = p;
// Reads each element where it writes it: computed in place.
t = t * 2.0;
// Would read elements already written: computed into a buffer first.
t = t | strata::permute<1, 0>;
std::println("transposed, doubled: {:l}", t);
// A promise that nothing is read after it is written: no check, no buffer.
t = t * 2.0 | strata::noalias;
std::println("doubled again: {:l}", t);
}
14. The cost model
Which parts of an expression are computed into a buffer first, watched through a function that counts its calls; the program behind the cost model guide.
// SPDX-License-Identifier: Apache-2.0
// The cost model, the way the guide shows it: which parts of an expression an evaluation computes
// into a buffer first, and why. A function that counts its calls, and says what a call costs, shows
// the decision from the outside.
import strata;
import std;
namespace {
using strata::Dyn;
using strata::einsum;
using strata::map;
using strata::materializes;
using strata::Shape;
using strata::Tensor;
// A function of our own that counts its calls, and names what one costs: in Eigen's units, where
// reading a value or adding two is 1.
template <double Cost>
struct Counted {
static constexpr double cost = Cost;
static inline std::size_t calls = 0;
auto operator()(double x) const -> double {
++calls;
return x * 2.0;
}
};
using Cheap = Counted<1.0>;
using Dear = Counted<30.0>;
} // namespace
auto main() -> int {
// A 3 x 4 matrix times a 4 x 2 one: each of the 12 elements of `a` is read once for each of the
// 2 columns of `b`.
const Tensor<double, Shape<3, 4>> a = strata::full(Shape<3, 4>{}, 1.0);
const Tensor<double, Shape<4, 2>> b = strata::full(Shape<4, 2>{}, 1.0);
const Tensor<double, Shape<3, 2>> cheap = einsum<"ij,jk->ik">(map(Cheap{}, a), b);
const Tensor<double, Shape<3, 2>> dear = einsum<"ij,jk->ik">(map(Dear{}, a), b);
std::println("a function costing 1: {} calls", Cheap::calls);
std::println("a function costing 30: {} calls", Dear::calls);
std::println("the same result: {} and {}", cheap[0, 0], dear[0, 0]);
// Read twice, a function costing 30 is worth a buffer; one costing 1 is not.
static_assert(!materializes<decltype(einsum<"ij,jk->ik">(map(Cheap{}, a), b))>);
static_assert(materializes<decltype(einsum<"ij,jk->ik">(map(Dear{}, a), b))>);
// Read once, nothing is worth a buffer, however dear.
static_assert(!materializes<decltype(strata::exp(a + a))>);
// A dynamic extent is taken as 64: read that often, even a sum is worth a buffer.
const Tensor<double, Shape<Dyn, Dyn>> x{Shape<Dyn, Dyn>{3, 4}, 1.0};
const Tensor<double, Shape<Dyn, Dyn>> y{Shape<Dyn, Dyn>{4, 2}, 1.0};
static_assert(!materializes<decltype(einsum<"ij,jk->ik">(a + a, b))>);
static_assert(materializes<decltype(einsum<"ij,jk->ik">(x + x, y))>);
}
15. Assignment
Writing into a tensor or a view of one: = and every compound operator, from a scalar, a tensor or an expression, with a slice, a block or a diagonal on the left.
// SPDX-License-Identifier: Apache-2.0
// Assignment: writing into a tensor, or into a view of one. `=` and every compound operator, from a
// scalar, a tensor or any expression; a slice, a block, a transpose or a diagonal on the left. An
// expression that reads the memory it writes is safe: it is computed first where it has to be.
import strata;
import std;
namespace {
using strata::block;
using strata::diagonal;
using strata::Dyn;
using strata::permute;
using strata::seq;
using strata::Shape;
using strata::slice;
using strata::Tensor;
} // namespace
auto main() -> int {
Tensor<int, Shape<4, 4>> grid{Shape<4, 4>{}, 0};
// A scalar is every element. The operators are those of the elements, as for built-in types:
// `grid += 2` is `grid = grid + 2`, converted back to the element type.
grid = 1;
grid += 2;
grid *= 3;
std::println("every element: {}", grid[0, 0]);
// A view on the left writes the memory it views. Make one with a pipe, and assign to it: a row,
// a block of 2 x 2, the main diagonal.
const auto first_row = grid | slice<0>(0);
first_row = 0;
const auto middle = grid | block(seq<2>{1}, seq<2>{1});
middle -= 4;
const auto diagonal_view = grid | diagonal<0, 1>;
diagonal_view = strata::iota<int>(4); // 0, 1, 2, 3
std::println("rows, a block and the diagonal written:\n{}", grid);
// An expression of the same shape is as good as a scalar. Here each row of the middle block takes
// the sum of the two views next to it.
(grid | block(seq<2>{1}, seq<2>{1})) = (grid | block(seq<2>{1}, seq<2>{0})) + (grid | block(seq<2>{1}, seq<2>{2}));
std::println("the block from its neighbours:\n{}", grid);
// Every operator, once, on a line of numbers.
Tensor<int, Shape<Dyn>> numbers{strata::iota<int>(6) + 6}; // 6 .. 11
const auto show = [&](std::string_view op) { std::println("{:>6} {:l}", op, numbers); };
show("start");
numbers += 1;
show("+= 1");
numbers -= strata::full(Shape<Dyn>{6}, 2);
show("-= 2");
numbers *= numbers;
show("*= self");
numbers /= 3;
show("/= 3");
numbers %= 7;
show("%= 7");
numbers <<= 2;
show("<<= 2");
numbers >>= 1;
show(">>= 1");
numbers |= 1;
show("|= 1");
numbers &= 0b110;
show("&= 6");
numbers ^= 0b011;
show("^= 3");
}
16. Making a tensor
From a shape, from lists and nested lists, from any range, from a generator or an expression; changing the shape of one with resize and conservative_resize.
// SPDX-License-Identifier: Apache-2.0
// Making a tensor: from a shape, from a list written out, from nested lists, from any range, from a
// generator or an expression; and changing the shape of one afterwards.
import strata;
import std;
namespace {
using strata::Dyn;
using strata::RowMajor;
using strata::Shape;
using strata::Tensor;
} // namespace
auto main() -> int {
// A shape, and a value or none. Without one every element is value-initialized: 0.
const Tensor<int, Shape<2, 3>> zeros;
const Tensor<int, Shape<Dyn, Dyn>> sevens{Shape<Dyn, Dyn>{2, 3}, 7};
std::println("zeros: {:l}, sevens: {:l}", zeros, sevens);
// Nested lists are written as a tensor is read: row by row, whatever the order in memory. The
// extents are the lists' where the shape leaves them open, and what the shape fixes is checked.
const Tensor<double, Shape<2, 3>> fixed{{1, 2, 3}, {4, 5, 6}};
const Tensor<double, Shape<Dyn, Dyn>> dynamic{{1, 2, 3}, {4, 5, 6}};
const Tensor<double, Shape<2, 3>, RowMajor> row_major{{1, 2, 3}, {4, 5, 6}};
std::println("rows as written:\n{}", fixed);
std::println("the same, extents from the lists: {}", dynamic.shape());
std::println("memory, column-major: {}, row-major: {}", std::span{fixed.data(), 6}, std::span{row_major.data(), 6});
// With no shape given, the lists name the tensor: one list a vector, nested lists a matrix.
const Tensor vector{1, 2, 3};
const Tensor matrix{{1.5, 2.5}, {3.5, 4.5}};
static_assert(std::same_as<std::remove_const_t<decltype(vector)>, Tensor<int, Shape<Dyn>>>);
static_assert(std::same_as<std::remove_const_t<decltype(matrix)>, Tensor<double, Shape<Dyn, Dyn>>>);
std::println("{:l} and {:l}", vector, matrix);
// A flat list, or any range, is the elements in the order of the memory: the k-th element of the
// range is the k-th of the tensor's memory, so a vector of the element type is one copy. For
// column-major, the first index runs fastest.
const std::vector<int> numbers{1, 2, 3, 4, 5, 6};
const Tensor<int, Shape<2, 3>> from_range{std::from_range, numbers};
const Tensor<int, Shape<Dyn, Dyn>> reshaped{Shape<Dyn, Dyn>{3, 2}, std::from_range, numbers};
const Tensor<int, Shape<2, 3>> from_list{1, 2, 3, 4, 5, 6};
std::println("from a range:\n{}\nthe same numbers as 3 x 2:\n{}", from_range, reshaped);
std::println("from a flat list: {}",
strata::all(strata::equal(from_list, from_range)) ? "the same tensor" : "another");
// Any range of values that convert: a view of the standard library is built and copied once, and
// `std::ranges::to` makes a tensor of one.
const auto squares = std::views::iota(1, 6) | std::views::transform([](int i) { return i * i; });
const auto from_view = std::ranges::to<Tensor<int, Shape<Dyn>>>(squares);
std::println("squares: {:l}", from_view);
// Elements that are moved in when the range gives them up.
std::vector<std::string> words{"strata", "tensor"};
const Tensor<std::string, Shape<Dyn>> kept{std::from_range, words | std::views::as_rvalue};
std::println("{:l} (the vector now holds \"{}\" and \"{}\")", kept, words[0], words[1]);
// A generator or an expression is a tensor in one pass over memory, without zeros first.
const Tensor<double, Shape<Dyn>> ramp = strata::linspace(0.0, 1.0, 5);
const Tensor sum = ramp + ramp;
std::println("ramp: {::.2f}, doubled: {::.2f}", ramp, sum);
// The shape of a dynamic tensor can change. `resize` gives a new shape and keeps nothing; it
// takes no more memory than the elements need, and none at all if their number is the same.
Tensor<int, Shape<Dyn, Dyn>> table{{1, 2, 3}, {4, 5, 6}};
table.resize(Shape<Dyn, Dyn>{3, 2});
table = 0;
std::println("resized to {}, then cleared:\n{}", table.shape(), table);
// `conservative_resize` keeps the elements that are at a position of both shapes, at that
// position; the new ones are the value given, 0 by default. A table that grows by a row and a
// column, and shrinks again.
Tensor<int, Shape<Dyn, Dyn>> grid{{1, 2}, {3, 4}};
grid.conservative_resize(Shape<Dyn, Dyn>{3, 3});
std::println("grown, zeros where it is new:\n{}", grid);
grid.conservative_resize(Shape<Dyn, Dyn>{2, 3}, -1);
std::println("one row less, a new column of -1:\n{}", grid);
}
17. A stencil
Heat spreading over a plate, as four shifted blocks of the plate itself, written in place: strata notices that the expression reads what it writes.
// SPDX-License-Identifier: Apache-2.0
// A stencil: heat spreading over a plate. Each cell takes the mean of its four neighbours, which
// are the same grid cut shifted by one: four blocks, no loops over cells. The grid is written
// in place, from an expression that reads it; strata sees that, and computes the right-hand side
// before it writes.
import strata;
import std;
namespace {
using strata::block;
using strata::Dyn;
using strata::seq;
using strata::Shape;
using strata::Tensor;
using Grid = Tensor<double, Shape<Dyn, Dyn>>;
// One step of the heat equation, explicit: the interior moves towards the mean of its neighbours by
// `rate`. The edges are not written, they hold their temperature.
auto step(Grid& plate, double rate) -> void {
const std::size_t rows = plate.extent(0);
const std::size_t columns = plate.extent(1);
const auto inner = block(seq{1, rows - 1}, seq{1, columns - 1});
const auto up = block(seq{0, rows - 2}, seq{1, columns - 1});
const auto down = block(seq{2, rows}, seq{1, columns - 1});
const auto left = block(seq{1, rows - 1}, seq{0, columns - 2});
const auto right = block(seq{1, rows - 1}, seq{2, columns});
const auto centre = plate | inner;
const auto neighbours = (plate | up) + (plate | down) + (plate | left) + (plate | right);
// Reads `plate` at the cells around each one while it writes them: computed first, then written.
(plate | inner) = centre + (rate * (neighbours - (4.0 * centre)));
}
// A shade for each temperature, from cold to hot.
auto draw(std::string_view title, const Grid& plate) -> void {
constexpr std::array<std::string_view, 5> shades{" ", "░", "▒", "▓", "█"};
std::println("{}", title);
for (std::size_t i = 0; i < plate.extent(0); ++i) {
for (std::size_t j = 0; j < plate.extent(1); ++j) {
const double level = std::clamp(plate[i, j], 0.0, 1.0);
std::print("{}", shades[static_cast<std::size_t>(std::lround(level * 4.0))]);
}
std::println("");
}
}
} // namespace
auto main() -> int {
constexpr std::size_t rows = 12;
constexpr std::size_t columns = 40;
// A cold plate, one edge held hot.
Grid plate{Shape<Dyn, Dyn>{rows, columns}, 0.0};
(plate | block(seq<1>{0}, seq{0, columns})) = 1.0;
draw("start", plate);
// The same step the long way, cell by cell from a copy, to check the in-place one against.
Grid copy = plate;
Grid expected = plate;
for (std::size_t i = 1; i + 1 < rows; ++i) {
for (std::size_t j = 1; j + 1 < columns; ++j) {
expected[i, j] =
copy[i, j] +
(0.2 * (copy[i - 1, j] + copy[i + 1, j] + copy[i, j - 1] + copy[i, j + 1] - (4.0 * copy[i, j])));
}
}
step(copy, 0.2);
std::println("one step, in place against cell by cell: largest difference {}",
strata::max(strata::abs(copy - expected)));
for (std::size_t i = 0; i < 60; ++i) {
step(plate, 0.2);
}
draw("after 60 steps", plate);
for (std::size_t i = 0; i < 400; ++i) {
step(plate, 0.2);
}
draw("after 460", plate);
// Questions about the whole plate are reductions: the heat in it, the hottest cell below the hot
// edge, how many cells are warm.
const auto below = plate | block(seq{1, rows}, seq{0, columns});
std::println("total heat {:.2f}, hottest below the edge {:.3f}, warm cells {}", strata::sum(plate),
strata::max(below), strata::count(plate > 0.5));
}