Skip to content

Poisson profiling cases with mass-matrix and stiffness preconditioners; Kronecker preconditioners in ImplicitDiffusion - #739

Merged
spossann merged 10 commits into
develfrom
more-poisson-profiling-examples
Oct 9, 2026
Merged

spossann merged 10 commits into
develfrom
more-poisson-profiling-examples

Conversation

@spossann

@spossann spossann commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

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

spossann and others added 8 commits October 9, 2026 10:11
…s; wire Kronecker preconditioners into ImplicitDiffusion

- ImplicitDiffusion/PoissonSolve/PoissonAdiabaticGyrokinetic: precond="MassMatrixPreconditioner"
  (and the Diagonal variant) now builds the preconditioner from M0 (it was silently ignored),
  new precond="StiffnessPreconditioner" (sigma = sigma_1, rebuilt when sigma_1/dt changes),
  new option precond_params (kwargs for the preconditioner), default precond=None
- PoissonSolve: default stab_mat="M0" (was "Id")
- ImplicitDiffusion: pass pc only to "pcg" (solver="cg" raised a TypeError)
- StiffnessPreconditioner: take the 1d difference matrices from the 3d derivative blocks
  (tokronstencil); the hand-built ones were wrong for a single periodic element
- new profiling cases cube_strong_scaling_{massmatrix,stiffness} with submit scripts
- tests: Kronecker preconditioners in PoissonSolve (pcg/cg, M0/Id), stiffness preconditioner
  with changing dt, single-element regression; existing tests use precond=None

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
profiling/submit_*.py -> profiling/examples/<Model>/submit_*.py, so each submit script sits next to
the folder with its params file. The scripts put profiling/ on sys.path for the driver modules,
params_dir is relative to the new location.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n path in profiling workflow

The profiling workflow called the nonexistent profiling/submit_diocotron_job.py with --language
and --compiler, which the submit scripts did not accept. All submit scripts now take both
(defaults fortran/GNU, as hardcoded before) and pass them to ProfilingCase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ussian exact solution

- 8 new cases (hollow cylinder and full hollow torus, a1=1e-2, a2=1, Lz=R0=4) with no, multigrid,
  mass-matrix and stiffness preconditioner, with submit scripts next to their folders
- all 12 Poisson cases (incl. the cube ones) use a Gaussian 0-form on the logical domain centered at
  eta=(0.5,0.5,0.5) as exact solution, with the analytic -Laplace(phi) per mapping as source; errors
  are computed at the pushed-forward evaluation points, thresholds calibrated at 64^3
- maxiter=3000 for all cases (multigrid on the hollow torus needs ~190 iterations at 32^3)
- slice plots in physical coordinates (cross-section / poloidal plane) or logical coordinates

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mit script names

- profiling/examples/Poisson/generate_cases.py writes params_poisson.py and the submit script of all
  12 cases (3 mappings x 4 preconditioners) from one template and formats them with ruff; it also
  holds the error limits calibrated at 64^3
- submit_poisson_strong_scaling*.py -> submit_poisson_cube_strong_scaling*.py (labels unchanged),
  docstring/name/description in the same wording as the other cases

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dim_reduce=None explicitly

- MassMatrixDiagonalPreconditioner was MassMatrixPreconditioner(dim_reduce=None, diagonal_scaling=True);
  with diagonal_scaling=True as default it is now MassMatrixPreconditioner(M, dim_reduce=None)
- "MassMatrixDiagonalPreconditioner" removed from OptsMassPrecond and OptsDiffusionPrecond (clean break)
- variational propagators and variational_utilities: MassMatrixPreconditioner(..., dim_reduce=None)
  (cheap update_mass_operator for weights that change every step), behavior unchanged; default
  precond option "MassMatrixPreconditioner"
- ShearAlfvenCurrentCoupling5D: default precond "MassMatrixPreconditioner" (dim_reduce=0, fixed mass matrix)
- default dim_reduce=0 kept: exact for weights/mappings varying only in eta1 (2 PCG iterations vs 7-10
  on HollowCylinder), comparable otherwise; docstring explains when to use None; the reference of the
  diagonal scaling moved to MassMatrixPreconditioner

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread profiling/examples/Poisson/generate_cases.py Outdated
@spossann
spossann requested review from max-models and a balanced review from Copilot October 9, 2026 11:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Adiabatic gyrokinetic multigrid is built before the stabilization projection, leaving it configured for a different operator.

1 open finding
What changed in this PR

Adds Kronecker preconditioners to Poisson diffusion solves and expands strong-scaling profiling across cube, hollow-cylinder, and hollow-torus geometries.

Changes:

  • Implements mass-matrix and stiffness preconditioners in ImplicitDiffusion.
  • Consolidates mass preconditioners and fixes single-element periodic stiffness factors.
  • Generates 12 Poisson profiling cases and relocates profiling submit scripts.
File Description
src/​struphy/​propagators/​variational_viscosity.py Migrates mass preconditioners.
src/​struphy/​propagators/​variational_resistivity.py Migrates mass preconditioners.
src/​struphy/​propagators/​variational_qb_evolve.py Migrates mass preconditioners.
src/​struphy/​propagators/​variational_pb_evolve.py Migrates mass preconditioners.
src/​struphy/​propagators/​variational_momentum_advection.py Migrates mass preconditioners.
src/​struphy/​propagators/​variational_mag_field_evolve.py Migrates mass preconditioners.
src/​struphy/​propagators/​variational_entropy_evolve.py Migrates mass preconditioners.
src/​struphy/​propagators/​variational_density_evolve.py Migrates and updates dynamic preconditioners.
src/​struphy/​propagators/​tests/​test_poisson.py Tests Kronecker preconditioners and dynamic timestep handling.
src/​struphy/​propagators/​tests/​test_gyrokinetic_poisson.py Preserves unpreconditioned test behavior.
src/​struphy/​propagators/​shear_alfven_current_coupling_5d.py Updates the default preconditioner.
src/​struphy/​propagators/​poisson_solve.py Exposes new preconditioner options and defaults.
src/​struphy/​propagators/​poisson_adiabatic_gyrokinetic.py Exposes diffusion preconditioners.
src/​struphy/​propagators/​implicit_diffusion.py Builds and updates selected preconditioners.
src/​struphy/​io/​options.py Revises preconditioner literals.
src/​struphy/​feec/​variational_utilities.py Migrates utility preconditioning.
src/​struphy/​feec/​tests/​test_stiffness_preconditioner.py Covers single-element periodic directions.
src/​struphy/​feec/​tests/​test_preconditioner_transpose.py Updates consolidated preconditioner tests.
src/​struphy/​feec/​preconditioner.py Consolidates mass preconditioners and fixes derivative factors.
profiling/​examples/​ToyGyrokinetic/​submit_diocotron_strong_scaling.py Relocates submission and adds compiler options.
profiling/​examples/​Poisson/​submit_poisson_hollow_torus_strong_scaling.py Submits unpreconditioned torus profiling.
profiling/​examples/​Poisson/​submit_poisson_hollow_torus_strong_scaling_stiffness.py Submits stiffness torus profiling.
profiling/​examples/​Poisson/​submit_poisson_hollow_torus_strong_scaling_multigrid.py Submits multigrid torus profiling.
profiling/​examples/​Poisson/​submit_poisson_hollow_torus_strong_scaling_massmatrix.py Submits mass-matrix torus profiling.
profiling/​examples/​Poisson/​submit_poisson_hollow_cylinder_strong_scaling.py Submits unpreconditioned cylinder profiling.
profiling/​examples/​Poisson/​submit_poisson_hollow_cylinder_strong_scaling_stiffness.py Submits stiffness cylinder profiling.
profiling/​examples/​Poisson/​submit_poisson_hollow_cylinder_strong_scaling_multigrid.py Submits multigrid cylinder profiling.
profiling/​examples/​Poisson/​submit_poisson_hollow_cylinder_strong_scaling_massmatrix.py Submits mass-matrix cylinder profiling.
profiling/​examples/​Poisson/​submit_poisson_cube_strong_scaling.py Updates unpreconditioned cube submission.
profiling/​examples/​Poisson/​submit_poisson_cube_strong_scaling_stiffness.py Submits stiffness cube profiling.
profiling/​examples/​Poisson/​submit_poisson_cube_strong_scaling_multigrid.py Updates multigrid cube submission.
profiling/​examples/​Poisson/​submit_poisson_cube_strong_scaling_massmatrix.py Submits mass-matrix cube profiling.
profiling/​examples/​Poisson/​hollow_torus_strong_scaling/​params_poisson.py Defines unpreconditioned torus case.
profiling/​examples/​Poisson/​hollow_torus_strong_scaling_stiffness/​params_poisson.py Defines stiffness torus case.
profiling/​examples/​Poisson/​hollow_torus_strong_scaling_multigrid/​params_poisson.py Defines multigrid torus case.
profiling/​examples/​Poisson/​hollow_torus_strong_scaling_massmatrix/​params_poisson.py Defines mass-matrix torus case.
profiling/​examples/​Poisson/​hollow_cylinder_strong_scaling/​params_poisson.py Defines unpreconditioned cylinder case.
profiling/​examples/​Poisson/​hollow_cylinder_strong_scaling_stiffness/​params_poisson.py Defines stiffness cylinder case.
profiling/​examples/​Poisson/​hollow_cylinder_strong_scaling_multigrid/​params_poisson.py Defines multigrid cylinder case.
profiling/​examples/​Poisson/​hollow_cylinder_strong_scaling_massmatrix/​params_poisson.py Defines mass-matrix cylinder case.
profiling/​examples/​Poisson/​generate_cases.py Generates all Poisson profiling cases.
profiling/​examples/​Poisson/​cube_strong_scaling/​params_poisson.py Updates unpreconditioned cube case.
profiling/​examples/​Poisson/​cube_strong_scaling_stiffness/​params_poisson.py Defines stiffness cube case.
profiling/​examples/​Poisson/​cube_strong_scaling_multigrid/​params_poisson.py Updates multigrid cube case.
profiling/​examples/​Poisson/​cube_strong_scaling_massmatrix/​params_poisson.py Defines mass-matrix cube case.
.github/​workflows/​reusable-profiling-clusters.yml Calls the relocated Diocotron script.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/struphy/propagators/poisson_adiabatic_gyrokinetic.py Outdated
max-models
max-models previously approved these changes Oct 9, 2026
Multigrid would be built in ImplicitDiffusion.allocate() for the stabilization without the average,
which this propagator applies afterwards, and is never updated (sigma is constant), so it would
precondition the wrong operator. The multigrid field is removed again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
max-models
max-models previously approved these changes Oct 9, 2026
The 12 params files and submit scripts stay as they are; they are edited directly from now on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@spossann
spossann merged commit 3b71fe5 into devel Oct 9, 2026
28 checks passed
@spossann
spossann deleted the more-poisson-profiling-examples branch October 9, 2026 15:16
spossann added a commit that referenced this pull request Oct 9, 2026
…stab, weighted average in the stabilization (#740)

## Summary

Makes `StiffnessPreconditioner` work for `PoissonAdiabaticGyrokinetic`
(and other `ImplicitDiffusion` operators 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 for
`PoissonAdiabaticGyrokinetic`.

Points the `feectools` submodule to the branch of
struphy-hub/feectools#102 (`KroneckerSumSolver` with stiffness
coefficients and additional Kronecker terms, feectools 0.8.0). The
`feectools` pin in `pyproject.toml` is raised once 0.8.0 is on PyPI.

### `StiffnessPreconditioner`: `mid`, `stab`, `stab_average`

Preconditioner 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 in `mass_ops` (e.g. `"M1gyro"`,
`"M1perp"`) or the operator itself (`WeightedMassOperator`, or
sums/multiples such as `M1gyro = M1_MHDeq - M1para_MHDeq`), with the
domain/codomain of $\mathbb M_{k+1}$; default $\mathbb M_{k+1}$. Blocks
with zero mean (e.g. the parallel direction of a perpendicular operator)
have no stiffness term; their 1d stiffness still defines the eigenbasis
(`stiffness_coeffs` in feectools#102).
- `stab`: name or operator with the domain/codomain of $\mathbb M_k$
(e.g. `"M0ad_withT"`), or `"Id"`; default $\mathbb M_k$.
- `stab_average` (gradient only): directions averaged in the
stabilization, e.g. `"eta3"`, `"eta2 eta3"`. The term is approximated by
$\bar s\,(M_1 \otimes M_2 \otimes M_3 - F_1 \otimes F_2 \otimes F_3)$
with $F_d = c_d c_d^T/(\mathbb 1^T c_d)$, $c_d = M_d \mathbb 1$, which
is exact in the eigenbasis of periodic directions (`mass_terms` in
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 `AverageOperator` when the weight of
$\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$.

**Why:** with the plain (unweighted) `AverageOperator`, the toroidal
stabilization $\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 `PoissonAdiabaticGyrokinetic` solves it with pcg.
**Toroidal results change** (the average is now weighted with $\sqrt g$,
and with $n/T$ of `M0ad_withT`, a flux function for the usual
equilibria); cylindrical results are unchanged.

### `ImplicitDiffusion`, `PoissonSolve`, `PoissonAdiabaticGyrokinetic`

- New option `stab_average`; `ImplicitDiffusion` builds `stab_mat @ (I -
WeightedAverageProjection)` before the preconditioners and passes
`diffusion_mat`, `stab_mat` and `stab_average` to the stiffness
preconditioner. `precond="MultiGrid"` is rejected with `stab_average`.
- `PoissonAdiabaticGyrokinetic`: `stab_average` follows from
`which_geometry` (cylindrical: `"eta3"`, toroidal: `"eta2 eta3"`)
instead of patching `_stab_mat` after `allocate()`; multigrid rejected.

### Results

PCG iterations, `PoissonAdiabaticGyrokinetic` (`M1gyro`, `M0ad_withT`,
epsilon = 0.1, 12x12x8 elements):

| geometry | no preconditioner | stiffness preconditioner |
|---|---|---|
| cylindrical (HollowCylinder) | 34 | 10 |
| toroidal (HollowTorus) | 109 (429 before, non-symmetric) | 20 |

On the unit cube the preconditioner is exact (2 iterations) for constant
anisotropic or perpendicular `mid`, a scaled `stab` or `stab="Id"`
(whose average is Euclidean, $F_d = \mathbb 1 \mathbb 1^T/n$), and
`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_options`
- `test_weighted_average_projection.py` (new file in `feec/tests`,
already in the CI shards): symmetry, projection, equality with
`AverageOperator` (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 rejected

Locally: stiffness, weighted average, gyrokinetic Poisson and Poisson
tests pass serially and on 2 ranks; default-params test of
`DriftKineticElectrostaticAdiabatic` passes; feectools kron tests pass
serially and on 2 ranks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
max-models added a commit that referenced this pull request Oct 9, 2026
MassMatrixDiagonalPreconditioner was removed on devel (#739); the CuPy test now checks
MassMatrixPreconditioner with and without diagonal scaling and with dim_reduce=None instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants