invariants 1 and 5 as crate boundaries, a metric as one datum, and the engine on the tensor - #132
Merged
Conversation
`tangent_blade` returns the wedge of a simplex's edge vectors, which is a multivector of Λ^k(RR^N), and said so only in its doc comment while returning a bare `Vector`. It returns a `Tensor` now, contravariant because it is spanned by tangent vectors: it pushes forward, and Λ^k g measures it rather than the inverse. The type carries what the comment asserted. `num_minors` was a hand-rolled `exterior_dim`, zero-off-range logic and all, in the crate that depends on the one providing it. Two `binomial` calls computing the same blade count go the same way. None of this changes a number; it stops three places from being able to disagree about what dim Λ^k(RR^n) is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017vrTsAZUQySdXrVuXizLts
The exterior derivative, the wedge, the contraction, the transfer and both pairings involve no metric; the inner product, the Hodge star and the musical isomorphisms involve nothing else. That is invariant 5, and it was documentation. It is now the boundary between two crates. `multialgebra` drops its dependency on `gramian` and becomes exactly the metric-free algebra. `Factor::gramian` becomes `induced_form`, taking a bare bilinear form and returning one: an induced *form* needs only a form, and non-degeneracy and signature are properties the algebra never asks about. `gramian` gains the dependency and, with it, everything that needs a metric: `variance_gramian`, `slot_gramian`, `tensor_gramian`, the multi/multivector/multiform Gramians, the free `inner`, and a `TensorExt` carrying `norm`, `hodge_star`, `star` and `musical`. `inner` is free where the star is a method, for the reason the pairings already are: an inner product is symmetric in its arguments and privileges neither, while a star is a unary operator written postfix. The laws move with the operations. `multialgebra`'s suite keeps the metric-free ones and states them against plain matrices; the seven that need a metric are `gramian`'s now. The counts add back to what they were, which is the check that matters here: 457 tests before and after. `Tensor::apply_to_slot` becomes public, being what the star and the musicals need from one crate up, and `slots_mut` with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017vrTsAZUQySdXrVuXizLts
Invariant 1 says a complex is combinatorics and a geometry is a second input, and that the separation is mathematical rather than a preference. It was documentation. It is now the boundary between two crates, as invariant 5 became the boundary below it. `simplicial` keeps the topology and the atlas and has no notion of length: incidence, orientation, homology, barycentric charts, transition maps, quadrature, refinement. `regge` takes the geometry: `MeshLengthsSq` as the primitive, `MeshCoords` and `CellGramians` as sources, the per-simplex metrics they derive, the meshers that produce coordinates, and the mesh formats, which are a topology and its coordinates together. The split runs through the meshers too. A Kuhn triangulation is combinatorics -- which simplices, in which vertex order, from the per-axis cell counts alone -- and the vertex order is what makes uniform refinement compose, so `CartesianTopology` belongs with the topology and `CartesianGrid` wraps it to place the vertices. What reaches down does so as an extension, a free function being the other option and method syntax carrying these better: `SimplexCoordsExt` for the metric an ambient realization induces, `SubdivisionExt` for the geometric half of a refinement, `BoundaryComplexExt` for the geometry a boundary inherits. `io::cbor` stays below, being format-agnostic; `io::gmsh` moves up, a mesh file being coordinates as much as cells. The tests are what made the boundary real. `simplicial`'s own fixtures are now combinatorial: a 2-sphere is the boundary of a tetrahedron, and the annulus is a grid with its middle box removed *by index* rather than by barycenter. So the topology crate tests itself with no coordinate anywhere, and needs no dev-dependency on `regge` -- which is as well, since a dev-dependency cycle does not work here: Cargo permits it, but the cfg(test) build of `simplicial` is a distinct crate instance and its types do not unify with the one `regge` links. 459 tests, up from 457: the combinatorial grid brought two of its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017vrTsAZUQySdXrVuXizLts
Review fixes on the two crate boundaries this branch draws. `simplicial` still depended on `gramian`, for one function: the L^2 Gramian of the barycentric coordinate functions on the reference cell, whose only consumer is `formoniq` three crates up and which both call sites immediately unwrapped to a matrix. So the topology crate transitively linked the whole exterior algebra, and the boundary was asserted in a commit message rather than in a manifest. `unit_bary_gramian` hands back a bare matrix and the edge is gone: a reference datum is not a metric. `slots_mut` is replaced by `Tensor::dualize_slot`. Flipping a slot's variance is metric-free -- variance has no representational footprint, so there is nothing to compute and the components are untouched -- which makes it algebra and puts it below, where `musical` composes it with the application of g. Its doc comment claimed a debug check on the borrow that no code performed; there was no Drop guard and no assertion anywhere. A stated invariant that nothing enforces is worse than none. `from_simplex_and_coords` becomes the free `simplex_coords`. It is an associated function with no receiver, so there was no method syntax for an extension trait to carry, which is the only reason to prefer one. CLAUDE.md caught up in three places the previous commits missed: the `multialgebra` row still claimed the star and the musicals, invariant 4 still named `Variance::gramian` and `Slot::gramian`, and the `SimplexCoords` passage still said `geometry::coord`. 460 tests, up from 459: dualizing a slot is an involution on the description that leaves the components alone, and it is what makes the duality pairing defined. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The crate is renamed from `gramian` to `metric`, and `Gramian` and `Metric` become one type. They were never two things: `Metric` held a `Gramian` and its inverse, and every one of its methods forwarded to the first of the two. A Gram matrix is a metric expressed in a basis, so naming the crate after the representation understated it, and naming a second type after the pair documented a cache as though it were a concept. `Metric` now carries a variance beside its matrix. Covariant is the metric tensor g, which measures vectors; contravariant is g^-1, which measures covectors. `dual` is the exact passage between them and `measuring` asks for the one that measures a given variance, so the recurring question of whether a computation needs g or g^-1 is answered by the type rather than by a caller indexing into a pair. Since each determines the other, either may be handed to an operation and the answer is the same: there is nothing to get wrong, and nothing stored that can fall out of step. Dropping the eager inverse is also strictly less work where only g is wanted. The covector Gramian had exactly one consumer, the induced metric on multiforms, and every other cell of a mesh paid for it: `cell_volume` and the curvature inverted a matrix per cell and discarded it. `variance_gramian` and `slot_gramian` are gone, being the choosing the type now does. `multi_gramian` is `Metric::induced`, one method over both families rather than an exterior-only free function, and `multiform_gramian`/`multivector_gramian` are one line each on top of it, naming the two sides. `SimplexLengthsSq::metric` and `to_metric_tensor` collapse into one for the same reason the types did: the second was the polarization identity, the first wrapped its result into the pair. `regge::metric` becomes `regge::lengths`, which is what it holds -- the squared edge lengths and the per-cell metrics they materialize into -- and it pairs with `regge::coord` in the same style. Two tests the merge made vacuous are replaced by the laws that carry the content: `dual` is an involution flipping the variance and `measuring` returns the same metric from either side, and the dual and the pullback do *not* commute, asserted as a genuine inequality so the contract on `pullback` is checked rather than stated. 460 tests, unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The unnormalized symmetric basis is not self-dual, and two operations that dualize were treating it as though it were. x^alpha sums over all k! orderings, so ||x^alpha||^2 = alpha! and the dual of x^alpha is x^alpha / alpha!. So the duality pairing on packed symmetric components is not the plain dot product, and the matrix of a pullback is not the bare transpose of the functor: both pick up alpha!, the pairing as a weight and the pullback as a conjugation D^-1 (F A)^T D. Both were written without it. Neither was visible from the tests, and for a structural reason worth stating: every alpha! is 1 without repetition, so on Lambda^k the basis *is* self-dual and both formulas are correct as written. `to_free` commutes with pullback and pullback is adjoint to pushforward were the two laws that would have caught this, and both were swept over the alternating family alone. They are swept over both families now, over rectangular maps as well, and each is stated with what it does and does not pin down. The two fixes are one fact and they check each other: adjointness holds exactly because the alpha! the pairing adds cancels the D^-1 the pullback divides by. Getting either alone wrong fails the law. `basis_multiplicity` is the one place the multiplicity is computed, and it does not spell out a factorial: it reads the diagonal of `Factor::induced_form` of the identity, which is alpha! by definition of the induced form. One convention, one place, any degree, both families. Found while expressing a metric as the Sym^2 element it is, where the round trip through `to_free` was self-consistent and the pullback was not. 464 tests, up from 460. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`Metric::tensor` and `Metric::from_tensor`, the two directions of an identity rather than a conversion: a symmetric bilinear form on V *is* an element of Sym^2(V*), and one on V* an element of Sym^2(V), so the variance is the slot's and nothing is chosen. The matrix stays the representation, computed against rather than replaced, because `det`, `dual` and the pullback are matrix operations with optimized implementations and they run per cell. What it buys is that the pullback stops being written twice. `Metric::pullback` is a hand-rolled J^T g J beside `Tensor::pullback`, which is the same functor, and a law now says so. That is the point of the identity: not storage, which is unchanged, but one implementation where there were two and a machine-checked statement that the metric transforms the way a covariant 2-tensor transforms. The symmetric multiplicity is handled and never written down. `tensor` divides each component by alpha! and `from_tensor` reads the entries back off `Tensor::to_free`, so the two directions cannot disagree about the convention, and the weights come from `Factor::induced_form` of the identity rather than from a literal 2. Four laws: - the round trip, in both variances, dimension 0 through 4, on a probe with distinct entries, since equal entries hide a wrong multiplicity; - the multiplicity is *real*, the packed diagonal being half the matrix diagonal, so the round trip cannot pass by ignoring the convention entirely; - the metric pullback is the tensor pullback; - g(v, w) is the pairing of g against the symmetric product of v and w, checked with v != w, where a stray factor of two survives. The third and fourth are what failed first, and they were right to: the fix is the preceding commit. 464 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`unit_edge_squares`: the symmetric square of each of a simplex's edge vectors, which is a basis of Sym^2 indexed by the edges. The dimension count is not a coincidence. An n-simplex has binom(n+1,2) = n(n+1)/2 edges and dim Sym^2(R^n) = n(n+1)/2, the same number, and the squares are independent, so they are a basis. A squared edge length is therefore a component of the metric in the dual of that basis, s_e = <g, u_e (.) u_e>, and `SimplexLengthsSq::metric` is that change of basis -- the polarization identity being what it looks like written out rather than a formula of its own. A law says so now, over every signature, so the two readings cannot drift. The payoff is that invariant 2's totality over grades stops being a property of the code and becomes a property of the basis. A face's edges are a subset of the simplex's, so restricting the geometry to a face is *selecting the components at that face's edge indices*, with nothing computed. In the cartesian frame the same restriction is a pullback J^T g J. A law asserts the two agree, which is the statement that the edge basis is the one adapted to the face lattice, and that is why geometry is defined on every simplex while a chart exists only on a cell. The edge squares are metric-free and a function of `dim` alone, like every other reference datum. They live here rather than in `simplicial::atlas` because naming Sym^2 needs the algebra, and `simplicial` does not depend on it -- the concept floats up to the crate that joins the simplex's edges to the algebra, which is this one. Refinement needed no change to become the functor: it goes through `Metric::pullback`, which the preceding commit's law pins to `Tensor::pullback` on the Sym^2 reading. The module doc says so where a reader needs it. 467 tests, up from 464. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The multiplicity was correct and anonymous. Now it has a name, and the name says which basis, so the rule is carried by the API instead of by a paragraph. `Tensor::reciprocal` reads the components in the basis dual to the stored one, `Tensor::from_reciprocal` builds a tensor from them, and the two dualizing operations go through them: the duality pairing is one side's reciprocal components dotted with the other's stored ones, and a pullback -- the adjoint of a pushforward, hence a pairing in disguise -- transports the reciprocal components through the transpose of the functor and lands in the reciprocal basis of the domain. `basis_multiplicity` is private now: the only ways to spend it say which basis they mean, which is what stops a third dualizing operation from quietly not spending it. The trade the two bases make is the actual content, and both are rational. The stored basis is *multiplicative*, x^a x^b = x^(a+b) and e_I ^ e_J = +-e_(I u J) with unit coefficients, and that is what lets Lambda and Sym be one construction. Its price is that a symmetric slot is not self-dual, ||x^a||^2 = a!. The reciprocal basis pays the reverse: the pairing is free, products carry a multinomial. Only an *orthonormal* basis would need sqrt(a!), which would put an irrational into a representation that is otherwise exact, and none is used here. The reciprocal basis turns out to be a metric's home. A metric's components there are its matrix entries, exactly, with no weight anywhere -- which is what a bilinear form is, an object that eats vectors, hence a dual one. So `Metric::tensor` and `from_tensor` are now stated in it and the 1/alpha! division the previous form spelled out by hand is gone, along with the detour through `to_free`. `to_free` stays as the independent check in the law that pins the multiplicity, which is the right division of labour: the implementation uses one route, the test the other. No numerical change. 467 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`AffineTransform<From, To>` is tagged by its two coordinate spaces, so invariant 3 reaches the morphisms and not only the points. Composition demands a shared middle space, and `pseudo_inverse` returns an `AffineTransform<To, From>`, which is the chart/parametrization distinction stated as a type rather than as a naming convention: `SimplexCoords::affine_transform` is a parametrization `LocalCartesian -> S`, and its inverse is the chart. `apply_forward` and `apply_backward` take and return `Coords`, so the tagging holds across the map instead of stopping at either end of it. The linear part stays a bare matrix, deliberately: it maps displacements, which this crate leaves untagged because the difference of two points of one space is a tangent vector rather than a point. `then` composes and `identity` is its unit, with laws for both, so an atlas can be stated in the category the charts live in. Not made a `Tensor`. A mixed tensor over two spaces would carry the dimensions at runtime, where the type parameters carry the *spaces* at compile time, and compile time is the whole content of invariant 3. It would also cost `coorder` a dependency on the algebra that a typed affine map does not need. 468 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`tensor_gramian` returns a bare matrix. It measures each slot by that slot's own variance, so a mixed tensor's Gramian has Kronecker factors from g *and* from g^-1 and no single variance describes the result. Typing it as a `Metric` forced a variance to be supplied, and the value supplied was `Covariant` unconditionally: right for a contravariant tensor, wrong for a multiform, and inert either way because only the matrix was ever read. That is exactly the constructor invariant 4 forbids, and the guess was plausible. Nothing about the object is a metric on the underlying space. It is the Gram matrix of a basis, which is all an inner product needs, so `inner` forms the quadratic form directly and `TensorExt::gramian` hands back the matrix. A law pins it: on a uniform tensor the Gramian agrees with the named per-family metric, compared as matrices so neither side claims a variance; on the mixed shape an endomorphism has, it is the Kronecker product of the multiform and multivector metrics, with an assertion that those two genuinely differ, so the law says something. 469 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Padding the ambient space with Euclidean axes does not change what the inner product measures, so the padded metric takes its variance from the one it extends rather than restating a literal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q41APHpQjiWdbY4re8RQSa
Every element integral picked a metric on Lambda^k by hand and paired raw coefficient vectors under it. That is right only on the alternating family, and by accident: the stored basis is multiplicative, so a component pairing is the inner product exactly where every alpha! is 1. On a symmetric slot it is off by those factorials, which is what `Factor::induced_form` accounts for with the permanent, and what `metric::tensor::inner` is the one place to know. P^-_r Lambda^k is Sym^r tensor Lambda^k, so the old form had a wrong answer waiting at higher order. Also gives `Tensor::scalar` the inverse it lacked, `as_scalar`, so a one-dimensional tensor is read as a number by name rather than by indexing component zero. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q41APHpQjiWdbY4re8RQSa
The wedge of a frame's columns was built twice, in `derham::project` for a face of the reference cell and in `formoniq::hx` for a simplex in the ambient, each spelling out `exterior_power(..).column(0)` and wrapping the result by hand. It is one construction and it needs neither a mesh nor a metric, so it belongs on `Tensor`: the image of the generator of Lambda^k(R^k) under the frame, hence functorial rather than a fold. Both call sites now say what they mean, and the law is stated over both readings: the blade is the fold of the wedge, it transports along a map by Cauchy-Binet, and it vanishes exactly on a dependent frame. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q41APHpQjiWdbY4re8RQSa
Four places held a compound matrix and applied it to raw components: the face trace, the Whitney shape function and its differential, and the prolongation's child Jacobian. Each was a pullback written out, and each was a pullback only on the alternating family. A pullback is the adjoint of a pushforward, so it acts on the reciprocal basis; the stored basis is multiplicative and self-dual on Lambda alone, so a bare transpose is the right answer there and off by alpha! on a symmetric slot. `Transport` is that functor materialized for a fixed shape, uniform over the slots, and `Tensor::pullback`/`pushforward` are now one-shot uses of it rather than a parallel implementation. The two shape-function sites also stop rebuilding the compound matrix at every quadrature node. A transport carries a variance rather than imposing one, which is what lets one stored functor serve both directions; the adjointness law over Lambda tensor Sym is what caught it doing otherwise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q41APHpQjiWdbY4re8RQSa
The glyph mark and the vtu exporter pushed a sharped field forward by multiplying its raw components with a cell's linear part. That is the pushforward only because the field is grade 1, where Lambda^1 A = A; going through the functor says what the operation is and checks the variance it needs. The solenoidal probe took the Euclidean norm of a vector field's components in each cell's own frame, which is its magnitude only where that cell's metric is the identity. It now measures with the cell metric, so the normalization no longer drifts with cell shape. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q41APHpQjiWdbY4re8RQSa
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q41APHpQjiWdbY4re8RQSa
The Gram matrix of a tensor product is the product of the Gram matrices, and the functor of a map is the product of the per-slot functors. Both were built with `kronecker` and then applied once, which allocates (prod d_i)^2 entries out of matrices holding sum d_i^2 and does the matching work. `apply_factorwise` is the multilinear product evaluated slot by slot, costing (prod d_i)(sum e_i) and forming nothing. `inner` and `Transport` now go through it; `tensor_gramian` and `Transport::to_matrix` remain for callers that need a matrix as a matrix, and say so. No change on a single slot, which is every tensor in the engine today: a Kronecker product of one factor is that factor. The cost is latent until Sym^r tensor Lambda^k, which is exactly what the trimmed complex is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q41APHpQjiWdbY4re8RQSa
Not forming a Kronecker product is now what the algebra does everywhere, so it no longer distinguishes this routine. The sparsity of C does, and it belongs to the Whitney basis rather than to the tensor product, which is why a generic factored application would be slower here rather than faster. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q41APHpQjiWdbY4re8RQSa
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q41APHpQjiWdbY4re8RQSa
The formula appeared twice, in the explicit matrix and inline in the pullback of a bilinear form, kept in agreement only by the test comparing the two. `column` is now the single definition and both readings consume it, so that test states a law (the factored evaluation equals the formed sandwich) rather than that two copies of one formula match. Folding the k! into the column also makes the (k!)^2 of a pullback a consequence instead of something squared by hand. The row layout now comes from `tensor_strides` on the codomain's shape, where it had been the same arithmetic written out. Getting that order backwards yields a matrix of the right shape and the wrong meaning, and `factorwise_kronecker` is where the convention is defined. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q41APHpQjiWdbY4re8RQSa
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q41APHpQjiWdbY4re8RQSa
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Invariants 1 and 5 are crate boundaries, and a metric is one datum.
Twenty-two commits, each passing fmt, clippy, test and doc. 473 tests.
The two boundaries
Invariant 5,
multialgebraagainstmetric. The wedge, the contraction, thetransfer and both pairings need no metric. The inner product, the Hodge star and
the musical isomorphisms need nothing else. So
multialgebrais exactly themetric-free algebra and
metricis everything above it that needs one: theinduced metrics, the free
inner, and aTensorExtcarryingnorm,hodge_star,star,musical. An operation's crate says what it depends on.Factor::induced_formtakes a bare bilinear form and returns one, because aninduced form needs only a form; non-degeneracy and signature are properties the
algebra never asks about.
Invariant 1,
simplicialagainstregge. A complex is combinatorics and ageometry is a genuinely second input.
simplicialholds the topology and theatlas and has no notion of length.
reggeholdsMeshLengthsSqas theprimitive,
MeshCoordsandCellGramiansas sources, the per-simplex metricsthey derive, the meshers, and the mesh formats.
The split runs through the meshers. A Kuhn triangulation is combinatorics, which
simplices in which vertex order from the per-axis cell counts alone, and that
vertex order is what makes uniform refinement compose.
CartesianTopologybelongs with the topology;
CartesianGridwraps it to place the vertices.What reaches down does so as an extension:
SimplexCoordsExtfor the metric anambient realization induces,
SubdivisionExtfor the geometric half of arefinement,
BoundaryComplexExtfor the geometry a boundary inherits.io::cboris format-agnostic and sits below;
io::gmshsits above, a mesh file beingcoordinates as much as cells.
Both boundaries hold in
Cargo.toml, which is the point of making themstructural:
multialgebradepends on neither the metric nor a mesh, andsimplicialon neither the metric nor the algebra. A reference datum typed as ametric would reintroduce the edge, so
unit_bary_gramianhands back a barematrix.
simplicialalso tests itself with no coordinate anywhere: a 2-sphere is theboundary of a tetrahedron, and the annulus is a grid with its middle box removed
by index. That is what makes the boundary real rather than declared, and it is
load-bearing rather than tidy, because a dev-dependency cycle does not work here.
Cargo permits one, but the
cfg(test)build ofsimplicialis a distinct crateinstance and its types do not unify with the one
reggelinks.One metric, and its dual
Metricis a non-degenerate symmetric bilinear form of any signature expressedin a basis, carrying a variance beside its matrix. Covariant is the metric tensor
g, measuring vectors; contravariant is g⁻¹, measuring covectors.
g and g⁻¹ are one datum.
dualis the exact passage between them,measuringasks for the one that measures a given variance, and
on_slotfor the one thatmeasures a slot. Nothing chooses between them by hand and no stored pair can fall
out of step. Since each determines the other, either may be handed to an
operation and the answer is the same, so passing the wrong one is not a failure
mode, only an inversion.
Riemannian and Lorentzian are one signature-parameterized type. The
signature-sensitive operations, the volume factor and the causal trichotomy, read
the signature off the metric.
Metric::inducedis one method over both families: Λᵏg measures multivectorswhen g measures vectors, so the variance is inherited rather than chosen.
multiform_metricandmultivector_metricare one line each on top of it,naming the two sides.
A metric is the Sym² element it is
A symmetric bilinear form on V is an element of Sym²(V*), and one on V* an
element of Sym²(V).
Metric::tensorandfrom_tensorare the two directions ofthat identity.
The dense matrix stays the representation:
det,dualand the pullback arematrix operations with optimized implementations, and they run per cell. What the
identity buys is that the pullback exists once.
Metric::pullbackisTensor::pullbackon the Sym² reading, stated as a law, so refining a geometry isthe same operation as transporting any other covariant tensor, and refinement
inherits it by going through
Metric::pullback.pullbackpreserves the variance. The dual and the pullback do not commuteunless the map is orthogonal, so the dual is taken after pulling back, never
before, and a law asserts the two orders genuinely differ.
Edge lengths are Sym² in the edge basis
unit_edge_squaresis the symmetric square of each of a simplex's edge vectors:a basis of Sym² indexed by the edges. An n-simplex has binom(n+1,2) = n(n+1)/2
edges and dim Sym²(Rⁿ) = n(n+1)/2, and the squares are independent.
So a squared edge length is a component of the metric in the dual of that basis,
s_e = ⟨g, u_e ⊙ u_e⟩, andSimplexLengthsSq::metricis that change of basis,with the polarization identity as its written-out form.
This is why invariant 2's totality over grades is a property of a basis rather
than of the code. A face's edges are a subset of the simplex's, so restricting the
geometry to a face is selecting the components at that face's edge indices, with
nothing computed, where the cartesian frame needs a projection JᵀgJ. A law
asserts the two agree. The edge basis is the one adapted to the face lattice,
which is why geometry is defined on every simplex while a chart exists only on a
cell.
The stored basis is multiplicative, hence not self-dual
x^α x^β = x^(α+β)ande_I ∧ e_J = ±e_(I∪J)with unit coefficients, which iswhat lets Λ and Sym be one construction. The price is
‖x^α‖² = α!on asymmetric slot, so the basis is orthogonal but not self-dual.
Everything that dualizes a slot therefore reads
Tensor::reciprocal, thecomponents in the basis dual to the stored one, and lands through
Tensor::from_reciprocal. The duality pairing is one side's reciprocalcomponents dotted with the other's stored ones. A pullback is the adjoint of a
pushforward, hence a pairing in disguise, so it transports the reciprocal
components through the transpose of the functor and lands in the reciprocal basis
of the domain; on a symmetric slot that is the transpose conjugated by α! rather
than the bare transpose.
basis_multiplicityis private. The only ways to spend the weights say whichbasis they mean, so they cannot be applied by hand.
Both bases are rational. Only an orthonormal basis would need √(α!), which would
put an irrational into a representation that is otherwise exact, and none is
used. The reciprocal basis is a metric's home: its components there are its
matrix entries, with no weight anywhere, which is what a bilinear form is, an
object that eats vectors.
Every α! is 1 on Λᵏ, where the two bases coincide. A law that dualizes is
therefore swept over both families, and
to_freecommutes with pullback andpullback is adjoint to pushforward are swept over both, and over rectangular
maps. Each pins one half of the multiplicity and they check each other:
adjointness holds exactly because the α! the pairing adds cancels the α! the
pullback divides by.
Anything relying on the pairing or the pullback of a symmetric slot changes
numerically here, which is worth checking when #131 rebases, that being where
Sym is exercised (
P⁻_r Λᵏ = Symʳ ⊗ Λᵏ).Typed affine maps
AffineTransform<From, To>is tagged by its two coordinate spaces, so invariant 3reaches the morphisms and not only the points. Composition demands a shared middle
space and
pseudo_inversereturns the map the other way round, which is thechart/parametrization distinction stated as a type rather than as a naming
convention. The linear part stays a bare matrix, deliberately: it maps
displacements, and the difference of two points of one space is a tangent vector
rather than a point.
Not made a
Tensor. A mixed tensor over two spaces would carry the dimensions atruntime where the type parameters carry the spaces at compile time, and compile
time is the whole content of invariant 3.
The engine speaks the tensor
With the algebra settled, the crates above it stop reaching past it. The rule is
that a tensorial computation runs on the tensor, and the storage may be whatever
it needs to be as long as it offers a conversion.
This is not tidiness. Because the stored basis is multiplicative, every
component-level shortcut is exactly right on Λ and silently off by α! on Sym, so
each one is correct until
Symarrives, whichP⁻_r Λᵏdoes, and fails as aplausible wrong number rather than as a mismatch anything catches.
paired raw coefficient vectors under it,
fe_l2_erroramong them. They now gothrough
metric::tensor::inner, which accounts for the multiplicity once,inside
Factor::induced_form, by the permanent on a symmetric factor.Tensor::blade_of. The wedge of a frame's columns was built twice, inderham::projectfor a face of the reference cell and informoniq::hxfor asimplex in the ambient. One constructor now, functorial rather than a fold, with
the law that it vanishes exactly on a dependent frame.
Transport. Four places held a compound matrix and applied it to rawcomponents: the face trace, the Whitney shape function and its differential, and
a refinement child's Jacobian. Each was a pullback written out, and a pullback
dualizes, so a bare transpose is right on Λ alone.
Transportis that functormaterialized for a fixed shape, and
Tensor::pullback/pushforwardare one-shotuses of it rather than a parallel implementation. It carries a variance rather
than imposing one, which is what lets one stored functor serve both directions.
Two of the sites also stop rebuilding the compound matrix at every quadrature
node.
product is the product of the Gram matrices and the functor of a map is the
product of the per-slot functors, so both are held as their factors and applied
slot by slot.
tensor_gramianandTransport::to_matrixremain for a callerthat needs a matrix as a matrix. No measurable change on a single slot, which is
every tensor in the engine today; the cost is latent until
Symʳ ⊗ Λᵏ.A
Tensoris for the geometric space and never for the space of unknowns. Itmodels the tangent and cotangent space of a point and their powers, sized by the
manifold's dimension and fixed once that is, where a degree-of-freedom space is
sized by the mesh and the polynomial degree and has no grade, no variance and no
wedge. So cochains, load vectors, element matrices and assembled operators stay
linear algebra, however much a bilinear form on them resembles a tensor, and the
map between the two kinds is an ordinary matrix.
Two fixes fell out of the sweep. The glyph mark and the
.vtuexporter pushed asharped field forward by multiplying its components with a cell's linear part,
which is the pushforward only at grade 1. And the solenoidal probe took the
Euclidean norm of a vector field's components in each cell's own frame, which is
its magnitude only where that cell's metric is the identity, so its normalization
drifted with cell shape.
Not in scope
componentsisf64; complex-valued forms are whattime-harmonic problems need.
defect; the general case needs dihedral angles between facets around a hinge and
the hinge bivector in Λ²(V).
Metrickeeps a dense matrix rather than packed Sym² storage, deliberately: atdegree 2 the redundancy is 2× while
det,dualand JᵀgJ are matrixoperations with optimized implementations.