Skip to content

Development supporting p-multigrid - #4546

Draft
lindsayad wants to merge 13 commits into
develfrom
orientation-dev
Draft

lindsayad wants to merge 13 commits into
develfrom
orientation-dev

Conversation

@lindsayad

@lindsayad lindsayad commented Sep 10, 2026

Copy link
Copy Markdown
Member

p-multigrid (idaholab/moose#33645) has led me to exploring our higher polynomial order spaces and I (Claude to be clear) believe I've found some bugs in various orientation related things

lindsayad and others added 10 commits September 6, 2026 14:08
Importantly this both sets the wrapper as initialized
and instructs the wrapper not to destroy the PETSc
matrix on exit

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…package

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
positive_face_orientation() reports the direction in which a face's vertices
run, which is a single bit. A basis whose shape functions follow the order of a
face's vertices needs the whole ordering, including which vertex is the
lexicographically least, so that two elements sharing a face arrive at the same
shape function for a degree of freedom they share. That single bit leaves the
least vertex of a face with more than three vertices open.

face_orientation() carries the whole ordering, as twice the position that the
least vertex takes in the face's node map plus the direction bit, so it runs
over the six symmetries of a triangular face or the eight of a quadrilateral
face. positive_face_orientation() now reads its low bit. edge_orientation()
gives an edge's single bit the same spelling, for callers that index over edges
and faces alike.

The new ElemTest cases hold face_orientation() to agreeing with
positive_face_orientation() and to naming the least vertex, and hold two
elements sharing a face to the same ordering of that face's vertices, which is
the property that lets the index select a conforming shape function.

Co-Authored-By: Claude <noreply@anthropic.com>
A hierarchic basis of order three or above places degrees of freedom on the
faces of a 3D element, and the two elements sharing a face have to arrive at the
same shape function for each of them. Seven places did not.

HEX27, in cube_indices(): face 1 case 7 and face 5 case 8 rewrote the reference
coordinates with the transformation of another case, so two of the eight
symmetries of those faces produced a shape function belonging to a different
symmetry.

PRISM*, in prism_indices(): a transposing case has to swap either the mode
indices or the in-plane coordinates. The transposing cases of all three
quadrilateral faces did both, which cancels, so two symmetries differing only in
the direction bit produced identical shape functions. Face 2 case 1 hard-coded
the offset from an edge to the second triangle's numbering, which holds only at
order three, and cases 5 through 8 of that face reflected a coordinate that
needs no reflection there.

PRISM*, in fe_hierarchic_3D_shape(): the parity of a triangle edge mode was
taken from the wrong index base, which agrees with the mode number only at order
three.

PRISM*, in prism_indices(): the interior basis of a triangle is not symmetric in
its barycentric coordinates, so the two triangular faces need those coordinates
ordered by the face's vertices, as the tetrahedral faces already are. The
comment saying the TRI code handled this was mistaken; it orders the edge modes
of a triangle, not the barycentric coordinates a face's interior modes read.

HEX27, in cube_remap(): the flip bit of a quadrilateral face compared the least
node of the face against a neighbor, which is true for any two distinct points,
so the four symmetries reached through a reflection were unreachable and
SIDE_HIERARCHIC was non-conforming from order three up. Rotating the least node
to the origin leaves the reflection about the diagonal through it, which is
settled by which of that node's two neighbors is the lesser, as orient_quad()
already does.

The new FEOrientationTest holds each family to two properties on a skewed mesh:
that the shape functions of a degree of freedom follow the orientation index of
the one entity that owns it, and that two elements sharing a side agree on the
shape function of a degree of freedom they share. Every one of the fixes above
is needed for a case of the second property, and the first property is what
lets a cache of reference shape functions carry one entry per entity and
orientation. A side-only family has no shape value at an interior point, so
FEConformityTest registers the second property alone.

Co-Authored-By: Claude <noreply@anthropic.com>
@moosebuild

moosebuild commented Sep 10, 2026

Copy link
Copy Markdown

Job Coverage, step Generate coverage on c021cd1 wanted to post the following:

Coverage

6fa635 #4546 c021cd
Total Total +/- New
Rate 65.99% 66.06% +0.07% 100.00%
Hits 79562 79640 +78 137
Misses 40996 40917 -79 0

Diff coverage report

Full coverage report

This comment will be updated on new commits.

lindsayad and others added 3 commits September 10, 2026 16:58
The one-dimensional hierarchic bubbles were (xi^p - 1)/p! for even p and
(xi^p - xi)/p! for odd. The 1/p! makes a shape function shrink factorially with
its order: at p = 8 it is about 2.5e-05, and a two-dimensional tensor product of
two such factors carries its square. An operator assembled from that basis
inherits the spread on its diagonal, because for the Laplacian the diagonal entry
is the squared H1 seminorm of its shape function. On a 4x4 mesh of QUAD9 elements
the diagonal of an order-8 stiffness matrix ran from 1.08e-17 to 8/3, so its
smallest entry sat below the roundoff of its largest and the discretization was
numerically singular in double precision however it was solved: its inverse
overflowed a double from order 7 upward.

The bubbles are now scaled to unit H1 seminorm on the reference interval, which
replaces the factorial with a factor growing as the square root of the order. The
same mesh now gives a diagonal from 0.128 to 8/3 at order 8 and a condition number
of 4.4e+10, against a reciprocal machine epsilon of 4.5e+15, and the attainable
residual of a solve there improves from 1.7e-12 to 4.0e-15.

The two vertex functions are deliberately left unscaled. They are interpolatory,
so their coefficients are values of the finite element solution at the vertices,
and scaling them would change what a nodal boundary condition or a nodal output of
a hierarchic variable means.

This is a change of basis within one space, so the finite element solution is
unaffected: the integral, L2 norm, H1 seminorm and interior point values of an
order-8 solution with content in every mode all agree with the previous basis to
every printed digit. Coefficient vectors change, and so does the 2-norm of a
residual taken in them, which makes iteration counts and residual histories
measured before and after incomparable.

Two other sites carried the same factorial and move with it: the branch each of
the 2D and 3D files takes when a triangle or tetrahedron edge coordinate vanishes
is the limit of the general expression, so it needs the same scaling or the shape
function is discontinuous there.

The second derivative's general branch was also wrong for p of 8 or more, where it
returned the first derivative's xi^(p-1)/(p-1)! rather than p (p-1) xi^(p-2). It is
rewritten here rather than left in place, having had to be rewritten for the
scaling anyway.

Co-Authored-By: Claude <noreply@anthropic.com>
The coefficients a DirichletBoundary's projection determines were only ever
reachable as a side effect: add the boundary, run create_dof_constraints(), read
the constraint rows back, then remove the boundary and sweep again to undo the
mutation.  A caller that wants the coefficients rather than the constraints paid
two sweeps, had to tell the rows it caused apart from the hanging-node and
periodic rows already present, and mutated a DofMap that other readers share.

DofMap::compute_dirichlet_values() returns them directly and constrains nothing.
The projection itself is unchanged: it is the same ConstrainDirichlet over the
same local element range, with a third AddConstraint implementation that records
each prescribed value instead of writing a constraint row.  A degree of freedom
the DofMap already constrains is omitted, as AddPrimalConstraint omits it, so the
values reported are those the constraint path would have added.

AddConstraint no longer holds a DofMap: writing into one is a property of the two
implementations that do it, not of disposing of a computed constraint, and the
collecting implementation has no DofMap to write to.  That leaves
ConstrainDirichlet needing only a const DofMap, which is what lets the new method
be const.

A projected value is a coefficient in whatever basis is current, so this is how a
caller obtains an essential boundary value that is correct on a modal basis such
as HIERARCHIC above first order, where a degree of freedom at a node is a bubble
coefficient rather than a point value of the solution.

Co-Authored-By: Claude <noreply@anthropic.com>
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.

2 participants