Make mesh.cell_size() local and partition independent - #692
Conversation
…#687) Adapt only the mesh-size correction from lmoresi's 68e545f on feature/navier-stokes-supg; do not import Navier-Stokes or other branch changes. Cache _radii_own from current DM vertex coordinates and use it for mesh.cell_size(). Preserve the legacy kd-tree radius arrays and global timestep/mesh-motion consumers. Use coordinate-section offsets and the full vertex stratum so the own-cell RMS definition also handles hexahedra, which have eight vertices but six faces. Correct the field documentation and Nitsche mechanism tests for the new definition; retain physical solve tolerances and use the exact nearest-centroid <= own-centroid ordering instead of an arbitrary approximate-equality tolerance. Add a first-failing independent geometry/deformation regression for triangles, tetrahedra, quadrilaterals and hexahedra plus a regular-square analytical control. Before: four failures in serial and on eight ranks. After rebuild: 21 passed/one expected skip serial (22.90 s), 22 passed on eight ranks (40.45 s), covering Nitsche solves, radius accessors, frozen PC2 migration and memory/disk snapshots. Own-cell geometry error is zero in these tests; style and whitespace gates pass.
|
Reviewed by measurement. The fix works, exactly as claimed, and the defect The fix does what it saysPer-cell values gathered and compared across rank counts on the same mesh
So the new field is bit-identical across partitions and the old one is not. The gap: the property is never checked at more than one rank count
What they assert is a within-rank oracle: each cell's size matches its own This is the shape that has bitten this repo twice in the last month. #675 was Concretely: a Two smaller points#687 duplicates #569 (15 August), which is still open and carries something Scope is right. Leaving Nothing here blocks the approach. The fix is correct and I would merge it with a |
|
Follow-up on "why the scope is narrow", since the obvious next question is
Timesteps are not currently affected, and the reason matters. Every I first measured a ~1e-5 spread in But the insulation is incidental, not structural. The min is stable because What is already partition-dependent today, and is not fixed by this PR:
So the narrow scope is defensible for One more, minor: the None of this needs to be in this PR. It is the follow-on issue, and #569 is |
|
Filed #694 for the other half, with your ruling recorded: The fix is half-built here — Not asking for it in this PR. The scope here is right; #694 is the follow-on. |
|
One naming request before this lands, and it is worth doing now because private
So Neither field is about rank ownership. Both are per-cell local arrays. The
A nomenclature that says that, per Charter §3 ("names state what a thing IS"):
Whatever is chosen, #694 will inherit it: the fix there is pointing |
Rename the new per-cell geometric radius cache from _radii_own to _cell_radii so the name describes cell geometry rather than rank ownership. Update the focused Nitsche and deformation checks accordingly.\n\nAdd an enumerated parallel regression that gathers owned-cell centroid/radius pairs and compares the complete sorted table with a fresh single-rank run on the same cached Gmsh mesh. This directly guards the rank-count-independence claim at np=2, np=4 and np=8 instead of relying only on within-rank geometric identities.\n\nValidated locally with 9 focused serial tests and the new MPI test at 2, 4 and 8 ranks.
|
Addressed the requested merge gap in
Local validation after rebuilding the branch:
This directly tests the title claim across rank counts; the existing independent vertex-geometry oracle remains as the separate correctness/deformation check. The practical Nitsche consequence remains documented in |
|
For sequencing context, this fix is also the direct cause of #689's current sole CI failure ( |
Remove the local_h=False workaround from the boundary-normal MPI regression now that Mesh.cell_size() is partition independent. The test again exercises the public local_h=True default and compares its Nitsche solve with a fresh serial process.\n\nRecord the user-visible consequence in the development changelog: the rank-local centroid kd-tree moved the default Nitsche velocity answer by 6.6e-3, while the cell-geometry replacement is identical cell by cell from one through eight ranks.\n\nValidated the focused Nitsche regression at 2, 4 and 8 Open MPI ranks (10.99 s, 7.31 s and 9.60 s respectively).
|
Thanks. Consolidated answers and the resulting changes:
The branch is pushed at |
…nded cell_size (#692) over the branch's #687 version Same definition (RMS distance of a cell's vertices from its own centroid), one implementation. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL
…pped; Picard reductions on every pass The merge of development took discretisation_mesh.py and test_1065 wholesale from development, losing the orphaned-field packing by name (test_1058) and the rest of the branch's non-conflicting edits; this is the hunk-by-hunk resolution with the landed cell_size (#692). The Picard loop of the Navier-Stokes solver now takes its two reductions on every pass, and the break predicate is recorded as rank-uniform in the collective-guard scan. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL
…independent cell size (#687) (#688) * Expose the BDF and Adams-Moulton coefficient symbols on the DDt managers A solver that assembles its own weighted sum of history terms (an Eulerian scheme applying a multistep rule to a spatial operator) needs the constants-routed coefficient expressions, not just their current values. Read-only accessors; no behaviour change. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Pack and index auxiliary fields by DM field, not by position in mesh.vars A MeshVariable that is dropped and garbage-collected (the default Model holds the only strong reference; uw.reset_default_model() releases it, and the statistics helpers delete temporaries deliberately) leaves its PETSc field in the DM. Mesh.update_lvec zipped mesh.vars.values() against the field decomposition by position, and the JIT's petsc_a[] offsets were a running count over the live variables, so every later variable was packed into, and read from, the wrong slots. Measured: a P0 cell-size field landing in a P2 slot as garbage, NaN residuals in one run and a subtly wrong answer in the next, depending on when the collector ran. update_lvec now packs by field name and zeroes an orphaned field; the JIT reads component offsets from the DM's own field list and patches each variable from its field_id. Regression test: 2 of its 3 checks fail without the fix. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Add the RotatingGaussian transport oracle; fix the integral-norm error for scalar variables A Gaussian carried round the origin by rigid rotation while diffusing is exact at every time (rotation commutes with the Laplacian), so a transport scheme's error can be measured directly and the round trip after one revolution is an absolute check. AnalyticSolution.error(norm='integral') added a 1x1 Matrix symbol to a scalar expression and had never been exercised on a scalar variable. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Extract the per-element timestep estimate shared by the advection-diffusion solvers The cell-crossing / diffusion-time reduction (isotropic or direction-aware, minimum or percentile) becomes a module-level helper so the Eulerian solver can call it rather than carrying a copy. SLCN behaviour unchanged. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Skip the mesh-owned multigrid pickup for a solver that owns its preconditioner A solver with no managed option block (_pc_option_prefix is None) sets its own PC; installing the adapt child's PCMG hierarchy on it segfaulted inside PETSc (additive-Schwarz PC, PCMG calls). The gate now treats that state as the explicit choice it is, alongside preconditioner='gamg' and the user override latch. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Eulerian advection-diffusion with SUPG: BDF and Adams-Moulton orders from the symbolic history uw.systems.AdvDiffusionSUPG(mesh, T, V_fn, order=N, integrator='bdf'|'am') assembles the implicit weak form from the Eulerian DDt history: the BDF stencil or the Adams-Moulton weights on the advective and diffusive terms at every stored time level, plus the SUPG flux tau R u with the strong residual of the same scheme. Timestep, multistep coefficients and the tau weights are runtime constants of the compiled kernels, so a change of dt costs nothing (the issue #657 prototype recompiled on every change). Diffusivity comes from the constitutive model like every scalar solver. Measured on the rotating Gaussian: stable at any cell Courant number, error set by u dt against the feature width (dt^2 for the second-order schemes), unchanged to three digits by a band refined to h/9 at local Courant 13; Crank-Nicolson reproduces the prototype's numbers to four digits. Tests: API and no-recompile contract, temporal convergence (slopes 0.8/0.9 for BDF1, 1.9 for BDF2), band invariance, round trip, np=2 = serial. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Design note for the Eulerian SUPG solver; BDF2 becomes the default from the integrator study Rotating-Gaussian study at res 32, Courant 0.25 to 8, pure advection and kappa 1e-3: Adams-Moulton above order 1 blows up from Courant 1 (bounded stability region), BDF3 fails from Courant 4, Crank-Nicolson is three to four times more accurate than BDF2 at the same timestep but rings once the feature is under-resolved in time, backward Euler carries 20-40% error at any practical timestep. Cost per step is the same for every scheme. BDF2 is the robust default; the note records the alternatives and when to pick them. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Integrator study, res 64: BDF3 grows slowly on pure advection at any Courant number BDF2 and Crank-Nicolson track their res-32 errors at the same u dt. BDF3's stability region misses the imaginary axis near the origin, so the low-frequency modes of a finer mesh grow: 31x the exact field after 590 steps at Courant 1. Safe only with diffusion, below Courant 2. Note and docstring updated; the BDF2 default stands. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * AdvDiffusionSUPG takes the semi-Lagrangian solver's interface: a drop-in replacement The constructor, order, theta, f, V_fn, constitutive_model, delta_t, estimate_dt and solve keep the meaning they have for AdvDiffusionSLCN, so a script changes the class name and nothing else. order=1 with theta=0.5 is Crank-Nicolson and the default, as for SLCN; order=2 takes theta=1 (BDF2) unless 0.5 is asked for explicitly, which is refused for the reason the SLCN documentation gives. The trace-back-only arguments (restore_points_func, monotone_mode, old_frame_traceback, DFDt) are accepted and ignored with a warning. integrator is inferred and only needs setting to reach the higher Adams-Moulton rules. delta_t is settable and solve() reuses it; the notebook viewer reports the scheme. User page docs/advanced/eulerian-advection-diffusion.md with the swap table and the when-to-use-which guidance. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Drop the integrator argument: order and theta already reach every safe scheme The only schemes the argument added were Adams-Moulton at orders 2 and 3, which the integrator study shows blowing up on advection from Courant 1. The multistep family now follows the order (the theta rule at order 1, BDF above); the higher Adams-Moulton assembly stays in the code, reachable only by switching the family on the instance, which is how the study measured it. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * An accuracy-based timestep for the Eulerian solver; credit NengLu in the module and note estimate_dt now returns the step at which the field changes by a fraction (0.02) of its range: from the advective rate |u . grad phi| at the vertices before the first solve, and from the rate the last step actually produced after it. The cell-crossing time the semi-Lagrangian solver reports is not a stability limit for this scheme and says nothing about its accuracy; it stays available as basis='resolution'. The estimate is mesh-independent, which the band test now checks (the resolution estimate collapses 3x on the refined child, the accuracy estimate moves under 25%), and at the default fraction Crank-Nicolson completes the rotating-Gaussian round trip under one per cent. The advective rate uses the vertex Clement gradient rather than a point evaluation of a derivative expression, which fails on a mesh carrying many variables. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Match the Krylov tolerance to the SNES tolerance, and make preconditioner="fmg" a real switch on the SUPG solver The Eulerian SUPG step took two Newton iterations on a linear operator: the Krylov default (rtol 1e-5) does not reach the SNES tolerance (1e-8), and the second Jacobian assembly cost more than every linear solve of the step. The Krylov tolerance is now 1e-9 and a step is one Newton iteration: 1.54 s to 0.91 s per step at 256^2 in serial. Measured against geometric multigrid at matched tolerances (design note, "Preconditioner"), GMRES with additive-Schwarz ILU is the cheaper linear solve at every Courant number from 1/2 to 32 and its iteration count is the same on one and eight ranks; the multigrid's cycle count grows with the Courant number nearly as fast, and a cycle costs about three Schwarz iterations. Schwarz stays the default on every mesh. preconditioner = "fmg" now hands the block to the managed multigrid route (custom-P transfers over the refinement hierarchy or an adapt child's coarse tail, flexible GMRES outside) for the rank count where a one-level method runs out of coarse space. The solver's solve() builds through the base _build, where a preconditioner choice is resolved; the pre-run of the three setup stages marked the solver set up first, so the request was silently inert. The semi-Lagrangian solvers share that pattern and the defect (#683). Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Design note: the 512^2 rows at matched tolerance * Let theta be set after construction, as the semi-Lagrangian solver allows The shipped convection examples set adv_diff.theta = 0.5 after building the solver; the Eulerian drop-in refused it. The blend is a runtime constant refreshed from the history manager before every solve, so the setter updates it without a recompile (order 1 only, the constructor's rule). Vector and tensor unknowns join the design note's deferred list: the solver is scalar, where the semi-Lagrangian trace-back carries them. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Navier-Stokes with Eulerian SUPG momentum transport, and a partition-independent cell size uw.systems.NavierStokesSUPG: the incompressible Navier-Stokes equations on the Stokes saddle-point solver with the momentum advection assembled implicitly and stabilised by the vector SUPG term F1 = tau R (x) a, the counterpart of the scalar Eulerian solver. Crank-Nicolson at order 1, BDF2 at order 2, with the velocity history on the mesh; no stress history, the viscous stress at an earlier level is rebuilt from the stored velocity through the constitutive model. The advecting velocity is a choice: the second-order extrapolation 2u^n - u^{n-1} (one linear solve per step, the default), Picard passes on the latest iterate, or the unknown itself under Newton. The strong residual the SUPG term sees carries the pressure gradient; without it the term is O(1) at the exact solution and costs fifty times the Galerkin error on Kovasznay flow. mesh.cell_size() now reports each cell's own radius, the RMS distance of its vertices from its own centroid, taken from the DM's coordinates. The kd-tree radius it used to copy picks the nearest centroid among the rank's cells, so the field differed with the partition (#687, found because the two-rank Navier-Stokes answer differed from serial by 5e-4 and matched to 1e-15 with a constant h); after a deform it also read stale vertex coordinates against fresh centroids. get_min_radius and the other consumers of the kd-tree radii are unchanged. Tests: the solver's API contract (construction rules, one linear solve per step, Picard passes, the Stokes limit, runtime-constant timestep and theta), a two-rank Kovasznay error that matches serial to 1e-7, the scalar parallel reference re-recorded for the new cell size, and the Nitsche local-h tests reading the field's definition. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Document the SUPG Navier-Stokes solver: user page and the design-note section with Kovasznay and cavity results Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Design note: cavity Courant 1 row, cylinder wake rows, and the corrected Re 1000 status * Design note: the semi-Lagrangian cylinder row * Design note: Re 1000 cavity rows, the finer cylinder mesh, and the Galerkin control that cannot run * Swarm.advection: let estimate_dt see a rank that holds no particles (#693) The velocity evaluated for the timestep estimate has shape (0, 1, dim) on an empty rank, and reshape(0, -1) cannot infer the trailing size; the empty-rank handling a few lines below never ran. Give reshape the size explicitly. Found with passive tracers released at the inlet of the DFG cylinder on four ranks, where every rank but the inlet's is empty at the first step. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Set the PETSc constants on the DS the integrals use, so expression values reach the kernels (#695) uw.maths.Integral, BdIntegral and CellWiseIntegral compile their integrands through the same JIT as the solvers, which routes every uw.function.expression to PETSc's constants array, but none of them ever called PetscDSSetConstants: the kernels read zeros, so any integrand with a viscosity, a time or another expression in it integrated to nothing, and a fresh Integral returned the same zero from the cache. Found on the DFG cylinder drag, where the viscous traction (eta is an expression) vanished and the drag read 23 to 28% low on two meshes without moving with the SUPG weights. Each class now packs the manifest and sets the constants right after the objective; the boundary integral sets them on its sandbox DS, which has its own discrete system. Regression test test_0503 covers the three classes, a changed value without recompilation, and the constitutive-flux traction that found it. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Design note: the cylinder drag was the missing viscous traction (#695), the vortex-decay benchmark, and #696 The DFG cylinder section is rewritten around what the tau sweep found: the stabilisation moves the drag by 2.6% and the deficit was the boundary integral dropping the viscous part (#695). With the integrals fixed and only the cylinder cells refined through gmsh at a fixed time step, drag, pressure difference and Strouhal number converge onto the reference bands on the 1/20 channel mesh, the traction and reaction measurements close on each other, and the whole-mesh 1/40 run buys less than the 1/320 cylinder cells do. The Galerkin form that "could not run" was the GAMG fallback; the refinement callback gives FMG on the gmsh mesh. New Taylor-Green vortex-decay subsection (dt and h sweeps for CN and BDF2, Galerkin against SUPG, the viscosity range, the advecting-velocity choices), and the two defects it found: #695 and the zero-valued expression folding (#696, raised, not patched). Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Design note: the FMG rows of the cylinder table (base mesh refined through the circle callback) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Design note: the Picard row at 1/640 cylinder cells settles the lift overshoot as the extrapolation lag Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Take swarm.py from development (#680): the empty-rank estimate_dt guard supersedes the branch's reshape fix Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Design note: LU is serial-only on the velocity block; parallel tracers run with #680 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Design note: the FMG cylinder-refinement table (Picard, Newton, BDF2, four ranks) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * NavierStokesSUPG: an opt-in recovered viscous term in the SUPG residual recovered_viscous=True projects the deviatoric stress of the advecting velocity onto a continuous symmetric tensor before each solve pass and puts its divergence in the strong residual the SUPG term sees. Without it the residual lacks the viscous term (the kernels see first derivatives only), an O(h^2) inconsistency for P2 velocity that shows on resolved viscous flow: four times the Galerkin error on the 1/64 vortex-decay mesh, sixteen times on Kovasznay at 1/32. The projection's function is set on first use (it needs the constitutive model) and its default is a zero matrix, not None. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * NavierStokesSUPG: the recovered viscous term is the previous level's momentum balance The differentiated projection of the stress was unstable (1/64 vortex decay and Kovasznay at 1/32 blew up) and did nothing at 1/32. Louis's form: the momentum balance of the stored level gives div sigma^n = rho (Du/Dt)^n + grad p^n - f from first derivatives of stored fields, so the residual the SUPG term weights becomes the increment of the out-of-balance force between levels, at the cost of one stored pressure level and no extra solve. At a discrete steady state that residual vanishes and the stabilisation switches off, which is a property to measure, not assume. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Design note: the recovered viscous term measured (balance form = Galerkin accuracy on resolved flow, unstable on the cylinder) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * NavierStokesSUPG: recovered_smoothing projects the balance term with a screened-Poisson length The plain balance term is unstable where advection dominates because it carries the grid-scale residual of the previous step. With a smoothing length the term is projected onto a continuous vector field (one vector projection per step), keeping the smooth viscous divergence and filtering the rest; zero keeps the plain form. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Revert "NavierStokesSUPG: recovered_smoothing projects the balance term with a screened-Poisson length" This reverts commit 19edda1. * Revert "NavierStokesSUPG: the recovered viscous term is the previous level's momentum balance" This reverts commit c5c72ec. * Revert "NavierStokesSUPG: an opt-in recovered viscous term in the SUPG residual" This reverts commit 42aaedf. * Design note: the recovered viscous term measured three ways and withdrawn Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * NavierStokesSUPG: tau_shape selects the Brooks-Hughes or doubly asymptotic parameter The inverse-sum tau (Shakib-Tezduyar) is above the optimal 1-D curve at cell Peclet numbers of order 1 to 10, where the resolved benchmarks sit. The optimal shape tau = (h/2|a|)(coth Pe - 1/Pe) and its two-limit approximation (h/2|a|) min(Pe/3, 1) are now selectable, each combined with the transient term so the time step still caps them. coth is written through tanh: the C printer rewrites coth through exp and drags the square root in |a| into exp(log(.)), which brings arg() into the kernel. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Design note: the shape of tau measured (Brooks-Hughes, doubly asymptotic) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Design note: the Re 1000 cavity converged (94 to 96% of Ghia at 1/64); the rank-local v_max explained Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * NavierStokesSUPG: peclet_weight turns the SUPG term off where the cell is diffusion-dominated The term is multiplied by Pe^2 / (Pe^2 + Pe_c^2) with Pe the cell Peclet number of the advecting velocity, so it is absent where it is not needed (where it costs a fixed multiple of the Galerkin error) and full where advection dominates. Zero (default) leaves the weight uniform. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Design note: the weight by cell Peclet number measured (Galerkin accuracy where resolved, stabilisation kept on the cylinder) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * NavierStokesSUPG: the cell-Peclet weight is the default (Pe_c = 4) Louis's ruling, the code being unreleased: the SUPG term is weighted by Pe^2 / (Pe^2 + 16) by default, off where a cell is diffusion-dominated and full where advection dominates. Kovasznay at 1/8 (the parallel test's reference) goes from 3.83e-3 to 1.42e-3; the design note's earlier tables were made at the uniform weight and say so. The scalar transport solver keeps the uniform weight until its convection benchmarks are re-measured. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * AdvDiffusionSUPG: the cell-Peclet weight, as for the Navier-Stokes solver (Pe_c = 4) Written without dividing by kappa, so pure advection (the default kappa = 0) keeps the uniform weight and its tests do not move. The convection benchmarks are re-measured with it in the design note. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Docs: the cell-Peclet weight on the scalar solver's user page Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Design note: the cell-Peclet weight is the default of both solvers; the convection rows with it Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Examples: the SUPG Navier-Stokes solver on the lid-driven cavity and the Taylor-Green vortex Two runnable examples in the repository's format: the cavity at Re 100 against Ghia (about four minutes) and the Taylor-Green vortex decay with its exact error and energy decay (about a minute). Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * The DDt history manager is the transport plugin: EulerianSUPG assembles advection and SUPG, the solvers compose A solver that owns an unknown now composes its residual from three contributions of its DuDt (time_derivative, advection, stabilisation_flux) plus the levels and weights of the scheme (states, spatial_weights), and never asks which flavour it holds. The new ddt.EulerianSUPG assembles the implicit advection component-wise for a scalar, vector or tensor unknown and the SUPG flux tau R (x) a of the solver's strong residual; the history-carrying flavours answer zero for both. V_fn is data on the manager (V_fn_history names the carrier of the stored levels, the stored velocity for momentum), the timestep is a runtime constant every flavour writes (delta_t), and the stabilisation knobs live on the manager with the solvers' properties passing through. AdvDiffusionSUPG and NavierStokesSUPG lose their own residual code and compose the same way. A SemiLagrangian manager dropped into the scalar solver reproduces AdvDiffusionSLCN on pure advection; a flattened symmetric tensor is transported through SNES_MultiComponent with a residual that is only the manager's terms (test_1057). The plain Eulerian manager keeps its explicit splitting correction behind an _advection_mode gate and gains num_components for MATRIX histories. Regression: Kovasznay 1/16 and 1/32, the vortex decay at 1/32, the Blankenbach box and both examples reproduce their recorded numbers to every printed digit; the cylinder keeps its mean drag, lift extrema and Strouhal number, with the drag peak moving 3.0797 -> 3.0802 (evaluation order in a shedding wake). Two-rank tests keep their serial constants. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * The composing solvers take the generic names: AdvDiffusion and NavierStokes; the semi-Lagrangian classes keep their SLCN names A solver that composes its transport from its DDt manager is not an SUPG solver: SUPG is a property of the EulerianSUPG manager it holds by default, and a SemiLagrangian manager makes the same solver a semi-Lagrangian scheme. So uw.systems.AdvDiffusion and uw.systems.NavierStokes now name the composing solvers (SNES_AdvectionDiffusion_Composed, SNES_NavierStokes_Composed) and the SUPG class names are gone. The semi-Lagrangian classes stay reachable as AdvDiffusionSLCN, NavierStokesSLCN and NavierStokesSwarm; every existing use of the generic names with the semi-Lagrangian meaning in docs, notebooks, examples and tests is moved to the explicit SLCN name, so nothing changes scheme. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Restore the branch's mesh changes the whole-file merge resolution dropped; Picard reductions on every pass The merge of development took discretisation_mesh.py and test_1065 wholesale from development, losing the orphaned-field packing by name (test_1058) and the rest of the branch's non-conflicting edits; this is the hunk-by-hunk resolution with the landed cell_size (#692). The Picard loop of the Navier-Stokes solver now takes its two reductions on every pass, and the break predicate is recorded as rank-uniform in the collective-guard scan. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Parallel tests: a refinement hierarchy for the Navier-Stokes reference, platform-tolerant comparison to the serial error The GAMG fallback on a mesh without a hierarchy gave a platform-dependent answer (7% on the Linux CI); the test now refines a 1/4 mesh once so the velocity block runs geometric multigrid, and the serial reference (0.00132279) is met by two and four ranks to 3e-10. Both tests compare to the serial error at 1e-6 relative: the partition effect they guard against was 5e-4 (#687), platforms differ at 1e-7. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Adversarial review of the plugin and the rename: six fixes From three reviews of the branch head (findings posted on #688): - the base contract's shape helper collided with Symbolic's `_shape` attribute, so Symbolic.advection() raised instead of answering zero; renamed; - a user-supplied EulerianSUPG on NavierStokes advected the stored level with the new velocity: the solver now sets V_fn and V_fn_history whoever built the manager, and its advection setter only steers such a manager; - the change-rate bookkeeping read the manager's history `.array`, which fails for a SemiLagrangian history under units and for a swarm-backed history; it now diffs a copy of the unknown's data; - a supplied manager silently overrode `order`/`theta`; a mismatch is an error, and the theta setter refuses a manager without theta; - the 1-D tau shapes divided by the diffusivity (zoo at the manager's default); - the timestep and SUPG knobs are created with unique names like the BDF coefficients, so they do not accumulate in the persistent registry; - a bare scalar residual is accepted by stabilisation_flux. Rename loose ends: an example that imported the bare NavierStokes name now uses NavierStokesSLCN explicitly; tutorial 9 prose; the solver-unification design table; API entries for the composing classes and the manager. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * DDt: a quantity timestep is non-dimensionalised before it reaches the kernels (#701) _as_float took the magnitude of a Pint or UW quantity, so a semi-Lagrangian solver stepped with 100 kyr under a 1 Myr reference time wrote 100 (not 0.1) into the manager's runtime timestep and into the variable-step BDF bookkeeping. It now goes through uw.non_dimensionalise, which handles both quantity types; without reference scales the magnitude is what comes back. Test with a negative control in test_1057. Closes #701. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Swarm.advection: a clear error for a swarm that was never populated (#702) DMSwarm reports a local size of -1 until particles are added on some rank, and the advection then failed inside numpy with 'negative dimensions are not allowed'. The empty rank of a populated swarm (size 0) is unchanged. Closes #702. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * UWexpression never reports is_zero, is_positive or is_negative from its value (#696) A runtime constant's value can change after construction, so sympy must not fold on its current sign or on it being zero: exp(c) with c created at 0 evaluated to 1 at construction and a time ramp that started at t = 0 stayed frozen (found on the Taylor-Green Dirichlet case). The three assumptions now answer None, as for a plain Symbol; the value is read when the expression is unwrapped for compilation. Control in test_0503: exp(c) survives, integrates to 1 at c = 0 and to e at c = 1. Level-1 suite: 1704 passed. Closes #696. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL * Copilot review of #688: NavierStokes.estimate_dt dimensionalises the accuracy estimate; EulerianSUPG takes no mutable default bcs The accuracy basis returned a bare non-dimensional number while the resolution fallback returns a quantity under a scaling model; both now come back through _dimensionalise_dt (test under reference scales). The manager's bcs default is None -> a fresh list; a caller's list is still kept by reference on purpose, so a solver's live essential_bcs reach the projections. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2VHUGaZiQVJ95qQ4DiSL --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Summary
Fixes #569 and fixes #687 by making
Mesh.cell_size()a per-cell geometric quantity that is independent of MPI partitioning.The previous field used
self._radii, whose nearest-centroid kd-tree contains only centroids held by the current rank. Near partition boundaries, the nearest available centroid can therefore change with rank count. Nitsche penalties and SUPG stabilization terms built frommesh.cell_size()consequently changed with the partition.Change
_cell_radii; the name describes cell geometry and does not overloadown/owned, which elsewhere denotes MPI-rank ownership._cell_radiionly formesh.cell_size()and refresh its field after mesh deformation through the existing reinitialization path.cell_size()and Nitsche test documentation to state the exact definition and the default Nitsche local-hconsequence.Why the scope is narrow
This does not redefine
get_min_radius(),get_max_radius(), orget_mean_radius(). It corrects only the local field consumed by spatially varying stabilization and penalty terms. Follow-up issue #694 tracks the global accessor contract.Regression coverage
hmagnitude, deformation refresh, and free-slip solve checks.tests/parallel/regression that gathers every owned-cell(centroid, cell radius)row and compares the complete sorted table with a fresh np=1 process using the same cached Gmsh mesh.Validation
The Stokes case emits the existing expected GAMG fallback warning because its tiny mesh has no multigrid hierarchy; all assertions pass.
This is split from #689 so the generic mesh-size correction can be reviewed and merged independently of Eulerian SUPG transport.
Underworld development team with AI support from Claude Code