Region views¶
On a raw torch.Tensor, t.unsqueeze(0) or torch.sum(t, dim=1)
address axes by absolute position — and once a tensor mixes batch,
sub-batch, and base axes, an absolute index is ambiguous and easy to
get wrong. NEML2 fixes this by exposing every shape-manipulating op
through a region view, so each reshape, reduction, or concatenation
names the region it acts on.
The views¶
Four properties select a region; each returns a fresh wrapper, so calls chain:
t.dynamic_batch— the dynamic batch region. Ops preservesub_batch_ndim.t.sub_batch— the sub-batch region. Ops adjustsub_batch_ndim.t.batch— the combined region, for read-only.shape/.ndimand forcat; shape-changing ops on it raise (see below).t.base— the base region. Read-only excepttransposeon square-base types (R2,SSR4).
Every mutable view exposes .shape, .ndim, .unsqueeze(dim),
.squeeze(dim), and .expand(*shape):
from neml2.types import SR2, R2
broadcast = SR2.fill(0.1, -0.05, -0.05, 0, 0, 0).dynamic_batch.expand(20)
print("dynamic_batch.expand(20) ->", tuple(broadcast.data.shape))
tr = R2.identity().base.transpose(-2, -1)
print("base.transpose(-2, -1) ->", tuple(tr.data.shape))
dynamic_batch.expand(20) -> (20, 6)
base.transpose(-2, -1) -> (3, 3)
Why t.batch can’t be reshaped¶
t.batch spans both batch regions at once, so a shape-changing op on it
would be ambiguous: should a new axis land in the dynamic region or the
sub-batch region? Rather than guess, those ops raise and tell you which
view to pick:
try:
SR2.zeros(3).batch.unsqueeze(-1)
except TypeError as err:
print(err)
batch.unsqueeze is ambiguous; use t.dynamic_batch.unsqueeze(...) or t.sub_batch.unsqueeze(...)
Sub-batch-only operations¶
The sub-batch view carries a few extra ops for building and collapsing
per-site structure — retag(n) / flatten() to change how many
trailing axes count as sub-batch, expand_at(size) to broadcast in a
new site axis, and diagonalize() to turn a per-site vector into a
per-site diagonal:
import torch
from neml2.types import Scalar
s = Scalar(torch.randn(5)).sub_batch.retag(1)
print("expand_at(3) ->", tuple(s.sub_batch.expand_at(3).data.shape))
print("diagonalize ->", tuple(s.sub_batch.diagonalize().data.shape))
print("flatten ndim ->", s.sub_batch.flatten().sub_batch_ndim)
expand_at(3) -> (3, 5)
diagonalize -> (5, 5)
flatten ndim -> 0
Disambiguating the free functions¶
This is where region views earn their keep. Reductions, concatenation,
and interpolation live as free functions in neml2.types, and they
take a region view instead of a dim= integer. The view tells the
function which region the axis index addresses — and lets it update the
wrapper’s metadata correctly (e.g. dropping sub_batch_ndim when a
sub-batch axis is reduced away).
sum, mean, and diff reduce or difference one region’s axis:
from neml2.types import sum, mean, diff, linspace, cat, stack
ramp = linspace(Scalar(300.0).sub_batch, Scalar(1200.0).sub_batch, 20)
print("ramp sub_batch_shape ->", tuple(ramp.sub_batch_shape))
total = sum(ramp.sub_batch, 0) # reduce the sub-batch axis
print("sum over sub_batch ->", tuple(total.data.shape), "sub_ndim", total.sub_batch_ndim)
avg = mean(ramp.sub_batch, 0)
print("mean over sub_batch ->", tuple(avg.data.shape))
delta = diff(ramp.sub_batch, n=1, dim=-1)
print("diff along sub_batch ->", tuple(delta.sub_batch_shape))
ramp sub_batch_shape -> (20,)
sum over sub_batch -> () sub_ndim 0
mean over sub_batch -> ()
diff along sub_batch -> (19,)
stack and cat combine along a region’s axis; the view fixes which
region grows:
from neml2.types import Vec
v0 = Vec.fill(6.0, 4.0, 0.0)
v1 = Vec.fill(8.0, 5.0, 0.0)
print("stack on dynamic_batch ->", tuple(stack([v0.dynamic_batch, v1.dynamic_batch]).data.shape))
a = Vec(torch.randn(2, 3))
b = Vec(torch.randn(3, 3))
print("cat on dynamic_batch ->", tuple(cat([a.dynamic_batch, b.dynamic_batch], dim=0).data.shape))
stack on dynamic_batch -> (2, 3)
cat on dynamic_batch -> (5, 3)
And linspace / logspace build a ramp along whichever region you pass
the endpoints as — the single v3 spelling that replaces v2’s separate
dynamic_linspace / intmd_linspace / base_linspace:
dyn = linspace(Scalar(0.0).dynamic_batch, Scalar(1.0).dynamic_batch, 5)
print("endpoints as .dynamic_batch -> new dynamic axis ", tuple(dyn.dynamic_batch_shape))
sub = linspace(Scalar(0.0).sub_batch, Scalar(1.0).sub_batch, 5)
print("endpoints as .sub_batch -> new sub-batch axis", tuple(sub.sub_batch_shape))
endpoints as .dynamic_batch -> new dynamic axis (5,)
endpoints as .sub_batch -> new sub-batch axis (5,)
See also¶
Indexing — region-scoped indexing follows the same view convention.
Tensor shape regions — the regions these views select.