origin/main| SRC | HEAD (743706f464c) |
| DEST | origin/main (e000d6ce225) |
| Commits reviewed | 3 — 655b17fd0c0, 71fad547eb1, 743706f464c |
| Files changed | 6 files, +168/−20 (no .out files touched) |
| Generated | 2026-07-23 18:14 UTC |
DMProjectFunction()part hard-coded to 0/1/2| File | Change |
|---|---|
| doc/changes/dev.md | +1 |
| include/petscds.h | +1 |
| src/dm/dt/interface/dtweakform.c | +36 — new PetscWeakFormGetKeys() |
| src/dm/impls/plex/plexfem.c | +1/−1 — boundary-type check in DMPlexComputeBdResidual_Internal() |
| src/dm/impls/plex/plexgeometry.c | +11/−4 — guards in DMPlexComputeCellGeometryFEM() |
| src/snes/utils/dmplexsnes.c | +118/−15 — hybrid evaluation in DMPlexSNESComputeResidualFEM(), and a rewrite of DMPlexSetSNESVariableBounds() |
DMProjectFunction() requires a global vector
src/snes/utils/dmplexsnes.c:1232, :1245 — DMPlexSetSNESVariableBounds()
DMPlexSetSNESVariableBounds() now builds a local work vector locb via DMGetLocalVector(), then calls:
PetscCall(DMGetLocalVector(dm, &locb));
PetscCall(VecSet(locb, PETSC_NINFINITY));
PetscCall(DMProjectFunction(dm, 0., lfuncs, lctxs, INSERT_VALUES, locb)); // ← locb is local
PetscCall(DMPlexInsertBounds(dm, PETSC_TRUE, 0., locb));
DMProjectFunction()'s own docstring states it "put[s] the coefficients in a global vector." Internally it allocates its own local work vector, projects into that, then does
DMLocalToGlobalBegin/End(dm, localX, mode, X) where X is the argument passed in — here that argument is locb, which is local-sized. This mismatches vector layouts whenever local ≠ global
(parallel runs, or any DM with ghosted/constrained dofs), causing the scatter to fail or silently write to the wrong offsets.
The correct call is DMProjectFunctionLocal(), which has the identical signature but expects a local vector (see src/dm/interface/dm.c:8475, parameter named localX).
This bug is masked by the only existing test that exercises this path, src/snes/tutorials/ex34.c, which runs at nsize=1 with no constrained dofs — there, local and global vector sizes happen to coincide.
Lines 1232 (hasLower branch, using lfuncs/lctxs) and 1245 (hasUpper branch, using ufuncs/uctxs) have the identical bug.
See Fix A below.
PetscFormKey.part hard-coded to 0/1/2 instead of the registered value
src/snes/utils/dmplexsnes.c:440–451 — DMPlexSNESComputeResidualFEM()
keys[0].part = 0;
keys[1].part = 1;
keys[2].part = 2;
Per include/petscdstypes.h:44, part "Selects the equation part… LHS = 0 and RHS = 1 in IMEX…", and lookups are an exact hash match on
(label, value, field, part) (PetscWeakFormGetFunction_Private, src/dm/dt/interface/dtweakform.c:101). Every existing registration of a boundary residual — including the only reference
implementation of this exact hybrid pattern, src/dm/impls/plex/tests/ex69.c:1317–1322 — uses part=0 for all three keys; the sides are distinguished by value/field, not by
part.
Because PetscFEIntegrateHybridResidual_Basic() (src/dm/dt/fe/impls/basic/febasic.c:639–640) does:
PetscCall(PetscWeakFormGetBdResidual(wf, key.label, key.value, key.field, key.part, &n0, &f0_func, &n1, &f1_func));
if (!n0 && !n1) PetscFunctionReturn(PETSC_SUCCESS);
the part=1 and part=2 keys will find nothing and silently skip computation. The positive-side and cohesive-surface-field residual contributions are dropped from the assembled residual
every time this hybrid path fires.
See Fix B below — propagate the actual .part found on the matched bdf0keys[i]/bdf1keys[i] entries instead of hard-coding the array index.
DMPlexSNESComputeResidualFEM()
for (PetscInt i = 0; i < bdf0Nk; ++i) {
if (bdf0keys[i].field == 0) {
if (!label0) { label0 = ...; value0A = bdf0keys[i].value; value0B = value0A; }
else if (bdf0keys[i].value != value0A) value0B = bdf0keys[i].value;
}
...
}
bdf0keys[] comes from PetscWeakFormGetKeys() → PetscHMapFormGetKeys(), whose iteration order follows
PetscFormKeyHash = PetscHashCombine(PetscHashPointer(label), ...) (include/petsc/private/petscdsimpl.h:42) — a function of the DMLabel's heap address, not insertion order.
But key[0]/key[1] are matched positionally against the mesh's structural sides inside DMPlexComputeResidualHybridByKey()/PetscFEIntegrateHybridResidual()
(s=0/s=1 come from which neighbor cell is in the face's support[], independent of key content). A pointer-hash-dependent A/B assignment can silently swap which registered function
(e.g. f0_bd_u_neg vs f0_bd_u_pos) is applied to which geometric side, and this can vary between runs/builds.
Note also that bdf1keys is fetched but its contents are never scanned, so any PETSC_WF_BDF1-only surface registration is silently ignored — consistent with the code's own
// In the future, we need a way to construct the keys... comment.
See Fix B below — sort the two field-0 values so the assignment is deterministic instead of hash-order dependent.
DMPlexSNESComputeResidualFEM(), DMPlexSetSNESVariableBounds()
None of the three commits add or update a test that exercises the new hybrid branch in DMPlexSNESComputeResidualFEM(), nor the local/global vector fix needed in
DMPlexSetSNESVariableBounds(). The one existing hybrid-assembly test, src/dm/impls/plex/tests/ex69.c, calls DMPlexComputeResidualHybridByKey() directly with hand-built keys
(part=0 throughout) — it never goes through dmplexsnes.c, so it cannot catch findings CRITICAL 2 or HIGH above. A small SNES/TS test driving a
fault/cohesive problem through DMPlexSNESComputeResidualFEM() would likely have surfaced these.
src/dm/impls/plex/plexfem.c: type & DM_BC_ESSENTIAL → !(type & DM_BC_NATURAL) is a real fix, not a no-op rewrite —
DM_BC_LOWER_BOUND (4) and DM_BC_UPPER_BOUND (8) have neither the ESSENTIAL nor the NATURAL bit set, so the old check let VI-bound boundary entries fall through into natural-residual processing;
the new check correctly excludes them.
src/dm/impls/plex/plexgeometry.c: both changes (hoisting the qdim==dim check earlier, and falling back to DMPlexComputeCellGeometryFEM_Implicit() when the coordinate
FE's basis-space dimension doesn't match the caller's quadrature) are consistent, correct guards for the new lower-dimensional (surface/hybrid) quadrature usage.
PetscWeakFormGetKeys() (new public API) matches the existing internal allocation pattern in PetscWeakFormViewTable_Ascii(); docstring formatting and header prototype are correct.
PetscWeakFormGetKeys() even when the weak form has no boundary residuals; and missing
doc/changes/dev.md entries for the DMPlexSNESComputeResidualFEM()/DMPlexSetSNESVariableBounds() behavior changes. Ask to show them in detail.
clang-format; they are not part of the reviewed commits. Both touch only
src/snes/utils/dmplexsnes.c.
part, make cohesive side ordering deterministicAddresses CRITICAL 2 and HIGH.
@@ -420,6 +420,7 @@ PetscErrorCode DMPlexSNESComputeResidualFEM(DM dm, Vec locX, Vec locF, PetscCtx
IS cohesiveCells;
DMLabel label0 = NULL, label1 = NULL;
PetscInt value0A = 0, value0B = 0, value1 = 0;
+ PetscInt part0A = 0, part0B = 0, part1 = 0;
// In the future, we need a way to construct the keys for both sides and the surface, and also the cellIS from the label
for (PetscInt i = 0; i < bdf0Nk; ++i) {
@@ -427,28 +428,41 @@ PetscErrorCode DMPlexSNESComputeResidualFEM(DM dm, Vec locX, Vec locF, PetscCtx
if (!label0) {
label0 = bdf0keys[i].label;
value0A = bdf0keys[i].value;
+ part0A = bdf0keys[i].part;
value0B = value0A;
+ part0B = part0A;
} else if (bdf0keys[i].value != value0A) {
value0B = bdf0keys[i].value;
+ part0B = bdf0keys[i].part;
}
}
if (bdf0keys[i].field == 1) {
label1 = bdf0keys[i].label;
value1 = bdf0keys[i].value;
+ part1 = bdf0keys[i].part;
}
}
+ // Order sides by value so the key assignment does not depend on PetscWeakFormGetKeys() hash map iteration order
+ if (value0B < value0A) {
+ PetscInt tmpValue = value0A, tmpPart = part0A;
+
+ value0A = value0B;
+ part0A = part0B;
+ value0B = tmpValue;
+ part0B = tmpPart;
+ }
keys[0].label = label0;
keys[0].value = value0A;
keys[0].field = 0;
- keys[0].part = 0;
+ keys[0].part = part0A;
keys[1].label = label0;
keys[1].value = value0B;
keys[1].field = 0;
- keys[1].part = 1;
+ keys[1].part = part0B;
keys[2].label = label1;
keys[2].value = value1;
keys[2].field = 1;
- keys[2].part = 2;
+ keys[2].part = part1;
PetscCall(CreateSurfaceCellIS_Private(plex, &cohesiveCells));
PetscCall(DMPlexComputeResidualHybridByKey(plex, keys, cohesiveCells, PETSC_MIN_REAL, locX, NULL, 0.0, locF, ctx));
PetscCall(ISDestroy(&cohesiveCells));
part now reflects whatever value was actually used when the residual functions were registered (normally 0), so PetscWeakFormGetBdResidual()'s exact-match lookup succeeds for all three keys
instead of only keys[0]. Sorting by value makes the side assignment reproducible instead of depending on PetscHMapFormGetKeys()'s pointer-hash-derived iteration order — and it
happens to match the convention already used in ex69.c (negative side = lower label value, positive side = higher). This does not address the unused-bdf1keys gap noted above, which needs a design
decision rather than a mechanical fix.
DMProjectFunctionLocal() instead of DMProjectFunction()Addresses CRITICAL 1.
@@ -1229,7 +1243,7 @@ PetscErrorCode DMPlexSetSNESVariableBounds(DM dm, SNES snes)
PetscCall(DMGetLocalVector(dm, &locb));
PetscCall(VecSet(locb, PETSC_NINFINITY));
- PetscCall(DMProjectFunction(dm, 0., lfuncs, lctxs, INSERT_VALUES, locb));
+ PetscCall(DMProjectFunctionLocal(dm, 0., lfuncs, lctxs, INSERT_VALUES, locb));
PetscCall(DMPlexInsertBounds(dm, PETSC_TRUE, 0., locb));
PetscCall(DMLocalToGlobalBegin(dm, locb, INSERT_VALUES, lb));
PetscCall(DMLocalToGlobalEnd(dm, locb, INSERT_VALUES, lb));
@@ -1242,7 +1256,7 @@ PetscErrorCode DMPlexSetSNESVariableBounds(DM dm, SNES snes)
PetscCall(DMGetLocalVector(dm, &locb));
PetscCall(VecSet(locb, PETSC_INFINITY));
- PetscCall(DMProjectFunction(dm, 0., ufuncs, uctxs, INSERT_VALUES, locb));
+ PetscCall(DMProjectFunctionLocal(dm, 0., ufuncs, uctxs, INSERT_VALUES, locb));
PetscCall(DMPlexInsertBounds(dm, PETSC_FALSE, 0., locb));
PetscCall(DMLocalToGlobalBegin(dm, locb, INSERT_VALUES, ub));
PetscCall(DMLocalToGlobalEnd(dm, locb, INSERT_VALUES, ub));
Both call sites just swap the function name; DMProjectFunction() and DMProjectFunctionLocal() share the same argument list, differing only in whether the final Vec is global or local.
clang-format --dry-run --Werror against the project's .clang-format and produced no diagnostics. Neither fix addresses MEDIUM finding (test coverage) —
that requires a new test fixture, not a mechanical patch.