Uh oh!
There was an error while loading. Please reload this page.
refactor(readability): Wave D smoothing — mover dedup, renames, MPI helpers, package split (D-01..D-13) - #345
Conversation
…_M_tensor _winslow_anisotropic carried a ~100-line inline copy of its own _build_M_tensor() closure body (density regimes + eigen-clamped tensor assembly). Delete the copy and call the closure once before the outer loop; the pre-loop gproj.solve()/gvec/gn/gmax/gref computation it duplicated is removed (the closure recomputes them identically), while old0 and h0 stay pre-loop as the closure depends on base = 1/h0**2. Bit-identical check: mover probe (spring/ma/ot/anisotropic x4 regimes incl. metric_refresh_per_iter=True/mmpde/jacobi) — all final coordinates bit-identical to base 8a94f67. Underworld development team with AI support from Claude Code
…step_to_edge_fraction The coherent global signed-area backtrack existed x3 (MA, OT, anisotropic movers) and the per-vertex step cap x2 (MA, OT). Extract both as module helpers: - _backtracked_move(..., area_floor=0.0): area_floor=0.0 default keeps the MA/OT flip-only acceptance bit-identical; the anisotropic mover passes its sliver floor (0.01 x undeformed median cell area). - _cap_step_to_edge_fraction(step, dm, coords, step_frac): None / non-finite step_frac is a no-op, as before. The MMPDE per-node cap and the spring/MMPDE line searches are structurally different and deliberately NOT folded in (per READ-05). Bit-identical check: mover probe (all movers/regimes) matches base. Underworld development team with AI support from Claude Code
…slow_* movers Only _winslow_anisotropic actually solves a Winslow (M-weighted Laplace) coordinate map; the other four prefixed movers were lying about their method: _winslow_spring -> _spring_equilibrium_mover _winslow_elliptic -> _monge_ampere_mover _winslow_equidistribute -> _ot_improvement_step _winslow_mmpde -> _mmpde_mover The old names stay for ONE cycle as DeprecationWarning aliases (functools.wraps, signature preserved) because they appear in exploratory scripts/ and possibly external user scripts. In-repo sweep: tests/test_0641 (+ class rename), tests/test_0850, _ot_adapt.py and remesh.py docstring mentions, adaptive-meshing skill. Historical design docs and the 2026-07 review record are deliberately untouched; scripts/ ride the one-cycle alias. Bit-identical check: mover probe matches base; alias smoke test (warn + delegate + signature) green; test_0641 + test_0850: 53 passed. Underworld development team with AI support from Claude Code
…{sum,min,max,mean} helpers
mpi4py is a hard dependency, yet the module carried 14 inline
'from mpi4py import MPI as _MPI' imports guarding ~35 scalar allreduce
sites. Import _MPI once at module top and add four serial-no-op scalar
reduction helpers:
_global_sum / _global_min / _global_max
_global_mean — the movers' historical rank-mean of rank-local means
(allreduce/size), documented as such
Every converted site keeps its exact numeric expression (float
coercion, SUM/MIN/MAX op) and its collective participation: reductions
that were unconditional stay unconditional (the guard moved inside the
helper), and the aniso-closure gmax reduction takes the local max
FIRST so empty ranks still participate. The spring _allsum closure and
mesh_metric_mismatch's _ar lambda are replaced by _global_sum.
sqrt-of-sum-of-squares and mean()-vs-sum()/n branch structures are
preserved verbatim where collapsing them could change bits.
Bit-identical check: mover probe (all movers/regimes + mismatch dict)
matches base.
Underworld development team with AI support from Claude Code_mmpde_mover(**_ignored) silently swallowed typo'd method_kwargs (no other mover has a catch-all). The catch-all was not a pure hedge: the strategy path injects resolution_ratio into method_kwargs for every mover. Accept resolution_ratio=None explicitly (documented as unused — MMPDE's clustering intensity comes from the metric tensor), and warn on any remaining unknown keyword instead of swallowing it. Sanctioned signature change per the D-05 worklist row. Checks: strategy='low' + method='mmpde' still dispatches; a typo'd kwarg now warns; mover probe bit-identical to base. Underworld development team with AI support from Claude Code
The old module docstring described the deleted design: spring as a Jacobi relaxation 'under development' that 'can stall against the tangle guard' (contradicted by the mover's own nonlinear-CG docstring), never mentioned mmpde (the mover the file's own DeprecationWarning recommends for production), and duplicated the 2026-05-16 MA investigation note that already lives on the MA section banner. Rewrite: one paragraph per mover (what it is, status, when to use), using the D-03 names; state that mmpde is recommended-for-production but NOT the API default (spring is); keep the parallel-exactness facts (Jacobi bit-identical, spring serial-exact); point the MA history at the section banner. Docstring-only commit — zero code changes. Underworld development team with AI support from Claude Code
…he envelope branch; list 'arc-length' metric_density_from_gradient carried a dead amp-inversion (amp = refinement**(cdim/power) - 1): every refinement-is-not-None path enters the envelope branch, which exhaustively returns or raises before amp's only consumer, so the assignment (and its superseded override-semantics comment) could never matter — deleted. The 'refinement' parameter docstring still documented those superseded override semantics; rewritten to describe the envelope branch (which params it ignores), with 'coarsening' and 'metric_choice' now documented alongside it. 'arc-length' — accepted by the code — added to the envelope comment option list, follow_metric's metric ansatz bullets, and follow_metric's metric parameter doc. Dead-code deletion verified behaviour-neutral (consumer unreachable); mover probe bit-identical to base. Underworld development team with AI support from Claude Code
…lude mmpde
The docstring header said method : {'spring', 'ma'} while the bullets
below it already described anisotropic and mmpde; the dispatch
ValueError listed spring/ma/ot/anisotropic but not mmpde — the mover
the same function's DeprecationWarning recommends. Header now lists
the four supported choices with mmpde noted as the recommended
production mover and 'ot' as deprecated-only (with a bullet); the
ValueError text names mmpde and marks 'ot' deprecated.
Docstring/error-string only; mover dispatch unchanged.
Underworld development team with AI support from Claude Code…ismatch return dict The docstring documented a 3-key return; the function returns 5 keys, and the undocumented 'misalignment' key is exactly what the skip_threshold machinery consumes. Document all five, with one line each for alignment (Pearson r of log(1/A_c) vs log(rho_c), globally reduced) and misalignment (sqrt(1 - max(0,r)^2), negative correlation clamps to 0 -> misalignment 1.0). Docstring-only commit — zero code changes. Underworld development team with AI support from Claude Code
…ov helpers; _zig renamed The gamg/direct _wire closure pair was duplicated x3 (MA / OT / anisotropic cache builds) and the cryptic _zig flag was computed identically x3, each with its own 3-9 line re-explanation of the direct-under-MPI fallback. Extract _solver_wiring(linear_solver) and _warm_start_krylov(linear_solver) (the rationale lives once, on the helper docstrings) and rename _zig to zero_init_guess at each use: zero_init_guess = not _warm_start_krylov(linear_solver). Bit-identical check: mover probe matches base. Underworld development team with AI support from Claude Code
…dial_tangential The move_anisotropy radial/tangential displacement reweighting was duplicated verbatim in _monge_ampere_mover and _winslow_anisotropic (with the full design rationale repeated at one site). Extract it as a module helper carrying the rationale in its docstring; both call sites keep the 'move_anisotropy is not None and cdim == 2' opt-in guard. Checks: helper output bit-identical to the original inline formula on synthetic data; mover probe (default None path) matches base. Underworld development team with AI support from Claude Code
The global mean-edge-length h0 measurement was implemented twice (_winslow_anisotropic pre-loop, follow_metric first-call cache), each with its own copy of the compounding-refinement commentary. Extract _mean_edge_length(dm, coords); the measure-once warning now lives on the helper docstring and the _FOLLOW_METRIC_H0_CACHE declaration, with one-line pointers at the two call sites. Checks: mover probe bit-identical to base (covers the anisotropic h0 path); test_0750 follow_metric suite green (covers the cached path). Underworld development team with AI support from Claude Code
…a meshing/smoothing/ package The graft-accumulation module (6 movers, MA solver wiring, Hessian recovery, metric builders, adapters, 8 module-level caches, an explicit 'grafted from feature/elliptic-ma' banner) becomes a package, pure code motion: graph.py topology/masks/reductions/shared mover primitives + smooth_surface_field spring.py spring-equilibrium mover (+_SPRING_CACHE) monge_ampere.py MA machinery, OT improvement step, solver wiring anisotropic.py tensor (true Winslow) mover (+_ANISO_CACHE/_GEMA_STATE) mmpde.py variational MMPDE mover metrics.py ADAPT_STRATEGIES, metric builder, mismatch diagnostic api.py smooth_mesh_interior dispatch + follow_metric __init__.py package docstring (module map), re-exports, the D-03 one-cycle _winslow_* aliases anisotropic.py is added to the row's module list because the mover is ~600 lines on its own and reuses monge_ampere's solver wiring. __init__ re-exports EVERY formerly-module-level name, so the private cross-module surface (_edge_pairs, _tri_cells/_tet_cells, _pinned_mask, _auto_pinned_labels, _owned_vertex_mask, _signed_areas, movers, wiring, ...) and attribute-style access keep working; the smoothing<->_ot_adapt mutual imports stay lazy (inside functions). Verification: cross-module import check green; mover probe bit-identical serial AND np2 (both ranks, all movers/regimes) vs base 8a94f67; owning+consumer test files 104 passed; np2 smoothing parallel tests 4 passed; full level_1+tier_a identical to baseline (373/10/1118/2/1). Underworld development team with AI support from Claude Code
…ore collectives Found during the Wave D sweep (not fixed here — scope discipline, Charter section 9): _spring_equilibrium_mover returns early on a rank with zero free vertices while other ranks proceed into collective reductions, a latent MPI deadlock. Consistent with the path's documented serial-exact status; marker only. Underworld development team with AI support from Claude Code
There was a problem hiding this comment.
Pull request overview
This PR refactors the mesh-smoothing / fixed-topology adaptation code by splitting the former meshing/smoothing.py into a underworld3.meshing.smoothing package, renaming internal mover functions for clarity, and centralising shared MPI helpers / mover utilities while aiming to preserve behaviour (except the explicitly sanctioned signature change).
Changes:
- Split smoothing into a package (
graph,spring,monge_ampere,anisotropic,mmpde,metrics,api,__init__) and re-export prior module-level symbols. - Rename movers (with one-cycle deprecated aliases) and update tests / docstrings accordingly.
- Add shared MPI scalar reduction helpers and refactor duplicated mover utilities into common helpers.
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_0850_mesh_smoothing.py | Update import to renamed _mmpde_mover. |
| tests/test_0641_wave_c_api_shims.py | Update signature-shim tests for renamed spring mover. |
| src/underworld3/meshing/smoothing/spring.py | New spring-equilibrium mover module (CG-based). |
| src/underworld3/meshing/smoothing/monge_ampere.py | New MA/OT module plus solver wiring helpers. |
| src/underworld3/meshing/smoothing/mmpde.py | New MMPDE mover module with kwargs warning + sanitisation. |
| src/underworld3/meshing/smoothing/metrics.py | Strategies + gradient-density metric builder + mismatch diagnostic. |
| src/underworld3/meshing/smoothing/graph.py | Shared topology helpers, reductions, and smoothing primitives. |
| src/underworld3/meshing/smoothing/api.py | Public dispatch (smooth_mesh_interior, follow_metric) updated for package split / renames. |
| src/underworld3/meshing/smoothing/anisotropic.py | New anisotropic Winslow mover module. |
| src/underworld3/meshing/smoothing/init.py | Package doc + re-exports + deprecated mover aliases. |
| src/underworld3/meshing/_ot_adapt.py | Docstring reference updated for renamed OT step (still mentions smoothing.py). |
| src/underworld3/discretisation/remesh.py | Docstring wording updated for mover call sites (still mentions smoothing.py). |
| .claude/skills/adaptive-meshing/SKILL.md | Update skill docs to renamed _mmpde_mover. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| dmax = _global_max(max(float(np.linalg.norm( | ||
| d[free_idx], axis=1).max()), 1.0e-30)) | ||
| t0 = 0.5 * L0_mean / dmax |
There was a problem hiding this comment.
Verified: at branch head this zero-size reduction is actually unreachable on a starved rank — the rank-local early return at the top of the CG setup (if n_free == 0: ... return) exits first, so d[free_idx] is never empty here. The raise you describe only materialises once that early return is made collective, i.e. it is the second half of the #346 hazard rather than an independent one. The spring mover is retired in favour of method='mmpde' per the maintainer, so we are not hardening this path; commit 300bba4 extends the existing #346TODO(BUG) to record that any collective-exit fix must also guard these rank-local zero-size reductions (this dmax seed included).
Underworld development team with AI support from Claude Code
| A_actual = np.abs(_signed_areas(coords, tris)) | ||
| centroids = coords[tris].mean(axis=1) | ||
| rho = np.asarray(_uw.function.evaluate( | ||
| metric, centroids)).reshape(-1) | ||
| rho = np.maximum(rho, 1.0e-12) # guard | ||
| inv_rho = 1.0 / rho | ||
| A_target = A_actual.sum() * inv_rho / inv_rho.sum() | ||
| if resolution_ratio is not None: | ||
| R = float(resolution_ratio) | ||
| A_mean = A_actual.sum() / len(A_actual) | ||
| # Clip target areas to the mover's achievable band | ||
| # [A_mean/R², A_mean·R²] (h in [h0/R, h0·R] ⇒ | ||
| # A in [h0²/R², h0²·R²] = [A_mean/R², A_mean·R²]). | ||
| A_target = np.clip(A_target, A_mean / R ** 2, | ||
| A_mean * R ** 2) |
There was a problem hiding this comment.
Verified — partially right, with an important scoping note. The A_target/A_mean sums here are indeed rank-local, so under MPI the returned δ moments (rms/max/median_abs) are partition-dependent diagnostics measured against a per-rank equidistribution target. However, the signal the live skip path actually consumes (alignment/misalignment in smooth_mesh_interior/follow_metric) is computed from globally reduced moment sums further down this function, so the collective skip/adapt decision is rank-consistent and unaffected. The rank-local sums are pre-existing behaviour preserved verbatim by this PR's code motion (verified against the Wave D base), so per the readability-wave contract they are not changed here; commit 300bba4 adds a TODO(BUG) marker with the fix sketch (globally reduce the sums/counts before forming A_target/A_mean, plus global reductions for the δ moments), and the defect has been reported for issue-filing.
Underworld development team with AI support from Claude Code
| skip_threshold : float, default 0.9 | ||
| Alignment threshold for the adapt-on-demand skip. If the | ||
| existing mesh's :func:`mesh_metric_mismatch` alignment is | ||
| ≥ this threshold, no adaptation happens and the function | ||
| returns ``False``. |
There was a problem hiding this comment.
Confirmed — the docstring described an "alignment ≥ threshold" skip, but the dispatch skips when mesh_metric_mismatchmisalignment< skip_threshold (with the default 0.9 that means skipping once alignment r exceeds ~0.44, not 0.9). Fixed in 300bba4: the parameter docs now describe the actual misalignment semantics and spell out the default's effect.
Underworld development team with AI support from Claude Code
| ``do_move`` is a closure that performs the actual coordinate move — | ||
| typically the body of a ``_winslow_*`` mover, the OT step, or a | ||
| ``follow_metric`` mover. It is expected to call | ||
| typically the body of one of the ``smoothing.py`` movers (spring / | ||
| MA / OT / anisotropic / MMPDE) or a ``follow_metric`` mover. It is expected to call | ||
| :meth:`Mesh._deform_mesh` one or more times and leave the mesh |
There was a problem hiding this comment.
Good catch — this line was rewritten by this PR (the D-03 mover renames touched it) and still pointed at the now-retired single file. Fixed in 300bba4: it now refers to the underworld3.meshing.smoothing package movers.
Underworld development team with AI support from Claude Code
| (see ``discretisation/discretisation_mesh.py``); this module holds the | ||
| algorithm and the boundary-slip helpers it shares with the OT mover | ||
| (``_winslow_equidistribute`` in ``smoothing.py``). | ||
| (``_ot_improvement_step`` in ``smoothing.py``). | ||
There was a problem hiding this comment.
Confirmed — the reference was updated for the D-03 rename but not for the D-13 package split. Fixed in 300bba4: it now points at _ot_improvement_step in the underworld3.meshing.smoothing package (smoothing/monge_ampere.py).
Underworld development team with AI support from Claude Code
| w, Vc = np.linalg.eigh(Ms) | ||
| wmax = float(np.nanmax(w)) | ||
| floor = max(wmax, 1.0) * 1.0e-8 |
There was a problem hiding this comment.
Verified by direct probe: an all-NaN eigenvalue batch gives np.nanmax(w) = nan (with an all-NaN-slice warning), max(nan, 1.0) propagates the NaN into floor, and the nan_to_num(..., nan=floor)/clip(..., floor, None) projection then emits NaN tensors instead of the intended benign SPD fallback. The function is bit-identical to the Wave D base (pre-existing, preserved by the split from the #259 SPD-floor work), and the wave contract forbids numeric changes, so it is not altered here. Commit 300bba4 adds a TODO(BUG) marker at the site with the guard sketch (finite default for wmax when np.isfinite fails), and the defect has been reported for issue-filing — mmpde being the surviving mover, this one is worth a real fix in a numerics PR.
Underworld development team with AI support from Claude Code
| free_idx = np.nonzero(free)[0] | ||
| n_free = int(free_idx.size) | ||
| # TODO(BUG): rank-LOCAL early return before the collective CG loop. | ||
| # Under MPI, a rank whose local vertices are all pinned returns here | ||
| # while the other ranks proceed into _global_sum/_global_min | ||
| # collectives -> deadlock. Consistent with the documented | ||
| # serial-exact status of this path, but a latent parallel hazard; | ||
| # the exit decision should be reduced globally (as the OT/MA movers | ||
| # do) before this mover is promoted to parallel-exact. | ||
| if n_free == 0: | ||
| mesh._deform_mesh(coords) | ||
| return |
There was a problem hiding this comment.
Confirmed — this is exactly the latent collective-mismatch deadlock tracked in #346 (a rank with no free vertices returns here while the others enter the _global_sum/_global_min collectives). A TODO(BUG) marker was already placed at this site as part of this PR (marker-only per the campaign policy for known-latent defects), and the spring mover is retired in favour of method='mmpde' per the maintainer, so the path is not being hardened here. Commit 300bba4 extends the marker to cite #346 explicitly.
Underworld development team with AI support from Claude Code
| # method_kwargs: fill in resolution_ratio from strategy | ||
| # if caller didn't already set it. | ||
| if method_kwargs is None: | ||
| method_kwargs = {} | ||
| else: | ||
| method_kwargs = dict(method_kwargs) | ||
| method_kwargs.setdefault( | ||
| "resolution_ratio", _s["resolution_ratio"]) |
There was a problem hiding this comment.
Probed at both branch head and base — the TypeError is real but it is not introduced by this PR's kwargs tightening. At head, smooth_mesh_interior(mesh, metric=..., strategy='med') raises TypeError: _spring_equilibrium_mover() got an unexpected keyword argument 'resolution_ratio' (runtime probe). At the Wave D base, a signature-binding probe shows _winslow_spring, _winslow_elliptic and _winslow_equidistribute never accepted resolution_ratio and had no catch-all — the only mover with **_ignored was _winslow_mmpde, and D-05 deliberately kept resolution_ratio accepted there (explicit resolution_ratio=None parameter). So the base raises the identical TypeError for strategy + spring/ma/ot: pre-existing defect faithfully preserved, not a D-05 regression. Per the wave contract it is not fixed on this branch; commit 300bba4 adds a TODO(BUG) at the injection site (fix: inject only for movers that accept it, or route strategy= to a surviving mover — spring/MA/OT are retired in favour of mmpde), and it has been reported for issue-filing as a live-API defect since 'spring' is the default method.
Underworld development team with AI support from Claude Code
…rkers Comment-and-docstring-only follow-ups to the PR #345 Copilot review: - api.py: follow_metric skip_threshold docstring described an 'alignment >= threshold' skip; the actual logic skips when mesh_metric_mismatch misalignment < threshold. Rewritten to match the code (default 0.9 ~= skip once alignment r > 0.44). - api.py: TODO(BUG) at the strategy resolution_ratio injection — strategy= with the default method='spring' (or 'ma'/'ot') raises TypeError since those movers do not accept the kwarg. Verified pre-existing at the Wave D base by signature-binding probe (only mmpde had a catch-all; D-05 preserved its acceptance explicitly). - metrics.py: TODO(BUG) — mesh_metric_mismatch A_target/A_mean built from rank-local sums makes the delta moments partition-dependent; the alignment/misalignment skip signal is globally reduced and unaffected. Pre-existing, preserved by the split. - mmpde.py: TODO(BUG) in _spd_sanitise — all-NaN eigenvalues on a rank propagate NaN through nanmax -> floor -> projection instead of the intended SPD fallback. Pre-existing, numerics untouched. - spring.py: extend the #346 TODO(BUG) — cite the issue, note the retirement, and record that a collective-exit fix must also guard the rank-local zero-size reductions (dmax line-search seed). - remesh.py / _ot_adapt.py: point readers at the underworld3.meshing.smoothing package instead of the retired single-file smoothing.py. No runtime behaviour change (comments and docstrings only). Underworld development team with AI support from Claude Code
Uh oh!
There was an error while loading. Please reload this page.
Wave D readability rewrites for
src/underworld3/meshing/smoothing.py(rows D-01..D-13 ofdocs/reviews/2026-07/REMEDIATION-WORKLIST.md; evidence indocs/reviews/2026-07/READABILITY-REVIEW.md). Base: development @ 8a94f67. Behaviour-neutral throughout, mechanically verified (bit-identical mover outputs, serial and np2), except the one sanctioned signature change (D-05).Preliminary: elliptic-ma allgather rescue check
The branch-triage ledger flagged a smoothing.py parallel allgather fix on
feature/elliptic-mato rescue before editing. Verdict: already superseded — nothing to rescue, no rows deferred.origin/feature/elliptic-macarries zero smoothing.py commits beyond its merge-base with development (git log development..elliptic-ma -- smoothing.pyis empty).rescue/2026-07-04/elliptic-ma(bloba067550d): allgather the(ref, M_ref)cloud in_winslow_mmpde's rbf bake so a rank-local KDTree can't produce a non-SPD metric.cKDTree(ref)fixed-reference FE bake) no longer exists: commit45132094(fix(meshing): mmpde metric — monotone RBF bake from nodal values #266, 2026-06-22, on development, NOT on elliptic-ma) replaced the bake with a Shepard interpolation from ghosted current-node analytic values — positive by convexity, halo-complete near partition boundaries — plus the_spd_sanitisebackstop. Same failure mode, fixed by construction.Per-row status
_build_M_tensordeleted; closure called once pre-loop; redundant pre-loopgvec/gn/gmax/grefremoved (old0,h0kept)_backtracked_move(area_floor=0.0)+_cap_step_to_edge_fractionextracted from the x3/x2 copies;area_floor=0.0default keeps MA/OT flip-only acceptance bit-identical; MMPDE's structurally different cap NOT folded in_winslow_spring/elliptic/equidistribute/mmpde→_spring_equilibrium_mover/_monge_ampere_mover/_ot_improvement_step/_mmpde_mover(_winslow_anisotropickept). One-cycle DeprecationWarning aliases for all four old names (they appear inscripts/and possibly external user scripts). In-repo sweep: test_0641 (+class rename), test_0850,_ot_adapt.py/remesh.pydocstrings, adaptive-meshing skill. Historical design docs and the 2026-07 review record deliberately untouched_MPIimport (14 inline imports removed) +_global_sum/min/max/meanserial-no-op helpers across ~30 reduction sites; collective participation preserved verbatim (one hazard caught in review: anisogmaxreduction made unconditional again);_global_meandocumented as the historical rank-mean_mmpde_moveracceptsresolution_ratio=Noneexplicitly (strategy dispatch injects it),**_ignoredreplaced by a warn-on-unknown-kwargs catch; typo'dmethod_kwargsnow warn instead of vanishingamp = refinement**(cdim/power)-1+ superseded comment deleted (consumer unreachable whenrefinementgiven);refinement/coarsening/metric_choicedocs describe the envelope branch and what it ignores;'arc-length'added to the envelope comment list, follow_metric ansatz bullets, and follow_metric'smetricdocmethod : {"spring", "ma", "anisotropic", "mmpde"}with mmpde recommended and'ot'deprecated-only bullet; dispatch ValueError now names mmpdealignment(globally-reduced Pearson r) andmisalignment(sqrt(1-max(0,r)^2), the skip criterion, clamp noted)_solver_wiring+_warm_start_krylovhelpers (x3_wirepair, x3_zigdeduped);_zig→zero_init_guess_reweight_displacement_radial_tangentialextracted (x2 verbatim copies); helper checked bit-identical against the inline formula on synthetic data_mean_edge_length(dm, coords)extracted (x2); measure-once/compounding-refinement warning lives on the helper + the_FOLLOW_METRIC_H0_CACHEdeclarationmeshing/smoothing/→graph / spring / monge_ampere / anisotropic / mmpde / metrics / api / __init__(anisotropic.py added to the row's list — the mover is ~600 lines and reuses MA's solver wiring).__init__re-exports EVERY formerly-module-level name (_edge_pairs,_tri_cells/_tet_cells,_pinned_mask,_auto_pinned_labels,_owned_vertex_mask,_signed_areas, movers, wiring, ...); smoothing↔_ot_adaptimports remain lazyBit-identical checks performed
Deterministic mover probe (UnstructuredSimplexBox 1/8, Gaussian metric): spring, MA, OT, anisotropic in all three density regimes +
metric_refresh_per_iter=True, MMPDE, Jacobi path, and themesh_metric_mismatch5-key dict. Probe verified self-deterministic (two base runs identical), then compared to the 8a94f67 baseline build:move_anisotropy=Nonepath).Gates (exact counts)
pytest tests/ -m "level_1 and tier_a"@ 8a94f67: 373 passed, 10 skipped, 1118 deselected, 2 xfailed, 1 xpassed.tests/test_075*.py: 120 passed, 2 skipped, 5 xfailed, 1 xpassed.tests/parallel/test_0855_mesh_smoothing_parallel.py: 4 passed (pre- and post-split).ptest_0763_mesh_adapt_transfer.pyis skipped upstream (PETSc adaptMetric error 63) — unchanged.test_follow_metric_skip_threshold...) is the known pre-existing Manifold-mesh PDE support + extract_surface #202 reconciliation xpass, present in the baseline.TODO(BUG) markers placed
smoothing/spring.py(~line 166): rank-localn_free == 0early return before the collective CG loop — latent MPI deadlock, consistent with the path's documented serial-exact status; marker only (Charter §9 scope discipline).Underworld development team with AI support from Claude Code