Skip to content

multialgebra: the tensor power and its two quotients, as one construction - #130

Merged
luiswirth merged 2 commits into
mainfrom
multialgebra-tensor
Jul 29, 2026
Merged

multialgebra: the tensor power and its two quotients, as one construction#130
luiswirth merged 2 commits into
mainfrom
multialgebra-tensor

Conversation

@luiswirth

@luiswirth luiswirth commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Closes #129.

Two commits, each standing on its own and each passing fmt, clippy, test
and doc. Higher-order FEEC is built on this and is #131, stacked on this
branch so it can be reviewed separately.

Λ^k, Sym^k and V^⊗k as one construction

Λ^k and Sym^k are the two quotients of V^⊗k by a character of S_k.
A character lands in an abelian group, so it factors through the
abelianization, and S_k^ab is Z/2 for k ≥ 2 — those two are the
complete list, not a pair someone picked. V^⊗k is the unquotiented
object above them, and all three are held here as one construction with a
single Symmetry carrying the distinction.

The representation follows the mathematics rather than being imposed on
it. The two quotients share one: the shift w_i ↦ w_i + i takes a weakly
increasing word to a strictly increasing one, so in shifted form a
multiset is a set and one bitset serves both — ranking, enumeration,
deletion and the complement become the same bit operations. The free
power has no symmetry to exploit and therefore no compression: its index
is a word ranked in the radix, carrying its alphabet because a positional
rank needs one.

That the free family costs more is its information content, not a defect,
and the type tells you which you are paying for.

What stays outside is Schur functors: R_ijkl = R_klij is cut out by
a higher-dimensional irrep of S_k, not by a character, and its basis is
no longer a word. Three independent things agree on that boundary — the
character classification, the monotone-word representation, and the
alphabet-independent rank.

Shape

struct Factor { symmetry, degree }            // the functor — no dimension
struct Slot   { factor, variance, dim }       // the functor, evaluated, with its side
struct Tensor { slots, strides, components }

Factor stays dimension-free, which is what lets Factor::induced
describe both ends of a rectangular map from one value. Slot carries
symmetry, variance and dimension, so a Tensor is slots plus strides
plus components — the shape a dense array has, with a symmetry and a
variance per axis on top. An all-free tensor is that array.

Two things fall out that were previously unexpressible:

  • the metric-free trace. tr on V* ⊗ V needs no inner product, but
    under a uniform variance it was reachable only by raising an index
    through g⁻¹ — a signature asking for more structure than the concept
    needs.
  • a rectangular map V → W as [Λ¹V*@n, Λ¹W@m], which previously
    had to live outside the algebra as a bare matrix.

Variance is therefore data and Tensor has no type parameter. It is the
one datum with no representational footprintdim Λᵏ(V) = dim Λᵏ(V*)
— so nothing derives it and no shape check catches a wrong one: the
operations check it, construction states it. Uniform variance is what
buys functoriality along an arbitrary map; a mixed tensor transports only
along an isomorphism, and pullback refuses it rather than pretending.

CLAUDE.md's invariant 4 is rewritten accordingly, and says plainly that
variance was type-level and deliberately is not any more.

multiindex restructured to match

It held three representations for two families: Combination a u64
bitset, Composition a heap-allocated parts vector, and MonoIndex
the shifted bitset that already represents both. The first two are now
newtypes over MonoIndex, which is what they always were — forbidding
repetition makes the shift zero, so the stored word is the set.

Combination reaches 128 indices rather than 64 (the cap that produced a
silent mesh-size bug), and Composition stops allocating.

The four families are the classical square, and the first three are the
bases of the three symmetry types:

no repetition repetition
order irrelevant Combination → Λᵏ Composition → Symᵏ
order relevant Permutation → the Sₖ action Word → V^⊗k

Operations

d and the Koszul operator are one operation, transfer, in its two
directions. Binary contraction is tensor then one trace per pair, so
multi-contraction is repeated tracing rather than a concept of its own,
and matrix composition and the duality pairing are that operation run to
different depths.

Pairings are free functions: a pairing is a bilinear map on two spaces
and privileges neither argument, which method syntax denies. Adds the
wedge pairing Λᵏ × Λⁿ⁻ᵏ → ℝ, the duality the exterior algebra
carries that is not the star — metric-free, needing only a top grade,
and nondegenerate. The star is exactly what turns it into the inner
product, ⟨α, ⋆β⟩_∧ = ⟨α, β⟩ vol, and that is where the metric enters.

to_free forgets each slot's symmetry, so Λ and Sym are visibly
compressed representations of subspaces of the free power. A test says it
commutes with pullback, which is what makes it a map of representations
rather than a serialization.

The exterior crate goes

ExteriorElement was a newtype whose invariant made hodge_star total,
and with variance as data MultiForm cannot even be an alias, since
Tensor<Covariant> would admit Sym³. Its free functions and its laws
move into multialgebra. The READMEs are updated with it: the root one
linked to crates/exterior/README.md, now deleted.

The discrete dualities

Chain lived in simplicial and Cochain in derham, and the two
never met. The pairing that makes C^k the dual of C_k is now there,
stated as the law that carries the content:

⟨dω, c⟩ = ⟨ω, ∂c⟩

discrete Stokes, and the reason the coboundary is the transpose of the
boundary. l2_pairing is the metric one beside it, with a test asserting
the two disagree on a nontrivial input — one needs the mass matrix
and a geometry, the other only the incidence.

Performance

Measured, not assumed. The element-level algebra started 5–8× slower than
the standalone implementation; three causes found by measurement (an
allocating enumerator, basis-tuple materialization, a Kronecker
intermediate) brought it to 1.4–1.7×, and end to end assembly is +2% in
3D with a no-algebra control confirming the harness.

Adding the free family then regressed the monotone path 1.3–4×, which
was invisible until benchmarked. Cause: an enum as wide as its largest
variant, so every alternating basis element paid for a family it never
used. Packing Word as its own radix rank (a word is a positional
number) made both variants the same size and recovered most of it —
contraction is back to ~1.1×, wedge sits at ~1.25×. A benchmark is now
committed so it cannot drift invisibly again.

What the tests caught

  • Degree-0 parity normalization was wrong, and it was my own proposal
    in this PR. A factor is a position in a shape, not the space it
    currently denotes: transferring into a degree-zero symmetric slot must
    give Sym¹, not Λ¹. The Koszul homotopy formula caught it.
  • The Lie derivative's velocity was covariant in three places and is
    contracted against a form, so it must be a vector. The new variance
    check caught it — exactly the construction-site hazard the design
    predicts.
  • contract/merge divided by zero on a trivial-space factor, caught by
    the Lie derivative test.
  • to_free must accumulate, not assign. On a symmetric slot several
    of the k! orderings land on one entry and their sum is the
    multiplicity; the law ⟨free(a), free(b)⟩ = (∏ kᵢ!)⟨a, b⟩ holds over
    both families only under the right convention.
  • The nilpotency guard I first wrote was itself wrong: ⟨dω, ∂c⟩ = ⟨ddω, c⟩
    is zero by the adjunction under test, so the guard disproved the
    theorem.

Every law was checked to be falsifiable before being trusted.

Not done

  • Generic scalars. components is f64; complex-valued forms are
    what time-harmonic problems need, and the Vector<T = f64> alias
    already anticipates it.
  • contract_with materializes the tensor product rather than fusing.
  • wedge is ~1.25× slower than before the free family. Removing the last
    enum branch means duplicated per-slot loops, which is not obviously
    worth it at that margin.

@luiswirth
luiswirth force-pushed the multialgebra-tensor branch from 9bd675a to cec83ff Compare July 29, 2026 13:19
@luiswirth luiswirth changed the title multialgebra: Λ and Sym as one construction, and higher-order FEEC on it multialgebra: Λ and Sym as one construction, higher-order FEEC on it, and the dualities Jul 29, 2026
@luiswirth
luiswirth force-pushed the multialgebra-tensor branch from cec83ff to c62ae74 Compare July 29, 2026 13:34
@luiswirth luiswirth changed the title multialgebra: Λ and Sym as one construction, higher-order FEEC on it, and the dualities multialgebra: Λ and Sym as one construction, and the dualities Jul 29, 2026
@luiswirth
luiswirth force-pushed the multialgebra-tensor branch from c62ae74 to 802e9ff Compare July 29, 2026 14:35
luiswirth and others added 2 commits July 29, 2026 18:45
…tion

Λ^k and Sym^k are the two quotients of V^⊗k by a character of S_k. The
characters factor through the abelianization, which is Z/2, so those two
are the complete list rather than a pair someone picked, and the free
power is the unquotiented object above them. All three live here as one
construction, with a single `Symmetry` carrying the distinction and every
operation written once over them.

The representation follows the mathematics rather than being imposed on
it. The two quotients share one: the shift w_i -> w_i + i takes a weakly
increasing word to a strictly increasing one, so in shifted form a
multiset is a set and one bitset serves both, with ranking, enumeration,
deletion and the complement becoming the same bit operations. The free
power has no symmetry to exploit and therefore no compression, and its
index is a word ranked in the radix, carrying its alphabet because a
positional rank needs one. That the free family costs more is its
information content.

`multiindex` is restructured to match. It had three representations for
two families: `Combination` a u64 bitset, `Composition` a heap-allocated
parts vector, and `MonoIndex` the shifted bitset that already represents
both. The first two become newtypes over `MonoIndex`, which is what they
always were, so they enforce the family without adding to the
representation. A `Combination` then reaches 128 indices rather than 64,
and a `Composition` stops allocating. The four index families are the
classical square: `Combination` and `Composition` for order irrelevant
without and with repetition, `Permutation` and `Word` for order relevant,
and the first three are the bases of Λ^k, Sym^k and V^⊗k.

A tensor is a list of slots. `Factor` is the functor, with no dimension,
which is what lets `Factor::induced` describe both ends of a rectangular
map from one value. `Slot` is that functor with the space it is over and
the side it is built from, so symmetry, variance and dimension are all
per slot. A `Tensor` is then its slots, its strides and its components:
the shape a dense array has, with a symmetry and a variance per axis on
top, and an all-free tensor is exactly that array.

Variance being per slot is what makes a mixed tensor expressible, and
with it the metric-free trace: tr on V* (x) V needs no inner product, and
a uniform variance could only reach it by raising an index through g^-1.
Dimension being per slot is what makes a rectangular map V* (x) W
expressible, which previously had to live outside the algebra as a bare
matrix.

Variance is therefore data, and `Tensor` has no type parameter. It is the
one datum with no representational footprint, since
dim Λ^k(V) = dim Λ^k(V*), so nothing derives it and no shape check
catches a wrong one: the operations check it and construction states it.
Uniform variance is what buys functoriality along an arbitrary linear
map, a mixed tensor transporting only along an isomorphism, and
`pullback` refuses it rather than pretending.

The `exterior` crate goes with the type parameter. `ExteriorElement` was
a newtype whose invariant made `hodge_star` total, and with variance as
data `MultiForm` cannot even be an alias, since `Tensor<Covariant>` would
admit Sym^3. Its free functions and its laws move here.

d and the Koszul operator are one operation, `transfer`, in its two
directions. Binary contraction is `tensor` then one `trace` per pair, so
multi-contraction is repeated tracing rather than a concept of its own,
and matrix composition and the duality pairing are that operation run to
different depths.

Pairings are free functions: a pairing is a bilinear map on two spaces
and privileges neither argument, which method syntax would deny. Adds the
wedge pairing Λ^k x Λ^(n-k) -> RR, the duality the exterior algebra
carries that is not the star: metric-free, needing only a top grade, and
nondegenerate. The star is exactly what turns it into the inner product,
<α, ⋆β>_∧ = <α, β> vol, and that is where the metric enters.

`to_free` forgets each slot's symmetry, so Λ and Sym are visibly
compressed representations of subspaces of the free power and the
embedding is a map of representations rather than a serialization: a test
says it commutes with pullback.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vrTsAZUQySdXrVuXizLts
Chain lived in simplicial and Cochain in derham, and the two never met.
A cochain assigns an integral to each simplex, so summing it against a
chain's coefficients integrates over the chain, and that pairing is what
makes C^k the dual of C_k rather than a space of the same dimension.

Stated as the law that carries the content,

  <d w, c> = <w, boundary c>,

which is discrete Stokes and the reason the coboundary is the transpose
of the boundary. Metric-free, orientation-free, geometry-free. Chain
gained the boundary operator it lacked, exact over ZZ since the incidence
coefficients are +-1.

`l2_pairing` is the other one, and the test says they disagree. One needs
the mass matrix and hence a geometry, the other only the incidence, so a
refactor routing either through the other now fails rather than looking
plausible.

The nilpotency test needed a guard that one step does not already vanish,
and the obvious guard is wrong: <d w, boundary c> = <d d w, c> is zero by
the adjunction being tested, so it pairs against a plain chain a grade up
instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vrTsAZUQySdXrVuXizLts
@luiswirth
luiswirth force-pushed the multialgebra-tensor branch from 802e9ff to bb1f33e Compare July 29, 2026 16:51
@luiswirth luiswirth changed the title multialgebra: Λ and Sym as one construction, and the dualities multialgebra: the tensor power and its two quotients, as one construction Jul 29, 2026
@luiswirth
luiswirth merged commit 9905ac9 into main Jul 29, 2026
5 checks passed
@luiswirth
luiswirth deleted the multialgebra-tensor branch July 29, 2026 18:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

multialgebra: Λ and Sym as one construction, and the tensor products of them

1 participant