0dc95a8de5eb08a44407d7f4de07979cd11d9b18
54
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
03a9bdf41f |
rtx-cfd: apply boundary conditions to u* before using its divergence
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Build CPU-Only (Explicit) (push) Canceled after 0s
CI / Python Bindings (maturin) (macos-latest) (push) Canceled after 0s
CI / Python Bindings (maturin) (ubuntu-latest) (push) Canceled after 0s
CI / WASM Build + Size Check (push) Canceled after 0s
CI / Distributed Training Tests (push) Canceled after 0s
CI / CI Success (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
CI / Format Check (push) Canceled after 0s
CI / Clippy Check (push) Canceled after 0s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Build (ubuntu-latest) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
Closes the relaxation-factor dependence. Converged solutions are now
identical for velocity relaxation 0.3, 0.5, 0.7 and 0.9 -- bit for bit --
where they previously spread 18%.
The cause was ordering, not formulation. Boundary conditions were applied
only at the end of the iteration, so `copy_to_starred` snapshotted a
predicted field whose boundary faces held whatever the momentum sweep had
written there: values the wall overwrote with zero moments later. The
divergence of that field is the pressure equation's source, so those
un-constrained faces entered it as a spurious mass source, concentrated at
the two lid corners where the moving lid meets a stationary wall. The
swept value scales with the relaxation factor, so the spurious source did
too -- and so did the answer.
Diagnosis is worth recording because the symptom pointed away from the
cause. At the stalled state the interior momentum equations were satisfied
to machine precision at every relaxation factor: a fresh Gauss-Seidel
sweep moved the interior by 1e-15, the pressure correction was 1e-14, and
the momentum residual was 3.6e-16. The entire residual floor lived in the
*mass* term, and only that term varied with alpha -- 3.7e-4 at 0.3 against
1.6e-4 at 0.9. Each relaxation factor was converging honestly, to the
solution of a slightly different problem.
The correction also moves the cavity substantially closer to the reference,
because the spurious corner source had been suppressing the recirculation:
grid before after Ghia (1982)
17^2 -0.068 -0.123 -0.2109
33^2 -0.109 -0.154
65^2 -0.142 -0.174
97^2 -0.157 -0.182
Richardson extrapolation on the two finest grids now gives about -0.199
against Ghia's -0.2109, within 6%, with the remaining gap consistent with
first-order upwind's numerical viscosity. The residual floor falls roughly
linearly with mesh size (1.6e-3, 5.2e-4, 1.6e-4, 7.8e-5), which is the
signature of the corner singularity rather than of an unconverged solve --
the same one Botella & Peyret (1998) subtract analytically.
Two further fixes fell out of it:
- The solver returned NaN rather than reporting divergence. Asked for an
8x8 cavity at a Reynolds number of a million it now stops, says it did
not converge, and reports the last finite residual, instead of handing
back a field of NaN that poisons everything downstream. Previously the
false transient's large diagonal damped that case into crawling rather
than diverging, which hid it.
- `apply_boundary_condition` used `start_index` where it meant
`end_index` for the bottom wall. The other three arms are correct; with
both indices unset the default masked it.
558 tests across the three crates, 0 failing.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
|
||
|
|
2db4e28760 |
rtx-cfd: make SIMPLE a steady solver; the converged answer no longer depends
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
CI / Format Check (push) Canceled after 0s
CI / Clippy Check (push) Canceled after 0s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Build (ubuntu-latest) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Build CPU-Only (Explicit) (push) Canceled after 0s
CI / Python Bindings (maturin) (macos-latest) (push) Canceled after 0s
CI / Python Bindings (maturin) (ubuntu-latest) (push) Canceled after 0s
CI / WASM Build + Size Check (push) Canceled after 0s
CI / Distributed Training Tests (push) Canceled after 0s
CI / CI Success (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s
on the pseudo-time step Acting on a literature pass. Standard SIMPLE is a steady-state algorithm: it has no pseudo-time term, and stability comes from under-relaxation folded implicitly into the momentum coefficients. Ours had a false transient *and* an explicit post-hoc blend of the whole field, which is why the converged cavity solution varied with `time_step` -- something a steady state cannot legitimately do. Four changes, in the order they mattered: 1. The convergence measure was `|u - u_old|`, the change between successive iterates. That is not a residual: it reports how far the iteration moved, which depends on how heavily it is damped, and the damping was set by `dt`. Replaced with the imbalance of the discretised momentum equations, `|a_p u_P - sum a_nb u_nb - b|`, normalised by `sum |a_p u_P|` as CFD solvers conventionally report it. An unnormalised sum grows with the cell count and with `dt` through `a_p0`, so the same numeric tolerance meant a different thing on every grid. The residual is measured against the *unrelaxed* equation. Relaxation inflates the diagonal by 1/alpha and adds a matching source; reporting the relaxed system's residual makes one tolerance correspond to a different true error for each alpha. 2. Steady by default: `a_p0 = 0`, and Patankar's implicit under-relaxation -- `a_p / alpha` with `(1-alpha)/alpha * a_p * u_prev` added to the source. At a fixed point the two cancel exactly, so the converged solution is independent of alpha by construction. The explicit velocity blend is removed; it relaxed a second time and undid part of the continuity the pressure correction had just enforced. `steady: false` restores the transient term for genuinely time-dependent problems. Result: dt = 0.001, 0.01 and 0.05 now give bit-identical fields. 3. Dropped the net convective flux from `a_p`. It vanishes identically once continuity holds, but during the iteration it does not, and it can exceed the sum of the neighbour coefficients -- driving `a_p` through zero and the solve to NaN, which is what the workflow tests hit once `a_p0` was no longer there to mask it. Omitting it is what makes `a_p = sum a_nb` positive unconditionally. 4. Anchored one cell of the pressure correction. With velocity prescribed on every boundary the pressure equation is pure Neumann and singular; `p'` is fixed only up to a constant and Gauss-Seidel lets it drift. Enforcing solvability by subtracting the mean source is the textbook remedy and is wrong here -- this source is assembled from face fluxes that include the boundaries, so it need not sum to zero, and subtracting its mean injects a spurious source everywhere. Tried; it diverged. Anchoring a reference cell changes no pressure gradient, which is all the momentum equation uses. Also measured, and it settles the open question about Ghia: the under-prediction is numerical diffusion, not a defect. First-order upwind carries a numerical viscosity of about |u| dx / 2, which at 65^2 is 0.0078 against a physical 0.01 -- an effective Reynolds number near 56, not 100. Refinement moves the centreline minimum monotonically toward the reference: -0.068 at 17^2, -0.109 at 33^2, -0.142 at 65^2, -0.157 at 97^2, against Ghia's -0.2109, with the vortex position tracking 0.375 -> 0.406 -> 0.469 -> 0.490 against Ghia's 0.4531. The cavity test moves to 65^2 and asserts the vortex position tightly (0.40..0.52, Ghia 0.4531) while bounding the strength to the band first-order upwind can reach there. Its tolerance is 1e-4 rather than 1e-6: the two lid corners hold a velocity discontinuity whose discrete imbalance does not reduce with iteration, so the normalised residual floors near 7e-5. That is a property of the problem -- the same singularity Botella & Peyret (1998) subtract analytically -- and the physical assertions, not the stopping rule, are what establish correctness. Still open: converged solutions retain a dependence on the relaxation factor that the implicit formulation should have removed (-0.159 at alpha=0.3 against -0.134 at alpha=0.9 on 65^2, each stable to six decimals over 200k iterations). Recorded rather than papered over. 558 tests across the three crates, 0 failing. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> |
||
|
|
bfd9f4dfd2 |
rtx-cfd: repair the pressure-velocity coupling, LBM walls and mesh quality
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
CI / Format Check (push) Canceled after 0s
CI / Clippy Check (push) Canceled after 0s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Build (ubuntu-latest) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Build CPU-Only (Explicit) (push) Canceled after 0s
CI / Python Bindings (maturin) (macos-latest) (push) Canceled after 0s
CI / Python Bindings (maturin) (ubuntu-latest) (push) Canceled after 0s
CI / WASM Build + Size Check (push) Canceled after 0s
CI / Distributed Training Tests (push) Canceled after 0s
CI / CI Success (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s
Clears the rest of the quarantine. All three crates now run 558 tests
with 0 failures and no `#[ignore]` markers.
SIMPLE could not converge, and the reason was not slow convergence but
wrong physics.
The pressure correction equation used a bare Laplacian, 1/dx^2 and
1/dy^2, while the velocity correction divided by a_p = rho dx dy / dt.
SIMPLE requires these to be each other's inverse: substituting the
corrected velocities into continuity must reproduce the pressure
equation, which fixes a_E = rho d dy/dx with d = dV/a_p. The two
disagreed by roughly 1/(h^2 dt) -- about 2e4 on a 16x16 cavity -- so the
pressure correction was that many times too weak to enforce continuity.
The consequence was visible and specific. A lid-driven cavity at Re=100
produced a monotonic profile rising from 0 at the floor to 1 at the lid:
Couette flow, with no recirculation anywhere, and a peak pressure of
1.6e-4 against the rho U^2 scale of 1. The return flow in a cavity is
driven entirely by the pressure gradient, so with the pressure pinned
near zero there was nothing to turn the flow around. With the
coefficients made consistent the profile recirculates, the peak pressure
is 2.9, and the solver converges.
Also in SIMPLE:
- `p'` was never reset between outer iterations. It is a correction
that `pressure_update_step` folds into `p`, so carrying it forward
applied the same correction twice.
- The convergence measure was the inner Gauss-Seidel residual, which
goes to zero whether or not the flow satisfies continuity. Now the
mass imbalance.
- The velocity correction used only the transient part of a_p,
`rho dV/dt`, rather than the diagonal the momentum equation was
actually solved with.
- All four convective face fluxes were computed from a single
cell-centred velocity, so `fe` and `fw` were the same number, as were
`fn` and `fs`. Upwinding then picked the same direction on opposite
faces of the control volume. Now interpolated per face on the
staggered grid.
Not claimed: agreement with Ghia, Ghia & Shin (1982). The vortex centre
moves toward their y = 0.4531 under refinement (0.400 at 16^2, 0.419 at
32^2, 0.460 at 64^2) but the minimum centreline velocity reaches only
-0.130 against their -0.2109, and the converged field still depends
slightly on the pseudo-time step, which a true steady state cannot. The
cavity test therefore asserts what is established -- convergence,
recirculation, vortex position, and an O(1) pressure field -- and the
remaining gap is recorded in omni-cortex/docs/solver_status.md rather
than papered over with a loose tolerance.
LBM bounce-back was doing neither of the things its name claims. It was
written as assignment (`f[2] = f[4]`) rather than a swap, discarding the
population being reflected -- bounce-back is a permutation and conserves
mass exactly, so the domain leaked 0.013% of its mass every 100 steps and
would have kept draining. And the pairs used were 5<->8 and 6<->7, which
reverse only the wall-normal component: that is specular reflection, a
free-slip wall, so the no-slip condition the walls were supposed to
impose never held.
Mesh quality:
- Quadrilateral aspect ratio included the diagonals in the maximum but
not the minimum, so it could never return 1: a unit square reported
sqrt(2) and a 2:1 rectangle sqrt(5).
- Triangle aspect ratio used longest-over-shortest edge, which does not
detect the failure mode that matters. A sliver with vertices (0,0),
(10,0), (5,0.1) scores 2.0 -- indistinguishable from a healthy 2:1
triangle -- while its area is a twentieth of what its edges suggest.
Now the radius ratio R/2r, which is 1 for equilateral and 1250 for
that sliver, and which also fixes the quality histogram.
- StructuredMesh aspect ratio took bounding-box extents and guarded the
z-extent with `.max(1e-10)`. On a 2-D mesh the depth is exactly zero,
so the guard became the minimum and a unit square reported 2e10.
Mesh refinement produced meshes that failed their own validation.
`subdivide_triangle` reserved midpoint ids as `next_node_id + k`, then
advanced the counter by 3, after which `refine_cells` called `add_node`
and advanced it three more -- so every refined cell referenced vertices
three ids away from the ones actually created. Separately, the position
lookup selected by slot rather than by id ("This is simplified, should
look up correct midpoint"), so three of four sub-triangles had their
areas computed from the wrong points; the quadrilateral version mapped
every new id to the cell centre.
Fixtures corrected rather than tolerances loosened: a structured mesh
test asserted 0.16 for the average cell volume while the comment beside
it computed 0.25 from the node-count convention the code actually uses;
the Zou-He pressure test built a *velocity* boundary at u = 1.2, far
above the lattice speed of sound, making the density negative; and the
cavity-setup test required the lid to influence the domain centre 16
rows away in 10 steps, which exceeds the lattice propagation speed.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
|
||
|
|
4d88dc0584 | Initial commit |