Repository navigation
KroneckerSumSolver: stiffness coefficients and additional Kronecker terms - #102
Merged
Merged
Conversation
…erms; version 0.8.0
- stiffness_coeffs: coefficients a_d of the stiffness terms; S_d defines the eigenbasis also for a_d = 0
- mass_terms: additional terms c_t F_{t,1} x ... x F_{t,n}, used through their diagonal in the eigenbasis
(exact if the U_d diagonalize the F_{t,d}, e.g. M_d, or c c^T with c = M_d 1 in a direction with
S_d 1 = 0: removing the mean in a periodic direction)
- default arguments give exactly the previous behavior
- test: coefficients (one zero) and a mean-removing term, serial and parallel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 9, 2026
There was a problem hiding this comment.
🟡 Changes recommended
Signed terms can cause cancellation that makes the pseudo-inverse threshold incorrectly discard nonzero eigenvalues.
2 open findings
What changed in this PR
Generalizes KroneckerSumSolver for stiffness preconditioning with weighted stiffness terms and additional Kronecker mass terms.
Changes:
- Adds stiffness coefficients and additional mass terms.
- Adds serial and MPI tests for the generalized solver.
- Bumps the package version to 0.8.0.
| File | Description |
|---|---|
pyproject.toml |
Updates the package version. |
feectools/linalg/kron.py |
Implements and documents generalized Kronecker terms. |
feectools/linalg/tests/test_kron_stencil_matrix.py |
Tests weighted stiffness and mean-removal terms. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
spossann
added a commit
to struphy-hub/struphy
that referenced
this pull request
Oct 9, 2026
…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>
…iagonal for signed terms With stiffness_coeffs or mass_terms, the cutoff is relative to the global maximum of |Lambda| (terms may cancel; the sum of their bounds could discard non-zero eigenvalues). Without them the previous estimate is kept. Docstring: mean-removing factor c c^T / (1^T c). Regression test with sigma = 1 and a term (eps - 1) M x M x M. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
spossann
added a commit
to struphy-hub/struphy
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>
spossann
added a commit
to struphy-hub/struphy
that referenced
this pull request
Oct 9, 2026
…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>
spossann
added a commit
to struphy-hub/struphy
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>
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
Generalizes
KroneckerSumSolver(fast diagonalization of sums of Kronecker products), needed by struphy'sStiffnessPreconditionerfor operators like the adiabatic gyrokinetic Poisson equation:stiffness_coeffs: coefficientsmass_terms: additional termsNoneforTests
test_kron_sum_solver_terms_{ser,par}: stiffness coefficients (one of them zero) and a term removing the mean in the two periodic directions; exact (pseudo-)inverse, relative residual < 1e-12.test_kron_stencil_matrix.py: 25 passed serially, 45 on 2 ranks.Paired struphy PR: struphy-hub/struphy#740
🤖 Generated with Claude Code