# Moving circles on the uniform dense grid — S0 DRAFT contract Status: **DRAFT for review, not frozen.** No implementation exists. Base: main `b2034f87e03656c5f67ab88562a70d6c338b9918`. Scope approved by Frank (via TRON, thread C0C78UFKGRM/1793335557.460509): uniform-grid prescribed translation and single-cylinder free transverse VIV. Out of scope: refinement/blocked path, pinball assembly, EsoPull, shifted storage, D3Q19, DynamisLab, manuscript. Items marked **OPEN** are undecided; items marked **CANDIDATE** are proposals. ## 1. Working standards - Lean, readable kernels in the existing idiom (`kernels/step/*.cu`, shared `apply_bouzidi_link`); Python manages state and validation. No framework, no compatibility layer, no new dependency. - The existing stationary/rotating path (`CurvedBoundaryKernel`, host link packing, `sync_bodies`) stays byte-identical. Moving circles are a separate, explicitly enabled path with its own numerical contract (this file). - One authority per state (section 4). No per-step full-domain download, rebuild or recompile; local swept-band device updates are in scope. - Small slices (S1 host spec, S2 kernels without motion, S3 motion + refill, S4 oscillator); each reviewed SHA is frozen before targeted runs. - Tolerances, benchmark parameters and the force composition used in the structural RHS are declared before the runs that judge them. Fresh vs carried evidence and existing baseline failures are recorded; smoke, convergence and physical validation are reported separately. No undeclared force filtering. - External code (waLBerla GPLv3, FluidX3D source-available, VIVSIM) is a concept reference only; nothing is copied. ## 2. Current update order (main b2034f87, dense double buffer) `Simulation.run(n)`: upload host action (if `upload_act`) → zero obs (if `zero_obs`) → `DoubleBufferStepper.step(n)` → download obs → synchronize. Per step (`lbm/stepper.py` `step`): 1. `CurvedBoundaryKernel` over the packed cut links: reads post-collision `G^{n-1}` at `k_f` (and `k_f - c` for q < 1/2), adds the moving-wall term (`Uw = -omega*ry, Vw = omega*rx` + optional per-link `surface_velocity`), writes the reflected value into slot `opp` of the solid node `k_s`, and accumulates `c(f_toward + f_reflected)` and torque into `obs`. 2. `OneStep` pull/collide `ddf → temp` (collides FLUID cells; obstacle interiors flagged `SOLID|OBSTACLE|BC_CURVED`), then buffer swap. 3. `ForceRegionKernel`, `SensorKernel`. Raw `obs` force is the force **on the body** (link derivation in section 6; DFG docs report `Cd > 0` from it). Translation `vx, vy` in the action slots is currently stored but not read by any curved kernel. ## 3. Proposed update order (moving circles) Per step n, before the existing step 1: - **M1 `MovingCircleCells`** (one thread per swept-band cell): classify with the analytic predicates `in_old = inside(x, c^{n-1})`, `in_new = inside(x, c^n)`; write `flag = in_new ? SOLID|OBSTACLE|BC_CURVED : FLUID`; covered cells (`!in_old && in_new`) accumulate `node_cover`; fresh cells (`in_old && !in_new`) are refilled (section 5) and accumulate `node_uncover`. - **M2 `MovingCircleLinks`** (one thread per band cell × direction): compute q and hit point at `c^n` (FP64, same formula as `find_circle_intersection`, same clamp [0.01, 1] and fallback classes as `CircleGeometry.build_cut_links`), skip non-cut slots, call `apply_bouzidi_link` with `u_w = (vx^n, vy^n) + omega × (hit - c^n)`, accumulate the link terms (section 6). Then the unchanged steps 1–3. M2 must follow M1 because a q < 1/2 link may read `k_f - c` at a fresh cell. ## 4. State, masks and authority - Host `ObjectState` is the single authority for `offset = (dx, dy)`, `velocity = (vx, vy)` and `omega`. Velocity is supplied, never differenced from offsets. - Per body a small device **motion record** `{c^{n-1}, c^n, u^n}` is uploaded with the action buffer; action-slot meaning is not overloaded. (CANDIDATE layout; OPEN.) - Membership is never read from a flag that M1 is overwriting. The host verifies at `add_body` that every swept-band cell has base flag FLUID, so the band's base state is known without reading `flag_gpu`. - `flag_gpu` is the device authority for band cells while a moving body exists; the host flag mirror is stale for those cells and is not used. ## 5. Refill of fresh cells (CANDIDATE) - Stored time level: everything in `ddf` at M1 is post-collision `G^{n-1}`; the refill writes all 9 slots at that same level. - `G = f_eq(rho_bar, u_w(x)) + G_neq(x_d)` where `rho_bar` = mean density of valid neighbours and `x_d` = the valid neighbour best aligned with the outward normal `(x - c^n)/|x - c^n|`; `G_neq(x_d) = G(x_d) - f_eq(rho(x_d), u(x_d))`. - Valid donor: inside the domain, base FLUID, `!in_old && !in_new`. - No valid donor for the NEQ part → equilibrium only (counted). No valid neighbour at all → cannot happen when the per-step move is < 1 cell (validated on host); counted anyway. - Fresh cells only write, old-fluid cells only read: no race. ## 6. Discrete momentum ledger (to review; not a selection) Notation per step n: `F^n` = fluid set after M1; links L = (k_f, dir) at `c^n`; `f_t = G_dir(k_f)`; `f_r` = value M2 writes into `(k_s, opp)`. Fluid momentum before streaming: P_pre = P(G^{n-1} on F^{n-1}) - sum_cover sum_q c_q G_q + sum_fresh sum_q c_q G_q^refill During the pull, every population entering a fluid cell comes from a fluid cell except the BFL-written slots; `f_t` leaves into solid and is never pulled by a fluid cell. Collision conserves momentum for SRT/MRT without forcing. Hence, in exact arithmetic, P_fluid^n - P_fluid^{n-1} = - sum_L c (f_t + f_r) - I_cover + I_uncover + B^n + S^n with `B^n` external-boundary momentum (inlet/outlet/walls) and `S^n` forcing. So **`F_CME + node_cover - node_uncover`** is, by construction, the body momentum that balances the fluid ledger of *this* storage/update order. To be checked analytically in review: q < 1/2 branch (reads `k_f - c`), halfway fallback, and that every fluid pull from a solid slot is BFL-written. The Wen 2014 relative-velocity link sum adds `-u_w (f_t - f_r) = u_w dm` per link (`dm = f_r - f_t`, the BFL mass defect). That term has no counterpart in the fluid ledger above. Peng et al. (PRE 2016 submitted, §II.C, Eqs. 20–21) show for a straight wall that the bounce-back error accumulated before a node switch cancels with the switching momentum. Whether `sum u_w dm` approximates `I_cover - I_uncover` here (implying double counting if both are used) is **NOT demonstrated** for this implementation: it needs a bounded experiment (prescribed slow translation; cumulative `sum u_w dm` vs cumulative node impulse, per crossing cycle). Momentum conservation is also distinct from Galilean invariance, which G4 tests. Accumulators (separate, per body, per step window; all reported raw): `link_cme`, `link_wen_correction` (= `sum u_w dm`), `node_cover`, `node_uncover`, plus counters of crossings and refill fallbacks, so force spikes can be correlated with crossing events. **OPEN — structural RHS candidates (not selected):** (a) `link_cme + node_cover - node_uncover`; (b) `link_cme + link_wen_correction` (no node impulse). Selection follows the ledger review and the bounded experiment above. ## 7. Rejection semantics - Before any mutation (`ValueError`, no device write, host state unchanged): moving body with EsoPull, `ddf_shifting`, refinement/blocked stepper, D3Q19, Jets or surface actuation on that body, `sync_bodies`/checkpoint while a moving body exists; swept band touching domain boundaries, other bodies, sensors or force regions; `set_body` offset outside the declared box, per-step move ≥ 1 cell, speed above the declared bound. - Host state is committed only after a successful upload (same contract as Jet amplitude updates); an uncertain device-write failure makes the simulation unusable. - Device-side soft diagnostics (refill fallbacks) are counters, not errors. ## 8. Coupling and units (CANDIDATE) Lattice units, rho0 = 1, per unit span. Step n uses `c^n`, `u^n` and yields `F^n`. Weak staggered update `v^{n+1} = v^n + (F^n - c v^n - k y^n)/m`, `y^{n+1} = y^n + v^{n+1}` (Newmark is an alternative; OPEN). Mass, natural frequency, damping and `U_r` follow the chosen benchmark's own definitions (Leontini et al. JFS 2006 under verification; vacuum vs in-fluid natural frequency must not be mapped silently). Stability/convergence is gated by a halved-time-step run, not assumed from the mass ratio. ## 9. Gates (map; tolerances OPEN, declared before runs) G0 zero-motion regression (stationary path unchanged; moving path at rest reproduces stationary link set and DDF) · G1 raw force sign (`Cd > 0`) · G2/G3 prescribed translation incl. ledger accumulators · G4 matched relative-flow (moving body in quiescent fluid vs fixed body in uniform stream), optional boost, co-moving sanity · G5 Dütsch et al. 1998 Re 100, KC 5 in-line (needs 2-component offset) · G6 Leontini free transverse VIV (m* = 10, ζ = 0.01, Re = 200, definitions pending) · G7 time-level contract frozen before the oscillator. ## 10. Remaining decisions Structural RHS (section 6); motion-record layout; refill details; coupling scheme; benchmark definitions and digitized reference data; tolerances; execution/resource feasibility (Astra, pending); whether a quiescent domain (zero inlet velocity) runs cleanly in the current config.