VoF campaign — handoff for the next session¶
Status 2026-09-02 (architect, finishing campaign started). Items 1–4 below are the 2026-08-31 handoff; their state now:
- Item 1 — precision: RESOLVED by the defect-correction campaign (P1 passed, double-diagonal
retired;
DEFECT_CORRECTION_PLAN.md). Open for VoF: whetherenable_vofshould switch the exact residual on (WO-R item 6 measures Hysing case 2 both ways). - Item 2 — Lamb mode-2: TRACED IN PART. (a) Cheapest hypothesis first, as instructed: the
capillary WAVE deviation is 100 % the inviscid reference. The exact normal-mode relation for an
interface between two semi-infinite fluids of equal ρ and ν is
s² + ω₀²(1 − k/√(k² + s/ν)) = 0(derived from the even-ŵ solutionA e^{−kz} + B e^{−mz},m² = k² + s/ν, continuity of ŵ, ŵ′, ŵ″ and the normal-stress jumpμ[ŵ‴] = −σk⁴ŵ(0)/s; first order:Δω/ω₀ = −γ/ω₀ = −k√(ν/ω₀)/(2√2)). With the finite-depthcoth(kH)factor the measured ω agree to −0.03 %, −0.23 %, +0.52 % (WO-P's −2.20 / −2.07 / −3.64 % against the inviscid formula), and the measured decay is within 5–24 % of−Re s(a two-extremum fit). The reference2νk²WO-P used is the free-surface rate; the two-fluid rate is the O(√ν) boundary-layer term, 3–4× larger. (b) The DROP is different: a viscosity sweep μ = 0.02 → 0.00125 at 32³/R = 8 leaves the frequency deficit flat at −6.95 → −6.3 % while the damping scales as √ν as it should, so the deficit is inviscid; and a static test of the cascade's κ on the initial prolate spheroid puts the P2 component within +2.6 / +1.0 / +0.55 % of the analytic value at R = 8/12/16, so it is not a static curvature bias either. Confinement accounts for ~0.6 % (−6.27 % at 32³ → −5.63 % at 48³, same R = 8). Remaining candidates under test: the moment-based frequency measurement (a damped-sinusoid fit on saved series), the dynamic (advected-fraction) curvature, and a resolution ladder at fixed confinement (R = 8/12/16). The quasi-2D cylinder mode-2 case gave erratic deficits (−4.7 / −8.9 / −3.9 % at R = 8/12/16), which points at the measurement. Findings will be appended here.
Addendum (same day, all at ν = 0.0025 unless stated; scratch scripts lamb_*.py,
drop_modes.py, dispersion.py, fit_series.py in the session scratchpad):
- Measurement is sound: a damped-sinusoid fit of the saved P2-moment series returns the
zero-crossing frequency to 0.01 %; the polar half-height and the equatorial half-width give
−5.7 / −5.0 % against the moment's −5.6 % (48³, R = 8); volume drift 2e-14.
- Exact viscous drop modes computed (unsteady Stokes, equal ρ and ν, potential + poloidal
vortical parts, 4×4 determinant): Δω/ω₀ = −0.885·√(ν/(ω₀R²)) to first order — −5.0 % at
ν = 0.02, −1.8 % at ν = 0.0025 (R = 8). The simulation captures only part of it (its
damping is 25–43 % below the exact −Re s) because the interfacial boundary layer
√(ν/ω₀) is 0.1–0.45 cells; a resolved-viscous Lamb test needs R ≳ 32.
- Inviscid residual after subtracting the exact viscous shift: −3.9 / −3.4 / −3.9 % at
R = 8/12/16 (48³/72³/96³, φ = 1.9 %) — resolution-independent. The quasi-2D cylinder
(mode 2, ω² = 6σ/(2ρR³)) shows ≈ −2.5 % after the same subtraction.
- Not the estimator: with the curvature FROZEN to the exact curvature of the moment-fitted
spheroid every step (set_vof_kappa_frozen + set_field("kappa")), the deficit is
larger, −9.0 % (ν = 0.0025) / −9.5 % (0.02); the cascade's κ carries a +3 % P2
over-estimate that partly compensates. Dynamic (advected-fraction) P2 content of the
cascade's κ stays within +2…+5 % of the fitted spheroid's over the whole oscillation.
- One-step impulse test (spheroid at rest, one CSF step, projected velocity along the axis
against the exact potential-flow impulse response with η₂ = εR, δκ₂ = 4η₂/R²):
0.94 inside / 0.98 outside, resolution- and κ-source-independent. BUT the planar standing
wave gives 0.956 by the same instrument while its frequency is exact to 0.03 %, so a
weak first impulse does not by itself set the frequency — the instrument is not decisive
and the analysis needs the full linearized discrete loop.
- Where it stands: a ~3.5–4 % inviscid, resolution-independent mode-2 frequency deficit
specific to curved interfaces (2-D cylinder ~2.5 %, 3-D sphere ~4 %), not attributable to
the reference, viscosity, confinement, amplitude, time step, the measurement or the
curvature estimator. The untested link is the transport: whether WY advection of a
curved interface by the mode's velocity field moves the P2 moment at the exact rate — a
kinematic test with the prescribed potential-flow field, runnable once WO-Q's
advect_vof(dt) exists. Recorded as an open measured deviation; the example page states it
as such (Basilisk's own oscillation.c reports a few % at comparable resolution, so it is
not exotic, but ours does not shrink with resolution and theirs does).
- Transport link CLOSED (same day, with WO-Q's advect_vof): freezing the projected
velocity after one NS step and advecting the exact spheroid kinematically, the WY rate of the
P2 moment equals the exact conservative rate Σ_f u_f C_f Δf (exact face fractions of the
spheroid, f quadratic so Δf is exact) to 1.0001 at R = 8/12/16. The colour transport
is not the mechanism. (A cell-centre-velocity form of the same identity is off by 4 / 2.8 %
at R = 8 / 12 — the discrete velocity has cell-scale structure across the interface band,
the same signature as the impulse test.) What remains is the response of the discrete
momentum/projection to the CSF band on a CURVED interface: resolution-independent in cell
units (the band is always 1–2 cells), ~2.5 % (2-D) and ~4 % (3-D) in ω. Next instrument, if
pursued: the linearised discrete normal-mode problem (band force + 7-point projection) on a
sphere, or a smeared-sheet analysis of the induced interface velocity at O(ξ/R).
- Item 3 — V3 curvature issues: carried into WO-S/WO-U as measurements (branch census on
every page); no change to the cascade planned before the examples exist.
- Item 4 — V5–V7 scope: V5 is split into V5a (transport in cut cells, WO-Q) and V5b (the
θ-fill, WO-S), a new V-BC rung (WO-R) covers open boundaries, V8 is pulled forward in minimal
form (WO-T) because the examples must run on the collocated grid too. See VOF_PLAN.md §12.
Written 2026-08-31 at the end of the session that built V0–V4. Read this first, then
VOF_PLAN.md (the campaign plan, now carrying measured results), then the findings logs in
flow/doc/history/vof_workorders*.md. Everything below is decided and unstarted — the user
authorized these four items and deferred them to a later session because budget was low.
Where the campaign stands¶
Part I rungs V0–V4 are complete, gated on host-openmp and nvidia-cuda, np 1/2/4 bitwise, single-phase regression +0.00% throughout. 26/26 kernel ctests, 60/60 MPI ctests.
| rung | what shipped | headline number |
|---|---|---|
| V0 | src/vof/plic.hpp — SZ/Lehmann–Gekle plane↔volume, MYC normals, slab flux |
round-trip 6.7e-15 over 1e5 samples |
| V1 | src/vof/advect_wy.hpp — Weymouth–Yue split advection, own g=3 halo |
volume drift 2.1e-16 over 1024 steps |
| V2a | src/vof/colour_field.hpp — C in the solver, closures, interface-local CFL |
∂P/∂z to 1.1e-15 at ratio 1000 |
| V2b | src/vof/momentum_advect.hpp — momentum-consistent transport |
uniform-velocity identity bitwise on advection |
| V3 | src/vof/curvature.hpp — HF cascade + paraboloid fallback |
sphere order 2.26 (L1) / 1.86 (max) |
| V4 | src/vof/surface_tension.hpp — balanced-force CSF + capillary dt |
stationary droplet max|u| 1.93e-17, Ca 1.9e-18 |
Four pre-existing production defects were found and fixed en route (rank-unaware domain BCs
across 12 sites, body-force ghosts, drag_beta ghosts, the MG prolongation ghost), plus two
published-algorithm errors corrected (Lehmann & Gekle's Listing 1 case boundary; Han et al.
eq. 14f sign).
Item 1 — Precision: evaluate defect correction FIRST, then the double-diagonal¶
⚠ EVALUATE THIS BEFORE IMPLEMENTING ANYTHING (user proposal, 2026-09-01). The user proposed the scheme WO-M never considered, and it may make the double-diagonal unnecessary:
- Compute
r = b − A_exact·xwithb,A_exact,xall in double — matrix-free, asr(x), which also generalises to a nonlinear residual.- Solve the correction approximately in float:
A_float·dx = −(float)r, thenx ← x + (double)dx.
Why this is stronger than the double-diagonal: it makes the float operator's errors
irrelevant rather than smaller. Once the residual and matvec use the true operator, the float
hierarchy is a pure preconditioner, and a preconditioner's errors affect only the
convergence rate, never the fixed point — so A·1 = 0 breaking inside it stops mattering. The
double-diagonal instead patches the operator so it can keep serving as both the problem
definition and the preconditioner; defect correction removes that double duty.
Why it is cheap here: buildCutcellOp(OpV AC, …, CCConst ox, CCConst oy, CCConst oz, …)
(mac_pressure.hpp:24) assembles the bands from the openness, which is already double
(CCField = View<double*>, mac_cutcell.hpp:22), and the coefficient is a local function of
openness and the ρ face mean. So the fine-level matvec can be matrix-free in double with no
new storage — and the fine-level float bands could be dropped, saving ~28 B/cell instead of
adding 17. On a bandwidth-bound kernel that trades flops for traffic in the favourable
direction. Coarse levels stay float; they only live inside the preconditioner.
Caveats to check: the optional star overlay (fluid-only constraint, star_elimination.hpp,
used by the collocated ghost path) applies extra couplings as a separate delta, so a
matrix-free fine matvec must carry it or be restricted to the standard path. And the
conditioning ceiling does not move — this removes the float floor, not eps_f64·κ.
Verdict of the evaluation (2026-09-01, by source inspection — no run yet)¶
Go. Every premise holds, the change is smaller than the proposal assumed, and on the one identity that matters it is strictly stronger than the double-diagonal. Three corrections to the framing above, in descending order of importance.
RESULT (2026-09-01): P1 PASSED, and it retires the double-diagonal rather than deferring it. Measured on the RCP bed, PCG rtol 1e-8 cap 300, nvidia-cuda (P1 session, plan §4 carries the full table and traces):
| Ng | float bands | exact residual | DIAGRESUM | full double |
|---|---|---|---|---|
| 48 | 24, div 4.51e-06 | 14, div 9.51e-12 | 14, div 6.19e-10 | 14, div 9.51e-12 |
| 64 | 33, div 6.48e-07 | 14, div 9.53e-12 | 14, div 1.24e-09 | 14, div 9.53e-12 |
| 96 | 300 CAPPED (invalid), div 4.36e-06 | 28, div 3.17e-11 | 28, div 2.04e-09 | 28, div 3.17e-11 |
Not a shared floor (the trap the collocated finding warned of): the columns do not all agree — float separates sharply — and the residual traces show float plateauing then rebounding (floor 1.115e-08 at it 14, final 8.657e-04, rebound ×7.8e4; Ng=96 ×1.6e5) while every exact solve is monotone, rebound ×1, crossing rtol far under the cap. The warm leg inverts the worry outright: Ng=96 float is [45, 300, 300, 300] across a 4-step march — the cold solve converges and the warm ones cap — against exact [21, 25, 27, 28], march-stable.
The decisive number is the one this evaluation did not predict. Exact and DIAGRESUM agree on
iteration counts but separate 65× on flux divergence (9.51e-12 vs 6.19e-10 at Ng=48; 3.17e-11 vs
2.04e-09 at Ng=96), while exact matches full double to every printed digit. The double-diagonal
converges to the float-face operator's solution; the exact residual converges to the true one.
So the bitwise-vs-eps_f64 distinction argued below is not academic — it shows up in a physical
output, and the fallback is measurably worse, not merely unnecessary. The double-diagonal is
retired as an option, not held in reserve.
Correction to point 1 below (found by the P1 session, verified here). matvecOverlap is NOT
the single choke point for all three drivers: solveBiCGStab has its own inline matvec lambda
(mac_cutcell_mg.hpp:995 distributed-g2 branch, :1003 single-rank) calling applyCutcellOp with
the float bands directly, because the g=2 gp staging needs it. P1 therefore covers solvePCG and
the flexible PCG only; the two BiCGStab sites belong with P2, which owns the gp overlay that is
the reason BiCGStab has a separate matvec at all.
Correction to the cost claim (P1 measurement). Per-matvec the exact form is slower, not
free: +6.5 % (N=128) / +16.8 % (64) / +31.2 % (96). The byte count was right (24 vs 28 B/cell, 0 B
new storage confirmed) and the access pattern was not — the bands are 7 perfectly coalesced loads
all at index i, while the flux form loads at i and i ± sx/sy/sz, touching more distinct cache
lines. At step level it is +0.1…+0.4 %, because the matvec is a small part of the step. This is
P3's go/no-go input, not P1's: P3 moves that penalty into the smoother, which runs many times
per V-cycle instead of once per PCG iteration.
1. No outer defect-correction loop is needed — the structure is already there. solvePCG
(src/mac_cutcell_mg.hpp:769) already separates the two roles: a matvec lambda and a precond
lambda that runs one symmetric V-cycle. The float hierarchy is already only a preconditioner
everywhere except that the level-0 matvec and the initial residual read the float bands. Make
the level-0 apply exact and the Krylov fixed point becomes A_exact by construction; the
proposal's steps 1 and 2 are then just what PCG already does. The work is one kernel plus a
switch at matvecOverlap (:1715), the choke point for solvePCG and the flexible PCG at
:881. [Corrected 2026-09-01 — solveBiCGStab at :988 does NOT route through it: it has its
own inline matvec at :995/:1003 for the g=2 gp staging, and belongs to P2. See the correction
block above.]
2. The float bands are the whole error — the arithmetic is already double.
applyCutcellOp (mac_pressure.hpp:178) and residualCutcell (:109) both promote to double
before accumulating ((double)AC(i)*x(i) + …). So nothing is lost in the summation; the entire
defect is the fp32 rounding of the stored coefficients, i.e. precisely the quantity the
double-diagonal targets. This confirms the diagnosis and means storage is the only lever.
3. The flux form gives A·1 = 0 BITWISE — the double-diagonal only gets it to eps_f64.
Writing the matrix-free apply in difference form,
y_i = t_w(x_i − x_{i-1}) + t_e(x_i − x_{i+1}) + …, annihilates the constant vector exactly,
whatever the precision of t_f, because each difference vanishes identically. A stored double
diagonal satisfies the identity only to double round-off. Defect correction therefore dominates
its own fallback on the very identity the fallback exists to protect — this is the strongest
argument for it and it was not in the proposal.
What the caveats turned out to be:
- Star overlay: already resolved, no work.
starApplyDeltais applied aftermatvecOverlapas a separate additive delta (:777-781), and the gp overlay follows the same pattern in BiCGStab. A matrix-free 7-point level-0 apply composes with both unchanged. - Variable density: nothing special. The caller pre-folds ρ into the coefficient
(
buildRhoCoeff,mac_pressure.hpp:251— "the coefficient rides the openness rails") and passes it as the openness (flow_ibm.hpp:4408,4442). The double array on level 0 is already the fullc_f, ρ included. gfplumbing: trivial. All threesetOpennesscall sites passidx2=idy2=idz2=1.0(flow_ibm.hpp:1659,4408,4442) — grid units. Store them as members anyway rather than assume it.- The coefficients are already resident and already double.
Level::ox/oy/ozareCCField = View<double*>(:276), ghost-filled and boundary-re-imposed bysetOpenness(:693-698) and retained for the coarsening. The matrix-free apply needs no new storage.
Correction to the cost claim — the −28 B/cell saving is not free. The proposal assumed the
level-0 float bands could simply be dropped. They cannot: the V-cycle smooths on level 0, and
cutcellSmoothColor divides by AC(i) and uses ac < 1e-30 to detect decoupled solid cells. So
the honest ledger is:
| variant | Δ bytes/cell | A·1 = 0 |
|---|---|---|
| double-diagonal (fallback) | +17 | to eps_f64 |
| matrix-free level-0 matvec, bands kept for the smoother | 0 | bitwise |
| + matrix-free level-0 smoother too | −28 | bitwise |
The middle row is the honest first step: zero new storage, still strictly better than the fallback. The bottom row is a follow-on — and note it comes with the double diagonal for free, since a matrix-free smoother must form the diagonal on the fly as the exact sum of six double face terms. Traffic on the matvec itself does drop either way (3 doubles = 24 B read instead of 7 floats = 28 B), in the favourable direction on a bandwidth-bound kernel.
Unchanged: the conditioning ceiling eps_f64·κ (as stated), the AMG bottom and all coarse
levels (inside the preconditioner, stay float), applyOutflowGhost and the CA ghost ring (they
act on the vector, not the operator).
Promoted to a suite-wide campaign (2026-09-01): the plan is DEFECT_CORRECTION_PLAN.md
(rule, per-solver inventory, rungs P1/P2/P3/A0/A1/M1/M2/D1/X, Opus↔architect handoff protocol) and the
executing session's prompt is DEFECT_CORRECTION_PROMPT.md. This item is now that campaign's P1.
The discriminating experiment, when this is implemented behind an env gate: the RCP bed at
rtol 1e-8, Ng = 48/64/96 — the ladder where float took 24/33/capped-at-300 (invalid) against
14/14/28 in double. Matrix-free level-0 should reproduce the double column while the hierarchy
stays float. PECLET_FLOW_MG_DIAGRESUM=1 gives the matched double-diagonal control on the same
ladder, so the three variants are directly comparable in one battery. Run it in an isolated
worktree (working practices, below), and not concurrently with other heavy batteries (WO-L race).
Why this generalises where storage-widening does not: with surface tension (V4) and later
phase change (Part II), the quantity driven to zero is genuinely not a fixed linear system —
the operator is a linearisation of a residual depending on curvature, colour and interfacial
mass flux. Defect correction on a matrix-free r(x) is the formulation that survives that.
Honest note on the gap: WO-M framed the question as which storage do we widen, never as should the residual and the preconditioner use different operators. Given that framing, the double-diagonal is the right answer — to the narrower question. Neither campaign raised this alternative.
The double-diagonal (user decision 2026-08-31, still valid as the fallback)¶
Decided: if defect correction does not pan out, implement this, default OFF, never make fp64 the default.
Why it is worth having despite buying little today (the user asked this directly, and the honest answer has two halves): - On physical outputs it buys nothing: Z&H drag identical to the 6th digit, RCP permeability negligible, regression +0.00% either way, hydrostatic already 3e-17 in float. - On robustness at tighter tolerance and higher resolution it is the difference between a result and a non-result: RCP bed at rtol 1e-8 took 24/33 iterations at Ng=48/64 in float and then capped at 300 (invalid) at Ng=96, against 14/14/28 in double. WO-M's measured κ(A) ≈ 0.18·N²·contrast makes that boundary predictable, not mysterious.
What exists already: PECLET_FLOW_MG_DIAGRESUM=1 (WO-M) is an ablation — it rounds
stored faces back to float and recomputes the diagonal as their exact double sum, which is
bit-for-bit the double-diagonal arithmetic. It refuses to run in a float build, so it proves
the numerics without being shippable. Judged against a matched full-double control it
recovers full-double behaviour at every grid from 48³ to 160³.
What the work is: view-type surgery carrying a double diagonal through smoother, residual, matvec, the CA ghost ring and the AMG bottom — not a line edit, which is precisely why WO-M proved the numerics first. Cost ≈ +17 B/cell (against +120 B/cell and +12% runtime for full fp64).
The rule it implements (WO-M's policy, stated deliberately as a rule rather than a patch):
A quantity an algorithm requires to satisfy an exact discrete identity must be stored in the precision in which that identity is asserted. A quantity carrying only an approximation may stay float.
Here the identity-bearing quantity is the operator diagonal in both the pressure and
momentum operators (A·1 = 0); the six face couplings are the approximation. The same root
cause has now surfaced in three unrelated places — pressure hierarchy, momentum stencil, and
the bottom AMG (already cured targetedly at mac_cutcell_mg.hpp:1411 and holding since
2026-08-13, which is the precedent for cheap-and-specific over blanket fp64).
Do not repeat these: the MReal switch was incomplete until WO-M completed it — four
families of hard (float) casts kept their operator fp32 even in a double build
(buildAdvStencil/buildAdvStencilVar, addDragDiagonal, applyBackflowStab, five sites in
mac_velocity_mg.hpp). Any pre-WO-M "double" momentum result is suspect.
Item 2 — Trace the Lamb mode-2 discrepancy [the architect]¶
The observation (WO-P): the oscillating-droplet frequency is 6.3–7.0 % below Lamb's analytical mode-2 result, consistently. Recorded as an open measured deviation, explicitly not a pass.
Already ablated, none of which explains it: amplitude, time step, initialisation, confinement (domain fraction φ swept 6.5 % → 0.8 %), and resolution. That the deviation is insensitive to all five is itself the strongest clue — it points at something systematic in the physics or the discretisation rather than a setup artifact.
Where I would start, in order:
1. Check the reference, not just the code. Lamb's formula is for an inviscid, isolated
droplet. Confirm which variant WO-P compared against: viscous damping shifts the observed
frequency (Prosperetti's viscous correction), and an ambient fluid of finite density shifts
it again (the two-density mode-2 result differs from the single-density one). A 6–7 % offset
is the right order for using the inviscid single-density formula on a viscous two-density
simulation. This is the cheapest hypothesis and it should be eliminated first.
2. Cross-check against the capillary-wave result, which came out at −2.1…−3.7 % against
its own dispersion relation on the same machinery. Two independent surface-tension
benchmarks both landing low, by different amounts, suggests a shared cause of partial size
— or that one of the two references is being misapplied. Compare what differs: the capillary
wave is planar (curvature from one direction), the droplet is spherical (the cascade's
mixed-direction and fallback branches engage).
3. Only then suspect curvature. V3 measured κ error to plateau with advection-realistic
fractions, and V4 showed spurious Ca is curvature-limited. A systematic κ bias would shift
an oscillation frequency systematically (ω ∝ √σκ-ish scaling), so measure the mean κ over
the droplet surface during oscillation against 2/R — a bias, not a scatter, is what would
do this.
4. Instrument rather than guess: the branch census field "kappa_branch" already exists, so
the fraction of the surface served by each cascade branch during the oscillation is
directly observable.
Do not tune anything to close the gap. If the reference turns out to be misapplied, fix the comparison and the "deviation" evaporates; if it survives a correct reference, it is a real finding about the method and is worth writing up.
Item 3 — The V3 curvature issues [the architect]¶
These now matter more than when V3 shipped, because V4 proved the spurious-current budget
is set by curvature, not by the force (see Item 4 and VOF_PLAN.md §4 V7 preamble).
- The paraboloid fallback fires on ~19–20 % of interfacial cells at every resolution, and WO-O showed this is geometry, not a defect: in 3D the corner column of a 3×3 patch must span √2·s, and s reaches √2 on the octant diagonal — 2.5 cells, exactly a 7-column's capacity. (Han et al.'s oft-quoted 0.9 % is a 2-D droplet, where the patch is 1×3.) Han use NH = 11 in 3D for this reason, which would need ghost width g = 5. WO-O judged the fallback good enough (its branch converges at 1.96–1.99) and declined to widen the halo. Open question worth revisiting now: given that κ accuracy is the binding constraint on pore-scale Ca, is g=5 + NH=11 worth the halo cost? Measure before deciding.
- Tier 2b (Popinet's generalised mixed-height fit) ships OFF because it destroyed max-error convergence (order 0.00 vs 1.86), across every fit width tested. WO-O isolated the mechanism: its data set is the columns that closed — a slope-selected, asymmetric subset whose lever-arm bias is scale-invariant; the paraboloid fit is immune because a PLIC polygon exists at every slope. It is retained as a re-measurable instrument with a ctest that regenerates the numbers. Worth revisiting whether this is fundamental or an implementation artifact, since Basilisk ships this tier in production — if our implementation differs from theirs in the selection criterion, that difference is the finding.
interfaceEpsdefaults to 0 so V3 stayed byte-identical, but WO-P found that advection round-off leaves ~5300 cells looking "interfacial" at 64³ where the cascade returns |κ| up to 2.9e+11 — harmless in V3, fatal once a force consumes it. Decide the production default, and note the general lesson: a rung's latent defect can be invisible until the next rung consumes it.- The advection-realistic plateau sets in at CΔ ≈ 0.16–0.08 while the exact-fraction control keeps converging at 2.16. This is the literature-expected behaviour (Han 2024), but it is now the thing standing between us and pore-scale capillary fidelity, so it deserves a deliberate decision rather than acceptance: improve the fractions (V5's RDF), improve the estimator (ELVIRA/LVIRA on 5³ — note 3D ELVIRA needs 5³ for 2nd order per Boniou 2022), or accept and design the campaign around the achievable Ca.
Item 4 — Scope V5–V7 [the architect]¶
V5 (SDF wetting / band RDF) has gained a second motivation. It was scoped for the contact angle; it is now also the most promising route to better curvature (plicRDF gives 2nd-order normals/positions — Scheufler & Roenby 2019) and it is the substrate Part II's phase change needs for normal-probe temperature gradients. Three consumers, one construction — scope it accordingly rather than as a wetting-only rung.
V7's premises changed under V4's measurements (recorded in VOF_PLAN.md §4 before V7):
- Spurious Ca ≲ 1e-7 is unreachable and the reason is structural. With exact κ the force
gives Ca = 1.9e-18; with computed κ it is 2.5e-4 … 1.4e-5 at D/Δ = 8 … 32. Ca ≈ δκ·h, so the
budget is a curvature requirement. Improve κ, never the force.
- The capillary time step binds 18 of 18 pore-scale combinations, by factors 6 to 5.9e4, and
worsens under refinement (dt_σ ~ h^{3/2} vs dt_CFL ~ h). 3.8e6–3.0e7 steps per pore volume
at Ca ~ 1e-6. This is the dominant feasibility constraint and it is arithmetic.
- Therefore: pick the largest Ca that still answers the physics question; budget wall-clock
from a measured ms/step before committing; and reconsider implicit surface tension, which
the plan parked on Popinet (2018)'s advice — advice written for cases where the capillary
limit is not four orders inside the advective one. Hysing confirms the crossover is real and
case-dependent: case 1 capillary-bound 204/204 steps, case 2 CFL-bound 108/113.
Also unresolved and worth folding into the scoping: S3 (coefficient-aware coarsening) is
real — WO-M confirmed the negative pivot survives in a double build, deepening with
hierarchy depth, distinct from the float-storage defect. It is unstarted. The collocated-paper
campaign has a far cleaner reproducer than ours (fully periodic, no domain BCs, no VoF, no
varRho, contrast tunable by PECLET_FLOW_APERTURE_ORDER=1|2), and the trap when fixing it is
that buildOpenness feeds both the geometric openness and the coefficient path.
Working practices this campaign learned the hard way¶
- Run attributable measurements in an isolated
git worktreecarrying only your own diff. Three live sessions share this checkout; a shared index caused three mis-attributed commits. A "+0.00 % regression" claim only means something if the tree held your changes and nobody else's. (Now rule 6 of the work-order preamble.) - Never
git add -A, and rungit diff --cached --statbefore every commit — staging named paths is not sufficient, becausegit commitalso commits what was already in the index. - A capped pressure solve makes a run invalid, not degraded (rule 3b). MG-PCG can report convergence per its own bookkeeping while the true residual sits ~100× above rtol.
- Gates must not demand what the arithmetic cannot deliver: use
rtol = max(1e-8, C·eps·0.18·N²·Δρ/ρ). At ratio 1000 on 256³, κ ≈ 1.2e7 and a fixed 1e-8 is already near the limit; at 512³ or ratio 1e4 it is unreachable. But treat this as a lower bound on what to demand, not a prediction of what is achievable. The κ law behind it is validated only 192³–384³: the collocated campaign's 768³ probe floored at 3e-4, three orders worse than the law predicts, invalidating three of their rung values (they had agreed to nine digits across cap settings — floor-limited, not converged). Cause under investigation: those were their only 2-node runs, so it is either an inter-node comm defect in the pressure path or a superlinear correction to the N² scaling. Above ~384³, measure the attainable residual; never use a prediction from this law to dismiss a measured floor. - If anything ever runs multi-rank on a
MREAL_DOUBLEbuild, run thekokkos_mpibattery first. The collocated campaign's double build never exercised it — its np4 path is vindicated only indirectly (one rung matching the float np4 result digit-for-digit). - Five gates in this campaign turned out to measure the wrong quantity (V0's MYC-normal order, WO-C's premise, WO-K's uniform-velocity discrimination, WO-J's acid-test velocity half, WO-P's falling drop). Every time, an agent isolating the mechanism and saying so improved the plan. Preserve that norm in the work-order prompts.
- The np=4
MPI_ERR_TRUNCATErace is load-triggered (WO-L, unstarted): re-run an affected leg on an idle machine before believing it. Do not run three heavy batteries concurrently.