mesh: strong relaxation morphs round the bad elements (harmonic interpolation within a reach)

The owner's direction: bad nodes move to better places, the nodes within a reach follow by
the graph Laplacian's harmonic extension, edges held, nothing outside the reach moves.
mesh.node.relax strong mode takes below and reach and reports each round.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-16 14:55:59 -04:00
1 parent 6f94346253
commit 4908f5679c
8 files changed
+430 -253

No files matched your search

+6 -5
View File
@@ -210,11 +210,12 @@ When `mesh.quality` shows a handful of spikes in an otherwise good mesh:
vertex nodes stay; nodes on a partition cut face move freely (both cells share them). The
reply gives the nodes moved, the largest displacement, and the region's smallest scaled
Jacobian, largest edge ratio and worst elements before and after. One undo step.
For a whole region at once, `mesh.node.relax {"mode": "strong", "bodies": ["Base"]}` (or
`"below": 0.7, "layers": 2` for the elements below 0.7 and two rings round them) optimises
every node together toward scaled Jacobian 1, edge and vertex nodes held, single elements
allowed to get worse on the way; it keeps the best state, never one with a lower smallest
scaled Jacobian, and replies the objective, min and mean per sweep (`history`).
For a whole region at once, `mesh.node.relax {"mode": "strong", "bodies": ["Base"], "below": 0.7}`
morphs the mesh round every element below 0.7: their nodes move to better places and the
nodes within `reach` (default three element lengths) follow by harmonic interpolation; edge
and vertex nodes are held, face nodes stay on their faces, nothing outside the reach moves,
and the smallest scaled Jacobian never drops. The reply lists each round's bad elements,
clusters and region size (`rounds`). It runs as a job (`job.status`, `job.cancel`).
2. The relaxation is kept as a field of the model (`field`, default `Relax1`, …; saved with it).
After a remesh, `mesh.node.relax.apply {"field": "Relax1"}` puts the relaxed shape on the new
mesh (each node takes the displacement interpolated in the old cell it lies in, on its face,
+11 -10
View File
@@ -970,16 +970,17 @@ input points), Steiner points kept half the local spacing from the boundary.
mesh takes the displacement at each node's parametric coordinates in the old cell holding it (barycentric or
trilinear shape functions), slides it onto its face or edge, undoes any move that would invert an element, and
smooths once (`mesh.node.relax.apply`, or `reapply` after every remesh).
- **Strong mode** (`"mode": "strong"`, `fieldcraft-mesh::relax::optimize`). An optimisation of every corner node
of the region at once: interior nodes in space, face nodes in their face's parameters, edge and vertex nodes held.
It minimises Σ φ(SJ) over the elements by nonlinear Gauss–Seidel (per node: the neighbours' centroid, then a
central-difference gradient step with backtracking, each kept when it lowers the node's part of the sum), first
with φ = (1/SJ − 1)², then with φ = SJ⁻¹⁶ (a smooth stand-in for the smallest SJ); an invalid element costs
far more than any valid one (a barrier, and an untangling objective while elements are inverted; P. Knupp,
*Hexahedral and tetrahedral mesh untangling*, Eng. Comput. 17, 2001; L. Freitag, P. Plassmann, IJNME 49, 2000).
A term ½ ln²(V/V₀) keeps element sizes, and no element may exceed the region's largest edge ratio before: the
scaled Jacobian alone lets elements flatten against the boundary. The best state passed through is kept, and
only if it is valid and its smallest SJ is not below the start.
- **Strong mode** (`"mode": "strong"`, `fieldcraft-mesh::relax::morph`). A mesh morph round the bad elements
(scaled Jacobian below `below`): their movable corner nodes are grouped into clusters (nodes within the reach of
each other, from a fixed-radius grid query); per cluster, the bad nodes first move to better places by a few
sweeps of their own optimisation with the rest held (a central-difference gradient step with backtracking on
Σ SJ⁻¹⁶ + ½ ln²(V/V₀) over their elements, an invalid element costing far more than any valid one), then that
displacement is carried to every node within the reach by harmonic interpolation: the graph Laplacian of the
element edges solved (Gauss–Seidel) with the bad nodes' displacements and zero at held nodes and beyond the reach.
Face nodes go back on their face (UV), edge and vertex nodes are held, nodes outside the reach do not move. A
cluster's morph is kept, at full or a halved scale, only when no more elements are invalid, the worst element it
touches is no worse, the smooth objective Σ (1/SJ − 1)² there drops, and no element gets longer than the longest
was; rounds repeat while any cluster is kept. The result never has a lower smallest SJ than the start.
---