From dfc7610f21425751018681e564998641b17cc9a5 Mon Sep 17 00:00:00 2001 From: lmoresi Date: Mon, 22 Jun 2026 10:01:08 +1000 Subject: [PATCH] =?UTF-8?q?fix(meshing):=20mmpde=20metric=20=E2=80=94=20mo?= =?UTF-8?q?notone=20RBF=20bake=20from=20nodal=20values?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restores the intended "RBF metric eval" robustness. The mmpde mover's RBF/Shepard metric path baked the metric via an FE evaluation at the FIXED reference cloud `ref`; on a deformed mesh that reference can mis-locate / drift outside the deformed interior and the FE evaluation returns garbage (the P1 density — strictly positive by construction — comes back NEGATIVE, even at a field's own DOFs). A negative density is a non-SPD metric, which the mover then either NaN-bails on (a hidden stall) or, with the #259 SPD-floor, acts on as "coarsen hard here" → giant cells / holes / divergence in the adapted mesh. Fix: bake the metric at the CURRENT mesh NODES (its own DOF locations) and Shepard-interpolate from there. A Shepard (positive-weight, convex) average of the positive nodal values is GUARANTEED >= 0 (monotone) — the metric can never go non-SPD from the eval — and nodes are always inside the mesh (no out-of-domain / drift) and need no per-step cell location (fast). `ref` is kept for the _edge_mats reference frame. Best paired with #264 (deformed-mesh point-location fix), which makes the nodal FE bake exact; together the adapted mesh stays clean under forced every-step adaptation at R=5 (folded=0, cell-area-ratio flat) where it previously tore holes. Validated in a sibling worktree (this is a one-line source change; CI runs the full mover suite). Underworld development team with AI support from Claude Code --- src/underworld3/meshing/smoothing.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/underworld3/meshing/smoothing.py b/src/underworld3/meshing/smoothing.py index 5272cfbb..4163c45e 100644 --- a/src/underworld3/meshing/smoothing.py +++ b/src/underworld3/meshing/smoothing.py @@ -3145,22 +3145,32 @@ def _dM_dx(cen): mesh._mmpde_reference_coords = ref # --- RBF/Shepard bake of the metric (the production-fast path) ------ - # Evaluate the analytic metric ONCE on the fixed reference cloud, then - # interpolate to the moving centroids each step via k-NN inverse- - # distance (Shepard). The reference cloud is fixed in space ⇒ Eulerian. + # Bake the metric at the CURRENT mesh NODES (its own DOF locations), then + # interpolate to the moving centroids each step via k-NN inverse-distance + # (Shepard). Source = nodes, NOT the fixed reference cloud `ref` (`ref` is + # kept for the _edge_mats reference frame). Two reasons: + # * MONOTONE: a P1 density is positive by construction; Shepard is a convex + # (positive-weight) average of the sampled node values, so the result is + # GUARANTEED positive — no negative/non-SPD garbage, the SPD floor / NaN + # bail never has to fire. + # * ROBUST + FAST: nodes are always inside the mesh (never out-of-domain), + # and Shepard needs no per-step cell location. RBF doesn't need + # high-precision eval — speed + monotonicity. (Restores the earlier + # "RBF metric eval" design intent: the fixed-`ref` FE bake could + # mis-locate / drift outside a deformed interior and return ρ<0.) if metric_eval == "rbf": from scipy.spatial import cKDTree - M_ref = _eval_M_analytic(ref) # one analytic pass - _tree = cKDTree(ref) + M_src = _eval_M_analytic(coords) # nodal values (positive) + _tree = cKDTree(coords) _kk = int(rbf_k) if rbf_k else (cdim + 2) def _eval_M(pts): dist, idx = _tree.query(pts, k=_kk) if _kk == 1: - return M_ref[idx] + return M_src[idx] w = 1.0 / np.maximum(dist, 1.0e-12) ** 2 w /= w.sum(axis=1, keepdims=True) - return np.einsum('nk,nkab->nab', w, M_ref[idx]) + return np.einsum('nk,nkab->nab', w, M_src[idx]) else: _eval_M = _eval_M_analytic