cb5771fa71ae6fe7b67784aeb539a2a9d41fe8b5
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
327da7ff47 |
rtx-cfd: multigrid-PCG projection — 30x faster, same answers — and the CFD1 refinement study
Performance Benchmarks / Run Benchmarks (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
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
Falsifier 4 of the Turek–Hron geometry decision fired (the SOR projection
cost 0.09 s/step at 250x41 and an hour per run at 5 mm); this answers it.
solvers::incompressible::poisson: PoissonProblem (cell-centred five-point
SPD operator as per-cell face coefficients + Dirichlet diagonal extra +
active mask) and solve_multigrid_pcg — conjugate gradient preconditioned
by one V-cycle of geometric multigrid: aggregation by 2 per direction (odd
sizes absorbed, coarse cell active iff any child is), the Galerkin coarse
operator for piecewise-constant prolongation / summation restriction,
symmetric Gauss–Seidel smoothing, coarse correction scaled by 2 (Braess's
under-correction of unsmoothed aggregation; scalar, so the preconditioner
stays symmetric and positive on range(A)), L1 TRUE-residual stop with a
stagnation guard. Singular systems are handled per connected component of
the active cells (mean projection and level per pure-Neumann component;
the anchor's component to p[anchor] = 0). PoissonSolverKind::{Sor,
Multigrid} on PisoParameters / EmbeddedParameters; Sor is the default and
its code is byte-for-byte untouched; an unconverged multigrid solve falls
back to the SOR sweeps for that projection.
Verified (poisson/tests.rs, tests/poisson_equivalence.rs):
- PCG iterations to cut the residual 1e-8 on the closed Neumann box at
32^2..256^2: 4, 4, 4, 4; ragged masked domains 8/8/8;
- manufactured recoveries to ~1e-14; Galerkin identity A_c v = R A P v to
7e-15 on every level (masked, outlet column, non-uniform conductances);
V-cycle symmetric to 1e-14; NaN-poisoned inactive cells untouched;
- two Neumann components with opposite imbalances, and a Dirichlet
component beside an imbalanced Neumann one (review scenarios): converge,
each component right up to its own constant;
- speed vs plain SOR at the same stop: 22.7x (128^2), 41x (256^2);
- same answers as SOR: PISO MMS 4.6e-8 relative, Taylor–Green divergence
1.4e-9 every step, embedded-circle MMS 7e-8, no-body bit-identity with MG
on both solvers, channel+outlet+circle 1.4e-10; CFD1 loads identical to
four digits at 0.003 s/step vs 0.094 (30x).
CFD1 refinement study (tests/turek_hron_cfd.rs, three grids, 257 s):
h = 10 / 6.6 / 5 mm -> control-volume drag 15.6156 / 15.2829 / 15.0988 vs
14.2929 (+9.25 / +6.93 / +5.64%), apparent order 0.71, Richardson
extrapolate 14.04; surface route and lift not monotone (flag 2/3/4 cells
thick) — the test asserts the measured band at the finest grid.
Built with a 4-agent workflow (core, integration, refinement study,
adversarial review); the review found no defects and four risks, three
fixed here (per-component projection, one symmetric smoother-sweep
parameter, acting on `converged` with an SOR fallback) and one recorded
(isotropic aggregation loses grid-independence on anisotropic cells).
rtx-cfd 301 -> 318 green.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
||
|
|
796cf173e6 |
rtx-cfd: second-order convection by deferred-correction TVD; MMS order 1.84, cavity closes on Ghia
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
First-order upwind's O(h) numerical viscosity was the measured limit on the
whole discretisation: MMS order ~0.9 at Re = 20 against 2.05 in the Stokes
limit. This adds a ConvectionScheme parameter to SimPLE — Upwind (default,
behaviour unchanged), TvdVanAlbada, TvdVanLeer — implemented by deferred
correction: the upwind operator stays implicit, so a_p = sum(a_nb) and
diagonal dominance survive unconditionally, and the limited
high-order-minus-upwind flux difference enters the source explicitly at the
current iterate. At a fixed point the two agree, so the converged answer is
the TVD discretisation. Faces whose far-upwind node lies outside the domain
fall back to pure upwind; wall faces pass no mass, so no correction enters.
Measured by the manufactured solution (van Albada, 16 -> 32 -> 64):
L2 velocity 1.325e-3 4.406e-4 1.232e-4 orders 1.59, 1.84
(upwind) 3.516e-2 1.954e-2 1.038e-2 orders 0.85, 0.91
The error is 27x to 84x below upwind's at equal resolution, the order climbs
toward 2 (the shortfall is limiter clipping plus the boundary fallback, both
of which shrink with h), the pressure error falls at the same rate, and
continuity still holds to solver tolerance in every cell.
On the Re = 100 lid-driven cavity at 65^2 the centreline minimum moves from
-0.1932 (upwind) to -0.2036 against Ghia's -0.2109 — 59% of the remaining
gap closed at equal resolution, converged in 790 iterations — and the vortex
position moves from 0.5000 to 0.4844 toward Ghia's 0.4531. Both new cavity
bounds exclude the upwind values, so falling back to first order fails them.
284 tests, 0 failing.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
||
|
|
b5814a304f |
rtx-cfd: manufactured solution finds the diffusion conductances were 1/h too
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
large
Applies MMS to the SIMPLE solver. It found a major discretisation error on
the first run, which is the point of the method.
The diffusion conductances read `mu / dx` and `mu / dy`. Finite volume
requires `Gamma * A / delta` — the face area over the distance between the
nodes it separates — so they should be `mu * dy / dx` and `mu * dx / dy`.
The face area was missing entirely, making viscosity too large by a factor
of `1/h`: sixty-five times on a 65x65 mesh. Every other term in the
equation was already a force (`dp * dy` for pressure, `rho u dy` for the
convective flux), so the mismatch was confined to diffusion.
The consequence was that the solver ran at an effective Reynolds number
far below the one requested. Before the fix the manufactured-solution
error did not reduce under refinement at all — observed order about -0.05,
because the spurious viscosity grows with the mesh. After it, the error
falls monotonically.
This also explains an apparent regression that is really a correction.
The cavity vortex position moved from y = 0.484 to y = 0.391 against
Ghia's 0.4531, which reads as worse agreement. It is not: a strongly
over-diffusive cavity approaches Stokes flow, whose vortex sits near
mid-height, so the old number was closer to the reference than the scheme
deserved. Correcting the viscosity exposed the discretisation's own error.
The test now states that disagreement plainly rather than asserting a band
around the reference.
What MMS reports now, and it is not yet good enough:
n = 16 L2 velocity error = 2.586104e-1 order -
n = 32 L2 velocity error = 1.797373e-1 order 0.52
n = 64 L2 velocity error = 1.277188e-1 order 0.49
First-order upwind should give 1. It gives about 0.5, and the u component
is markedly further from exact than v on the same mesh. Both say there is
at least one more defect in the discretisation or its boundary treatment,
and the asymmetry between the two momentum equations is the clue. The test
asserts only monotone error reduction — what is established — and records
the shortfall, because asserting a rate the solver does not achieve would
either redden the suite or invite someone to weaken it later.
This changes the plan: raising the observed order to 1 is now a
precondition for the second-order convection work rather than a
consequence of it. There is no value in adding a higher-order scheme to a
discretisation that has not demonstrated first order.
Supporting changes:
- `SimpleSolver::set_momentum_source` applies a volumetric body force,
which is what lets a manufactured solution be imposed at all.
- Divergence is now detected by growth, not only by NaN. The 8x8 case at
Reynolds 10^6 reached 1e149 before anything caught it, because
`is_finite` stays true right up until it does not.
- `test_simple_solver_workflow` specified water properties on a unit
domain, which is Reynolds 10^6 on ten cells: no steady laminar
solution exists and the solver diverges on it, correctly. It passed
only while the excess diffusion stabilised it. Now set to Reynolds 100.
561 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]> |
||
|
|
4d88dc0584 | Initial commit |