Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Shape composition

Shape composition joins, repeats, or grids tensors. matten provides six functions on the numeric Tensor only, across two themes:

  • concatenate — join along an existing axis (RFC-039).
  • stack — join along a new axis (RFC-039).
  • repeat / repeat_axis — repeat each element (RFC-087).
  • tile — repeat the whole tensor (RFC-087).
  • meshgrid — build coordinate grids for evaluating f(x, y) (RFC-087).

Each has a panicking convenience form and a non-panicking try_* form. All reject dynamic tensors — convert with try_numeric() first — and check the output allocation against MattenLimits before copying any data.

concatenate

Tensor::concatenate(tensors: &[&Tensor], axis: usize) -> Tensor
Tensor::try_concatenate(tensors: &[&Tensor], axis: usize) -> Result<Tensor, MattenError>

All inputs must have the same rank and the same size on every axis except axis. The output axis size is the sum of the inputs’ axis sizes; all other axes are unchanged. axis must be in 0..rank.

[2, 3] ++ [4, 3]  along axis 0  ->  [6, 3]
[2, 3] ++ [2, 5]  along axis 1  ->  [2, 8]

concatenate extends an axis that already exists:

axis 0 concatenate: add more rows

[ a a a ]      [ b b b ]      [ a a a ]
[ a a a ]  ++  [ b b b ]  ->  [ a a a ]   shape [4, 3]
                              [ b b b ]
                              [ b b b ]

axis 1 concatenate: add more columns

[ a a a ]      [ b b ]      [ a a a b b ]
[ a a a ]  ++  [ b b ]  ->  [ a a a b b ]   shape [2, 5]

A single-element list returns a clone of that tensor (after validating the axis and dynamic status).

stack

Tensor::stack(tensors: &[&Tensor], axis: usize) -> Tensor
Tensor::try_stack(tensors: &[&Tensor], axis: usize) -> Result<Tensor, MattenError>

All inputs must have identical shapes. A new axis of size n (the number of inputs) is inserted at position axis, so the output rank is the input rank plus one. axis may be 0..=rank.

three [2, 4] tensors stacked at axis 0  ->  [3, 2, 4]
three [2, 4] tensors stacked at axis 1  ->  [2, 3, 4]
three [2, 4] tensors stacked at axis 2  ->  [2, 4, 3]

stack creates a new axis whose size is the number of inputs:

two vectors shape [3]

a = [1 2 3]
b = [4 5 6]

stack([a, b], axis 0) -> shape [2, 3]

[ 1 2 3 ]
[ 4 5 6 ]

stack([a, b], axis 1) -> shape [3, 2]

[ 1 4 ]
[ 2 5 ]
[ 3 6 ]

The short rule:

concatenate: existing axis gets longer
stack:       new axis appears

A single-element list inserts a length-1 axis (the analogue of expand_dims).

repeat / repeat_axis

Tensor::repeat(&self, n: usize) -> Tensor
Tensor::try_repeat(&self, n: usize) -> Result<Tensor, MattenError>

Tensor::repeat_axis(&self, n: usize, axis: usize) -> Tensor
Tensor::try_repeat_axis(&self, n: usize, axis: usize) -> Result<Tensor, MattenError>

repeat repeats each element n times, flattening the result to rank 1:

[1, 2, 3].repeat(2)  ->  [1, 1, 2, 2, 3, 3]

repeat_axis repeats each element n times along axis, preserving rank:

[[1, 2], [3, 4]].repeat_axis(2, 0)  ->  [[1, 2], [1, 2], [3, 4], [3, 4]]

A rank-0 scalar .repeat(n) produces a rank-1 tensor of length n; repeat_axis on a rank-0 scalar is a Shape error (there is no axis to repeat along). n = 0 returns an empty tensor (RFC-111): repeat gives shape [0], repeat_axis gives a zero-length result on axis.

repeat is explicit allocation, unlike broadcasting, which is implicit and materializes nothing: [1, 2, 3] * 2 and [1, 2, 3].repeat(2) differ for exactly that reason — the first never allocates a doubled-length tensor, the second always does.

tile

Tensor::tile(&self, reps: &[usize]) -> Tensor
Tensor::try_tile(&self, reps: &[usize]) -> Result<Tensor, MattenError>

tile repeats the whole tensor, one repetition factor per axis:

[1, 2, 3].tile(&[2])         ->  [1, 2, 3, 1, 2, 3]
[[1, 2]].tile(&[2, 1])        ->  [[1, 2], [1, 2]]

repeat repeats elements; tile repeats the whole tensor — the single most confused pair in this area:

[1, 2, 3].repeat(2)   -> [1, 1, 2, 2, 3, 3]   (each element, in place)
[1, 2, 3].tile(&[2])  -> [1, 2, 3, 1, 2, 3]   (the whole tensor, twice)

If reps is shorter than the input’s rank, it is padded with leading 1s (NumPy-compatible). If reps is longer than the rank, this is an explicit Shape error naming both lengths — NumPy would silently promote the tensor’s rank instead, which matten treats as the surprising direction, not the safe one: the result would have more dimensions than the input, with no obvious place for a caller to look. This is a deliberate, one-directional divergence from NumPy (see below). reps must be non-empty; a 0 entry returns a zero-length result on that axis (RFC-111), rather than an error.

meshgrid

Tensor::meshgrid(x: &Tensor, y: &Tensor) -> (Tensor, Tensor)
Tensor::try_meshgrid(x: &Tensor, y: &Tensor) -> Result<(Tensor, Tensor), MattenError>

Builds the two coordinate grids for evaluating a function of two variables over a grid. x and y must both be rank-1 (a rank-2 input is a Shape error, never silently flattened). For x of length m and y of length n, both outputs have shape [n, m], using NumPy’s xy indexing:

out_x[i][j] == x[j]     (each row is a full copy of x)
out_y[i][j] == y[i]     (each row is constant, equal to y[i])
x = [1, 2, 3]        (len 3)
y = [10, 20]         (len 2)

meshgrid(x, y) -> both outputs shape [2, 3]

out_x = [[1, 2, 3], [1, 2, 3]]
out_y = [[10, 10, 10], [20, 20, 20]]

xy is used deliberately, matching NumPy’s default, even though the alternative ij convention (out[i][j] == (x[i], y[j])) can feel like the more natural matrix reading. When x and y have equal length, xy and ij differ only by a transpose — an invisible mistake with no shape error to catch it — so this matches the ecosystem instead of diverging on an axis a caller cannot see. A reader who specifically wants ij gets it by transposing both outputs.

The tile/meshgrid divergence principle

tile’s rank-promotion rejection and meshgrid’s NumPy-matching xy indexing look like opposite choices — one matches the ecosystem, one does not — but both follow the same rule:

MATCH the ecosystem when a divergence would be SILENT — wrong numbers, or a
      wrong shape the caller cannot see.                (meshgrid's indexing)

DIVERGE where the ecosystem's own behaviour is itself implicit, the divergence
      surfaces as an explicit error, and the error teaches.   (tile's rank promotion)

This is not a license to diverge generally — it is consistent with matten’s standing preference for explicit over silent behaviour.

Errors

Conditiontry_* returns
empty input list (concatenate/stack)InvalidArgument { argument: "tensors" }
any dynamic inputUnsupported (convert with try_numeric() first)
rank / dimension / shape mismatchShape
axis out of range (0..rank for concatenate, 0..=rank for stack/repeat_axis)Shape
empty reps (tile)Shape
reps longer than rank (tile), non-rank-1 input (meshgrid)Shape
repeat_axis on a rank-0 scalarShape
result exceeds the allocation limitAllocation

The convenience forms panic with the same message the try_* forms would return.

Allocation safety

The output shape is checked against MattenLimits before any data is copied, so an oversized result fails with Allocation (or Shape when the stacked rank would exceed the dimension limit) rather than attempting a huge allocation. repeat, tile, and meshgrid all multiply sizes and can overflow trivially, so every output size is computed with a checked product before allocating — never a bare *.

Example

See 14_concatenate_stack.rs for concatenate/stack, and 58_repeat_tile_meshgrid.rs for repeat/tile/meshgrid, for runnable walkthroughs.