Multigrid projection integration - #201
Merged
Merged
Conversation
One symmetric V(2,2) weighted-Jacobi multigrid cycle in Dirichlet mode per preconditioner apply: Dirichlet matches the interior-only operator CG applies to its zero-halo Krylov vectors (keeping M nonsingular), and equal pre/post Jacobi sweeps keep M symmetric as CG requires. Gives grid-size-independent convergence (5 iterations at 33..129 vs 50-170 for plain CG at tol 1e-8) on uniform grids where Jacobi preconditioning gives no benefit. The convenience API gains a POISSON_SOLVER_PCG_MG_SCALAR preset because poisson_solve_3d has no params argument: the preset carries the preconditioner into its per-preset cached instance. Backends without the preconditioner (OMP/SIMD CG, GMRES, GPU CG) reject POISSON_PRECOND_MULTIGRID explicitly instead of silently ignoring it.
New ns_solver_params_t.pressure_solver field selects the scalar projection's pressure Poisson solve: default CG (0, backward compatible), standalone multigrid V-cycles, or MG-preconditioned CG. The standalone mode subtracts the RHS interior mean first: the projection RHS is only near-compatible and the true Neumann system stalls on the incompatible component, while the CG-based presets are insensitive to it. Non-2^k+1 grids fail projection init with CFD_ERROR_UNSUPPORTED (probe-init of the MG hierarchy), and projection_optimized/omp/gpu reject any non-default selection at init: multigrid has no SIMD/OMP/GPU backend and cross-backend fallbacks are forbidden.
Contributor
There was a problem hiding this comment.
Pull request overview
Integrates geometric multigrid into the pressure Poisson path for the scalar projection Navier–Stokes solver, and adds a multigrid V-cycle preconditioner option for scalar CG, with explicit “no silent fallback” behavior across other backends plus new tests and documentation.
Changes:
- Add
ns_solver_params_t.pressure_solverto select CG (default), standalone MG, or MG-preconditioned CG for the scalarprojectionsolver. - Add
POISSON_PRECOND_MULTIGRIDand thePOISSON_SOLVER_PCG_MG_SCALARconvenience preset, while explicitly rejecting MG preconditioning on non-scalar CG/GMRES backends. - Add convergence/selection tests and update docs/roadmap/changelog to reflect the new capabilities and constraints (2^k+1 grid dims).
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/solvers/navier_stokes/cpu/test_projection_pressure_solver.c | Adds projection pressure-solver selection tests for MG + MG-PCG modes and backend rejection behavior. |
| tests/math/test_mg_pcg_convergence.c | Adds MG-preconditioned CG convergence and scaling tests (2D/3D) plus backend rejection checks. |
| ROADMAP.md | Updates roadmap items/constraints to reflect MG-PCG completion and projection wiring. |
| README.md | Expands feature list to include GMRES and geometric multigrid (incl. preconditioner + projection pressure solve). |
| lib/src/solvers/navier_stokes/cpu/solver_projection.c | Maps pressure_solver to Poisson presets and applies Neumann-compatibility mean subtraction for standalone MG. |
| lib/src/solvers/navier_stokes/cpu/solver_explicit_euler.c | Initializes new pressure_solver field in ns_solver_params_default(). |
| lib/src/solvers/navier_stokes/avx2/solver_projection_avx2.c | Rejects non-default pressure_solver on SIMD projection init. |
| lib/src/solvers/linear/simd_template/linear_solver_gmres_simd_template.h | Explicitly rejects MG preconditioner in SIMD GMRES init. |
| lib/src/solvers/linear/omp/linear_solver_gmres_omp.c | Explicitly rejects MG preconditioner in OMP GMRES init. |
| lib/src/solvers/linear/omp/linear_solver_cg_omp.c | Explicitly rejects MG preconditioner in OMP CG init. |
| lib/src/solvers/linear/neon/linear_solver_cg_neon.c | Explicitly rejects MG preconditioner in NEON CG init. |
| lib/src/solvers/linear/linear_solver.c | Adds cached POISSON_SOLVER_PCG_MG_SCALAR preset and initializes cached instance with MG preconditioner params. |
| lib/src/solvers/linear/linear_solver_internal.h | Adds shared helper to reject MG preconditioner on unsupported backends. |
| lib/src/solvers/linear/gpu/poisson_solver_cg_gpu.cu | Explicitly rejects MG preconditioner in GPU CG init. |
| lib/src/solvers/linear/cpu/linear_solver_gmres.c | Explicitly rejects MG preconditioner in scalar GMRES init. |
| lib/src/solvers/linear/cpu/linear_solver_cg.c | Implements MG V-cycle preconditioner option inside scalar CG (inner MG solver). |
| lib/src/solvers/linear/avx2/linear_solver_cg_avx2.c | Explicitly rejects MG preconditioner in AVX2 CG init. |
| lib/src/api/solver_registry.c | Validates MG modes at projection init time via multigrid probe-init on the exact grid. |
| lib/include/cfd/solvers/poisson_solver.h | Adds POISSON_PRECOND_MULTIGRID and POISSON_SOLVER_PCG_MG_SCALAR to the public API. |
| lib/include/cfd/solvers/navier_stokes_solver.h | Adds ns_pressure_solver_t and pressure_solver to the NS params public API docs/comments. |
| examples/poisson_solver_tuning.c | Updates example grid to 65×65 and benchmarks CG+MG preconditioning; documents standalone MG Neumann compatibility caveat. |
| docs/reference/solvers.md | Documents projection pressure_solver selection and MG preconditioner behavior/constraints. |
| docs/reference/api-reference.md | Documents new pressure_solver field and POISSON_PRECOND_MULTIGRID/preset exposure. |
| docs/guides/examples.md | Updates tuning-example narrative and expected output to include CG+MG PC and 2^k+1 grid note. |
| CMakeLists.txt | Adds new test executables and registers them with CTest. |
| CHANGELOG.md | Records the new projection wiring and MG-preconditioned CG feature additions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The MG probe-init previously mapped any failure to CFD_ERROR_UNSUPPORTED with a grid-dimension message, masking other causes (e.g. CFD_ERROR_NOMEM) and overwriting their error context. Only the non-2^k+1 grid rejection (CFD_ERROR_INVALID) is remapped now; other statuses propagate unchanged so the real failure cause survives.
The previous "zero-init backward compat" test ran make_params() twice, so both configs went through ns_solver_params_default() and only differed by NS_PRESSURE_SOLVER_DEFAULT vs (t)0 - the same value - making the memcmp assertions compare identical configurations. Build three configs from one genuinely zero-initialized struct that differ only in pressure_solver: a zero-init run must match the explicit-CG run bitwise and must differ from the explicit-MG run, proving the zero value selects CG (not the multigrid path). Anchoring on a zero base avoids default()'s nonzero source amplitudes, which otherwise confound the compare. The helper now takes a full ns_solver_params_t so callers control exactly how each field was initialized.
poisson_solver_tuning.c prints L2=-1.0 when a solve does not return CFD_SUCCESS (max_iter exit), so the Jacobi row's L2=-1.0e+00 is a "not computed" marker rather than a real negative error. Call this out in the Expected Output so readers don't misread it as a valid value.
The reference docs advertised a projection_jacobi_gpu solver type and an
NS_SOLVER_TYPE_PROJECTION_JACOBI_GPU macro, neither of which exists: the
registry registers NS_SOLVER_TYPE_PROJECTION_GPU ("projection_gpu") and the
GPU source is solver_projection_gpu.cu. Copy-pasting the documented name
would fail at cfd_solver_create().
Also corrects the backend table description - the GPU projection uses a
Conjugate Gradient pressure solve, not Jacobi.
Pre-existing: master already carried the stale name; this PR made it visible
by adding adjacent text that uses the correct one. Historical records under
technical-notes/ and validation/ are left as-is.
poisson_solver_create() returning NULL made projection_init return CFD_ERROR_UNSUPPORTED with no last-error message, leaving callers and tests without a reason for the rejection. Set an explicit message, matching the other rejection paths in this init.
projection_init probe-initializes a multigrid solver purely to validate the grid dimensions, but a default init builds the entire coarse-grid hierarchy on conforming grids - an O(N) allocation and transient memory spike that the real pressure solve then rebuilds from scratch. Pass mg_max_levels = 1 so the probe stops at the finest level. Level 0 borrows the caller's buffers and needs no residual buffer, so the probe now allocates only the context and one level descriptor. The 2^k+1 dimension check runs before any of that, so the rejection behavior is unchanged.
The INVALID->UNSUPPORTED remap in projection_init assumed the probe's CFD_ERROR_INVALID could only mean a non-2^k+1 grid. But poisson_solver_init also returns INVALID for degenerate grids (nx<3, ny<3, or nz==2), so those were misreported as UNSUPPORTED with a "requires 2^k+1" message. Screen degenerate grids up front and return INVALID with a fitting message. The two statuses drive different recovery: UNSUPPORTED tells the caller to try another pressure solver, which is useless advice for a grid too small for any solver. After this screen the probe's INVALID is unambiguous. Adds test_projection_mg_rejects_degenerate_grid_as_invalid covering a 2x2 grid, alongside the existing non-2^k+1 (UNSUPPORTED) case.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.