Skip to content

Layouts, Storage, and Tensor Composition

A layout defines the coefficient space. Storage determines how its coefficients occupy a tensor's final axis. Leading axes remain ordinary PyTorch axes: they can describe samples, channels, grids, or any other application structure.

Choose the coefficient space

import torch
from clifra import TensorContract, make_algebra

algebra = make_algebra(3, 0)
vectors = algebra.layout((1,))
products = algebra.layout((0, 2))
assert vectors.dim == 3
assert products.dim == 4

Basis blades follow increasing bitmask order. Bit zero represents \(e_1\), bit one \(e_2\), and bit two \(e_3\). The canonical basis is therefore \(1,e_1,e_2,e_{12},e_3,e_{13},e_{23},e_{123}\), not grade order. A compact layout filters this ordering to its selected grades. The vector lanes are \(e_1,e_2,e_3\); the product lanes are \(1,e_{12},e_{13},e_{23}\).

Two arrays of the same width can mean different things. Width three could describe vectors or bivectors in this algebra, so operations need declarations rather than guessing a layout from tensor shape.

product = algebra.plan_product(
    op="geometric_product", left=vectors, right=vectors, output=products,
)
a = torch.tensor([1.0, 0.0, 0.0])
b = torch.tensor([0.0, 1.0, 0.0])
result = product(a, b)
assert torch.equal(result, torch.tensor([0.0, 1.0, 0.0, 0.0]))

Convert storage explicitly

Compact storage holds only the selected lanes. Canonical storage has \(2^n\) lanes, placing those same coefficients at their full-basis positions and zeros elsewhere. Conversion does not change the mathematical value.

canonical = products.full(result)
assert canonical.shape == (8,)
assert canonical[3] == 1  # e12 has bitmask 3
assert torch.equal(products.compact(canonical), result)

canonical_product = algebra.plan_product(
    op="geometric_product",
    left=TensorContract(vectors, storage="canonical"),
    right=TensorContract(vectors, storage="canonical"),
    output=TensorContract(products, storage="canonical"),
)
assert torch.equal(canonical_product(vectors.full(a), vectors.full(b)), canonical)

Passing a layout directly declares compact storage. TensorContract makes the storage choice explicit while retaining the same semantic layout. Prefer compact storage when the computation needs only a few grades; a canonical array allocates all basis lanes even when most are zero.

Compose leading dimensions

Broadcasting follows PyTorch's usual rules on every axis before the coefficient axis. A singleton axis can create all point-to-direction products with the same operation.

points = torch.tensor([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])
directions = torch.eye(3)
all_products = product(points[:, None, :], directions[None, :, :])
assert all_products.shape == (2, 3, 4)
assert torch.equal(all_products[0, 1], result)

# Pool over directions, leaving one multivector per point.
pooled = all_products.mean(dim=1)
assert pooled.shape == (2, 4)

Stacking, indexing, concatenating along sample axes, and reducing those axes are ordinary tensor operations. Changing the final axis can change the coefficient meaning, so it requires a matching layout rather than just a new shape. The next tutorial examines what the reusable product has fixed and what remains dynamic in Planned Differentiation.