Repository navigation
StiffnessPreconditioner for PoissonAdiabaticGyrokinetic: general mid/stab, weighted average in the stabilization - #740
Merged
Conversation
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Identity-weighted averaging is constructed incorrectly, and the new preconditioner API is incompatible with the currently pinned feectools version.
3 open findings
What changed in this PR
Extends diffusion and gyrokinetic Poisson operators with weighted stabilization averages and generalized stiffness preconditioning.
Changes:
- Adds
WeightedAverageProjectionfor symmetric weighted averaging. - Generalizes
StiffnessPreconditionerfor custom diffusion, stabilization, and averaging operators. - Adds validation and numerical tests for cylindrical and toroidal geometries.
| File | Description |
|---|---|
src/struphy/propagators/tests/test_gyrokinetic_poisson.py |
Tests gyrokinetic Poisson preconditioning. |
src/struphy/propagators/poisson_solve.py |
Exposes stabilization averaging options. |
src/struphy/propagators/poisson_adiabatic_gyrokinetic.py |
Selects geometry-dependent weighted averages. |
src/struphy/propagators/implicit_diffusion.py |
Builds weighted stabilization and generalized preconditioners. |
src/struphy/feec/tests/test_weighted_average_projection.py |
Tests projection, symmetry, and plain-average equivalence. |
src/struphy/feec/tests/test_stiffness_preconditioner.py |
Tests generalized stiffness configurations. |
src/struphy/feec/preconditioner.py |
Implements generalized Kronecker approximations. |
src/struphy/feec/mass.py |
Adds weighted-average projection. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
spossann
force-pushed
the
stiffness-precond-gyrokinetic
branch
from
October 9, 2026 14:05
1a9e352 to
9cdc643
Compare
spossann
added a commit
that referenced
this pull request
Oct 9, 2026
…s; Kronecker preconditioners in ImplicitDiffusion (#739) ## Summary Adds two Poisson strong-scaling profiling cases that differ from the existing ones only in the preconditioner, and makes the preconditioner choice actually reach the solver in `ImplicitDiffusion` (and thus `PoissonSolve` and `PoissonAdiabaticGyrokinetic`). ### New profiling cases - `profiling/examples/Poisson/cube_strong_scaling_massmatrix/` + `profiling/examples/Poisson/submit_poisson_cube_strong_scaling_massmatrix.py` (`precond="MassMatrixPreconditioner"`, `maxiter=3000`) - `profiling/examples/Poisson/cube_strong_scaling_stiffness/` + `profiling/examples/Poisson/submit_poisson_cube_strong_scaling_stiffness.py` (`precond="StiffnessPreconditioner"`, `maxiter=100`) ### Poisson cases on HollowCylinder and HollowTorus - 8 new cases `profiling/examples/Poisson/hollow_{cylinder,torus}_strong_scaling{,_multigrid,_massmatrix,_stiffness}/` with submit scripts `submit_poisson_hollow_{cylinder,torus}_strong_scaling*.py`. HollowCylinder `a1=1e-2, a2=1, Lz=4`; full HollowTorus (`tor_period=1`) `a1=1e-2, a2=1, R0=4`. - All 12 Poisson cases (incl. the 4 cube ones) use the same manufactured solution: a Gaussian 0-form on the logical domain, exp(-|eta-0.5|^2/0.1^2), with the analytic -Laplace(phi) per mapping as source (both `given_in_basis="0"`). The errors are computed at the pushed-forward evaluation points x = F(eta). The Laplacians agree with a metric-based finite-difference Laplacian to 3e-6; the error converges at second order (16^3 -> 64^3); thresholds are calibrated at 64^3. - `maxiter=3000` for all cases (multigrid on the hollow torus needs ~190 iterations at 32^3, the stiffness preconditioner ~60 on the hollow mappings at 64^3). - Slice plots in physical coordinates (cube slices, cylinder cross-section, torus poloidal plane) or logical coordinates. - The cube submit scripts are renamed to `submit_poisson_cube_strong_scaling*.py` (labels unchanged), like the other cases `submit_poisson_<mapping>_strong_scaling<_precond>.py`. ### Profiling results (local, 64^3, 1 and 2 MPI ranks) All 12 cases run locally with the submit scripts (rank counts limited to 1 and 2 on a laptop with an i5-1135G7, 4 cores), commits 71e3a91 / 49f252a (no change to the Poisson code in between). Times of the slowest rank in seconds, from the `profiling_data.h5` files (`solve: PoissonSolve`, `setup: total`); the iteration counts are the same for 1 and 2 ranks. All runs pass their error checks (max relative error of phi: cube 9.03e-3, hollow cylinder 9.72e-3, hollow torus 9.85e-3). | mapping | precond | iterations | solve 1 rank | solve 2 ranks | speedup | setup 1 rank | setup 2 ranks | |---|---|---|---|---|---|---|---| | cube | none | 198 | 26.4 | 21.4 | 1.23 | 14.8 | 8.4 | | | multigrid | 9 | 20.5 | 10.2 | 2.01 | 29.2 | 16.2 | | | mass matrix | 423 | 85.6 | 51.6 | 1.66 | 22.0 | 9.7 | | | stiffness | 2 | **0.4** | **0.2** | 1.93 | 27.3 | 15.5 | | hollow cylinder | none | 478 | 62.9 | 41.0 | 1.54 | 16.1 | 9.0 | | | multigrid | 81 | 143.4 | 97.1 | 1.48 | 26.4 | 15.2 | | | mass matrix | 2885 | 554.6 | 337.4 | 1.64 | 22.8 | 10.2 | | | stiffness | 57 | **9.4** | **6.1** | 1.53 | 31.1 | 19.4 | | hollow torus | none | 584 | 80.1 | 56.4 | 1.42 | 17.1 | 10.3 | | | multigrid | 293 | 519.2 | 372.6 | 1.39 | 30.2 | 18.8 | | | mass matrix | 3000 (maxiter, res 1.9e-8 vs tol 1e-8) | 568.4 | 355.0 | 1.60 | 18.9 | 11.2 | | | stiffness | 60–61 | **10.1** | **6.7** | 1.51 | 30.9 | 19.0 | - The **stiffness preconditioner** gives the fastest solve in all geometries (exact on the cube; 7–8x faster than unpreconditioned CG on the hollow mappings), at 10–15 s more setup. - **Multigrid** pays off only on the cube; on the hollow mappings (a1 = 1e-2) it is slower than no preconditioner. - The **mass-matrix preconditioner** is the slowest everywhere (it does not approximate the Laplacian); on the hollow torus it reaches maxiter=3000 without converging (the error check still passes). - 1 -> 2 ranks: solve speedups 1.2–2.0, setup speedups 1.6–2.3 (includes the mass-matrix assembly). ### `MassMatrixPreconditioner`: `diagonal_scaling=True` by default Affects all propagators that build a mass preconditioner by name and the multigrid mass smoother. FEEC/multigrid/Poisson/curl-curl/saddle-point tests pass on 2 ranks (251 passed). ### `PoissonAdiabaticGyrokinetic`: multigrid rejected Multigrid would be built for the stabilization without the flux-surface average (applied after `ImplicitDiffusion.allocate()`) and never updated, see the review comment. Full support of the stiffness preconditioner for this propagator (M1gyro, M0ad_withT, weighted average) follows in the stacked PR #740 (paired with struphy-hub/feectools#102). ### `MassMatrixDiagonalPreconditioner` removed (API change) - It was `MassMatrixPreconditioner(dim_reduce=None, diagonal_scaling=True)`; with the new default it is now `MassMatrixPreconditioner(M, dim_reduce=None)`. - **`"MassMatrixDiagonalPreconditioner"` removed from `OptsMassPrecond` and `OptsDiffusionPrecond`** (clean break, params files naming it fail `check_option`). - Variational propagators and `variational_utilities`: `MassMatrixPreconditioner(..., dim_reduce=None)` explicitly (cheap `update_mass_operator` for weights that change every step), behavior unchanged; default `precond` option is now `"MassMatrixPreconditioner"`. - `ShearAlfvenCurrentCoupling5D`: default `precond="MassMatrixPreconditioner"`, i.e. now `dim_reduce=0` (its mass matrix is fixed). - Default `dim_reduce=0` kept: exact for weights/mappings varying only in eta1 (PCG on a HollowCylinder: 2 iterations vs 7–10 with `None`), comparable otherwise (HollowTorus 7–9 vs 8–11, Colella 10–85 vs 10–77). Tests: preconditioner, variational utilities/propagators, current coupling 5D, variational dissipation and the default-params fluid/hybrid/visco-resistive model tests pass serially (36); FEEC/variational tests on 2 ranks (20 passed). ### Submit scripts next to their cases - All `profiling/submit_*.py` moved to `profiling/examples/<Model>/`, next to the folder with their params file (Poisson and ToyGyrokinetic). They put `profiling/` on `sys.path` for the driver modules; `params_dir` is relative to the new location. - New options `--language` (`fortran`/`c`) and `--compiler` (defaults `fortran`/`GNU`, as hardcoded before), passed to `ProfilingCase`. - `reusable-profiling-clusters.yml` called the nonexistent `profiling/submit_diocotron_job.py` with `--language`/`--compiler`, which no script accepted; it now calls `profiling/examples/ToyGyrokinetic/submit_diocotron_strong_scaling.py`. Smoke run at 32³ on 2 ranks: no preconditioner 103 iterations, mass matrix 214, stiffness 2 (identical errors). ### Preconditioner options in `ImplicitDiffusion` - `precond="MassMatrixPreconditioner"` / `"MassMatrixDiagonalPreconditioner"` now builds the preconditioner from `M0`. Before, every value except `"MultiGrid"` was silently ignored (`pc=None`). - New `precond="StiffnessPreconditioner"`: `StiffnessPreconditioner(mass_ops, "grad", sigma=sigma_1)`, rebuilt in `__call__` when `sigma_1` changes (e.g. with `divide_by_dt`), like multigrid. `sigma = sigma_1` for any `stab_mat` (with `sigma = 0` the pseudo-inverse drops the constants, and PCG does not converge for periodic BCs with `stab_mat="Id"`). - New option `precond_params: dict`, passed to the preconditioner constructor (e.g. `{"weights": "unit"}`). - **Default `precond=None`** (was `"MassMatrixPreconditioner"`, which had no effect), so existing models keep their behavior. Any preconditioner requires `solver="pcg"`. - `"StiffnessPreconditioner"` added to `LiteralOptions.OptsDiffusionPrecond`. Note: the mass-matrix preconditioner does not approximate the Laplacian and makes CG 3–6x *slower* (e.g. 555 vs 143 iterations in the new test); it is there for the profiling comparison. ### Other changes - **`PoissonSolve`: default `stab_mat="M0"`** (was `"Id"`); consistent with `ImplicitDiffusion` and with the weak form ε(ψ, φ) (most examples already set it explicitly). - **Bug fix `solver="cg"`:** `pc` was always passed to `inverse`, and `ConjugateGradient` has no `pc` argument (TypeError). Now passed only for `"pcg"`. - **Bug fix `StiffnessPreconditioner`:** the hand-built 1d difference matrix was wrong for a single periodic element (`+1` overwrote `-1`, giving `[[1]]` instead of `[[0]]`), adding a spurious stiffness term in that direction (e.g. 2.5D grids `[Nx, Ny, 1]`). The 1d matrices are now taken from the 3d derivative blocks via `DirectionalDerivativeOperator.tokronstencil()` (global on every rank), so they match the operator by construction. On a 2.5D cube the preconditioner is exact again (≤ 3 PCG iterations instead of 5–9). ### Tests - `test_poisson_2d_kronecker_preconds` (periodic/Dirichlet/Neumann × `stab_mat` M0/Id): mass-matrix and stiffness preconditioners, and `solver="cg"`, give the unpreconditioned solution; stiffness needs fewer iterations. - `test_implicit_diffusion_stiffness_dt`: the stiffness preconditioner follows changes of `dt`, exact on the cube. - `test_stiffness_approximation_unit_cube`: new case with one element in z (fails without the fix). - Existing tests that set `precond="MassMatrixPreconditioner"` (effectively unpreconditioned so far) now use `precond=None`. Locally on 2 ranks: `feec/tests/test_stiffness_preconditioner.py`, `propagators/tests/test_poisson.py`, `propagators/tests/test_gyrokinetic_poisson.py` all pass; `test_verif_Poisson.py` passes. Other model tests left to CI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…stab, weighted average in the stabilization
- StiffnessPreconditioner: mid (default M_{k+1}) and stab (default M_k, or "Id") as names of mass
operators or operators (incl. sums such as M1gyro), stab_average ("eta3", "eta2 eta3", grad only);
blocks of mid with zero mean have no stiffness term (their 1d stiffness still defines the eigenbasis);
the averaged stabilization S (I - A) is approximated by mean-removing terms, exact on the logical
cube; the average is left out of the diagonal scaling (not resolvable by probing)
- WeightedAverageProjection: S-orthogonal projection onto functions constant in the averaged directions,
S (I - P) is symmetric; equals the plain AverageOperator if the weight of S does not depend on the
averaged directions; e.g. the flux-surface average with sqrt(g) for M0
- ImplicitDiffusion/PoissonSolve: option stab_average; stab_mat @ (I - P) built in ImplicitDiffusion;
the stiffness preconditioner gets diffusion_mat, stab_mat and stab_average; MultiGrid rejected with
stab_average
- PoissonAdiabaticGyrokinetic: stab_average from which_geometry (cylindrical: eta3, toroidal: eta2 eta3);
with the weighted average the toroidal operator is symmetric (it was not with the plain average,
pcg was not applicable); MultiGrid rejected
- feectools submodule: KroneckerSumSolver with stiffness coefficients and additional Kronecker terms
(struphy-hub/feectools#102)
- tests: exactness of the new options on the cube, option checks, WeightedAverageProjection
(symmetry, projection, plain average), gyrokinetic Poisson with the stiffness preconditioner
(cylindrical 34 -> 10, toroidal 109 -> 20 PCG iterations)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d terms (feectools#102 review) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
spossann
force-pushed
the
stiffness-precond-gyrokinetic
branch
from
October 9, 2026 15:17
c5b6563 to
f50e096
Compare
…average in WeightedAverageProjection - StiffnessPreconditioner: with stab='Id' the averaged factors are 1 1^T / n (the projection is Euclidean), not the M_d-weighted ones; exact on the cube again (2 instead of 44 PCG iterations for stab_average='eta3'). - WeightedAverageProjection: allow averaging over all three directions (global weighted mean, 1x1 reduced system). - Tests: stab_id with eta3 / eta2 eta3 averages, global average (preconditioner and projection). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.


Summary
Makes
StiffnessPreconditionerwork forPoissonAdiabaticGyrokinetic(and otherImplicitDiffusionoperators with a general diffusion matrix and stabilization), and makes the toroidal adiabatic gyrokinetic Poisson operator symmetric.Follow-up of #739 (merged), rebased onto
devel. Addresses the review comment #739 (comment) properly; #739 itself only rejected multigrid forPoissonAdiabaticGyrokinetic.Points the
feectoolssubmodule to the branch of struphy-hub/feectools#102 (KroneckerSumSolverwith stiffness coefficients and additional Kronecker terms, feectools 0.8.0). Thefeectoolspin inpyproject.tomlis raised once 0.8.0 is on PyPI.StiffnessPreconditioner:mid,stab,stab_averagePreconditioner for$\mathbb d^T \mathbb M_{\textrm{mid}} \mathbb d + \sigma , \mathbb S (\mathbb I - \mathbb A)$ , all new inputs as strings with the previous behavior as default:
mid: name of a mass operator inmass_ops(e.g."M1gyro","M1perp") or the operator itself (WeightedMassOperator, or sums/multiples such asM1gyro = M1_MHDeq - M1para_MHDeq), with the domain/codomain ofstiffness_coeffsin feectools#102).stab: name or operator with the domain/codomain of"M0ad_withT"), or"Id"; defaultstab_average(gradient only): directions averaged in the stabilization, e.g."eta3","eta2 eta3". The term is approximated bymass_termsin feectools#102). The average couples whole lines and is left out of the diagonal scaling (probing cannot resolve it; its diagonal is O(1/n)), which keeps the preconditioner exact on the logical cube.WeightedAverageProjection(new,feec/mass.py)The$\mathbb S$ -orthogonal projection onto functions constant in the averaged directions, $\Pi = \mathbb E (\mathbb E^T \mathbb S \mathbb E)^{-1} \mathbb E^T \mathbb S$ ; then $\mathbb S(\mathbb I - \Pi)$ is symmetric. For $\mathbb M^0$ this is the flux-surface average with $\sqrt g$ ; it equals the plain $\mathbb S$ does not depend on the averaged directions (cube, cylinder: identical to round-off). The reduced matrix is assembled once by colored probing and factorized (sparse LU); one application costs one application of $\mathbb S$ .
AverageOperatorwhen the weight ofWhy: with the plain (unweighted)$\mathbb M^0 (\mathbb I - \mathbb A)$ is not symmetric ($\sqrt g = rR$ depends on $\eta_2$ ; $|x^T S y - y^T S x| / |x^T S y| \approx 0.3$ ), but $\sqrt g$ , and with $n/T$ of
AverageOperator, the toroidal stabilizationPoissonAdiabaticGyrokineticsolves it with pcg. Toroidal results change (the average is now weighted withM0ad_withT, a flux function for the usual equilibria); cylindrical results are unchanged.ImplicitDiffusion,PoissonSolve,PoissonAdiabaticGyrokineticstab_average;ImplicitDiffusionbuildsstab_mat @ (I - WeightedAverageProjection)before the preconditioners and passesdiffusion_mat,stab_matandstab_averageto the stiffness preconditioner.precond="MultiGrid"is rejected withstab_average.PoissonAdiabaticGyrokinetic:stab_averagefollows fromwhich_geometry(cylindrical:"eta3", toroidal:"eta2 eta3") instead of patching_stab_matafterallocate(); multigrid rejected.Results
PCG iterations,
PoissonAdiabaticGyrokinetic(M1gyro,M0ad_withT, epsilon = 0.1, 12x12x8 elements):On the unit cube the preconditioner is exact (2 iterations) for constant anisotropic or perpendicular$F_d = \mathbb 1 \mathbb 1^T/n$ ), and
mid, a scaledstaborstab="Id"(whose average is Euclidean,stab_average"eta3"/"eta2 eta3".stab_average="eta1 eta2 eta3"(global weighted mean) is supported as well.Tests
test_stiffness_preconditioner_mid_stab_average(exactness on the cube),test_stiffness_preconditioner_mid_stab_optionstest_weighted_average_projection.py(new file infeec/tests, already in the CI shards): symmetry, projection, equality withAverageOperator(Cuboid, HollowCylinder, HollowTorus x M0, M0ad_withT)test_poisson_adiabatic_gyrokinetic_stiffness_preconditioner(cylindrical, toroidal): same solution as without preconditioner, fewer than half the iterations; multigrid rejectedLocally: stiffness, weighted average, gyrokinetic Poisson and Poisson tests pass serially and on 2 ranks; default-params test of
DriftKineticElectrostaticAdiabaticpasses; feectools kron tests pass serially and on 2 ranks.🤖 Generated with Claude Code