Skip to content

formoniq: FEEC at any polynomial degree - #131

Draft
luiswirth wants to merge 3 commits into
mainfrom
higher-order-feec
Draft

formoniq: FEEC at any polynomial degree#131
luiswirth wants to merge 3 commits into
mainfrom
higher-order-feec

Conversation

@luiswirth

@luiswirth luiswirth commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Stacked on #130, which it needs for the tensor layer. Review that one
first; the base will retarget to main once it merges.

One commit, passing fmt, clippy, test and doc.

What it is

P⁻_r Λ^k is Sym^r ⊗ Λ^k in barycentric coordinates, so the trimmed
family needs no construction of its own: it is the tensor the algebra
already builds, read at a degree.

Barycentric coordinates are what make the coefficient factor homogeneous.
Σ λ_i = 1 supplies the missing variable, so
dim P_r(ℝ^n) = binom(n+r, n) = dim Sym^r(ℝ^(n+1)), no graded layer sits
above a homogeneous factor, and a polynomial degree is one number exactly
as a grade is.

The two structural operators are one operation in opposite directions.
d moves a degree from the coefficients to the blade, the Koszul
operator moves it back, and dκ + κd = (r+k) id on the homogeneous
pieces is what cuts out the trimmed spaces. Whitney forms fall out as
k! κ(dλ_σ), so the familiar deletion formula
W_σ = k! Σ_i (-1)^i λ_{σ_i} dλ_{σ∖σ_i} is a consequence of what κ does
to a blade rather than a definition.

The geometric decomposition

P⁻_r Λ^k(K) = ⊕_{f ⊆ K} int_f, a direct sum over every subsimplex, each
summand the piece whose support is all of f and no smaller face. That
is what makes the global space a subspace of HΛ^k rather than a direct
sum of unrelated cell spaces.

Two cells sharing a face give that face's degrees of freedom the same
global index with no communication and no orientation convention: a dof
is named by data intrinsic to f (a subsimplex of f and a monomial in
f's own barycentric coordinates, both in positions within f), and a
skeleton stores every simplex colex-sorted, so both cells see the same
order. Nothing here consults a CellOrdering.

Assembly

Through an explicit local-to-global map, so one routine serves every
degree and the first-order case, where a dof is a k-simplex, is that map
rather than a different mechanism.

The mass matrix is integrated, not quadratured. The integrand is
polynomial in the barycentric coordinates and their moments are the
closed form n! α! / (|α| + n)!, so there is no rule to be exact to and
no order to check. HodgeMassElmat is this at r = 1.

TrimmedComplex implements the same HilbertComplex, so every problem
written against that interface — source, eigenvalue, heat, wave, Dirac —
runs at any degree with no solver touched. tests/higher_order.rs
exercises all five at r = 2, each against a law rather than a stored
number.

Convergence

Hodge-Laplace eigenvalues on [0, π]^n under the absolute boundary
condition, against the exact bottom of the spectrum:

dim 3, grade 1 ndofs error rate
r=1 4913 1.2e-2 1.91
r=2 3665 4.4e-4 3.50
r=3 10345 1.8e-6 5.75

Rates come out at 2, 4 and 6 across dimensions and grades, matching the
predicted O(h^(2r)).

Compared against the exact eigenvalue rather than a Richardson
extrapolation on purpose: at r = 3 the error reaches roundoff within
two refinements, and a self-convergence estimate cannot tell that from a
bug. Getting the exact value right needed deriving it: the Hodge
Laplacian on the box splits into scalar Laplacians with a Dirichlet
factor on each direction in I and a Neumann factor on each outside it,
so the smallest nonzero eigenvalue is max(k, 1) — not the specific
eigenform's λ = n, which the first run wrongly compared against.

Assembly costs 2.1 ms, 13 ms and 77 ms for r = 1, 2, 3 on a fixed 3D
mesh; space setup 193 µs, 559 µs and 1.22 ms.

Ties to trusted code

The r = 1 case is identical, not merely close, and three tests say so:
the trimmed basis is pointwise the Whitney basis, the reference d is
exactly the simplicial coboundary with entries in {−1, 0, 1}, and the
assembled dof counts, mass matrices and differential equal the Whitney
complex's on real meshes.

Ported to the new tensor layer

PolyForm is Sym^r ⊗ Λᵏ over the barycentric ℝⁿ⁺¹, which is now
stated in the slots rather than in a tensor-wide dimension: both slots
carry cell_dim + 1 explicitly. That the coefficient and blade factors
live over the same enlarged space, and that at_bary leaves it by
pulling back along unit_difbarys, is visible in the types.

What the tests caught

  • The discrete d was scattered wrongly. It filled each column from
    a single cell, but a basis function's derivative is supported on all
    cells containing its dof, so the scatter now averages over the
    multiplicity. Caught by the r=1-equals-Whitney test.
  • The local-to-global map capped meshes at 64 vertices, by routing a
    Simplex through a 64-bit Combination of global vertex indices.
    Caught by the p-convergence run, not by a test — no small-mesh test can
    reach it.
  • Q_r ⊗ H was Kroneckered by hand under a comment asserting the
    order matched the tensor's strides, with nothing enforcing it.
    Reversing it yields a symmetric positive definite matrix of the right
    shape and the wrong meaning, so no shape check catches it. Now one
    function and one test.

Not done

A higher-order L² error against an analytic solution needs an interpolant
into P⁻_r Λ^k. The convergence study uses eigenvalues, which need none,
but a source-problem convergence table would want it.

luiswirth and others added 3 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
P^-_r Λ^k is Sym^r (x) Λ^k in barycentric coordinates, so the trimmed
family needs no construction of its own: it is the tensor the algebra
already builds, read at a degree. Barycentric coordinates are what make
the coefficient factor homogeneous, since sum_i λ_i = 1 supplies the
missing variable and dim P_r(RR^n) = dim Sym^r(RR^(n+1)), so no graded
layer sits above it and a polynomial degree is one number exactly as a
grade is.

The two structural operators are one operation in opposite directions.
d moves a degree from the coefficients to the blade, the Koszul operator
moves it back, and dκ + κd = (r+k) id on the homogeneous pieces is what
cuts out the trimmed spaces. Whitney forms fall out as k! κ(dλ_σ), so the
familiar deletion formula is a consequence rather than a definition.

The geometric decomposition is the direct sum over subsimplices, and it
is what makes a global space out of the local ones. Two cells sharing a
face give that face's degrees of freedom the same global index with no
communication and no orientation convention, because a dof is named
intrinsically to its attachment and a skeleton stores every simplex
colex-sorted. Nothing here consults a cell ordering.

Assembly goes through an explicit local-to-global map, so one routine
serves every degree and the first-order case, where a dof is a k-simplex,
is that map rather than a different mechanism. The mass matrix is
integrated, not quadratured: the integrand is polynomial in the
barycentric coordinates and their moments are the closed form
n! α! / (|α| + n)!, so there is no rule to be exact to.

`TrimmedComplex` implements the same `HilbertComplex` as the first-order
one, so every problem written against that interface runs at any degree
unchanged. `WhitneyComplex` is its r = 1 case and a test pins them
together on real meshes: the same dof counts, mass matrices and exterior
derivative.

Eigenvalue convergence comes out at the predicted O(h^(2r)), rates 2, 4
and 6 for r = 1, 2, 3 across dimensions and grades, measured against the
exact bottom of the spectrum rather than a Richardson extrapolation,
since at r = 3 the error reaches roundoff within two refinements and a
self-convergence estimate cannot tell that from a bug.

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 force-pushed the higher-order-feec branch from fb7eb12 to 62c438f Compare July 29, 2026 16:51
@luiswirth
luiswirth changed the base branch from multialgebra-tensor to main July 29, 2026 18:47
@luiswirth

Copy link
Copy Markdown
Owner Author

Parking this. #130 is in, and the next work is architectural: gramian moves above multialgebra so a metric is the Sym²V* tensor it is, and simplicial splits into topology and a new regge crate carrying the metric geometry — invariants 5 and 1 each becoming a crate boundary.

Both touch the layers this builds on, so rebasing this first would mean doing it twice. Revisiting once the ladder settles. The work is complete and green as it stands; nothing here is in doubt.

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.

1 participant