Skip to content

DriftKineticElectrostaticAdiabatic: Poisson solve between the two guiding-center pushers - #799

Merged
spossann merged 3 commits into
develfrom
dk-strang-order
Oct 20, 2026
Merged

spossann merged 3 commits into
develfrom
dk-strang-order

Conversation

@spossann

@spossann spossann commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Summary

Both guiding-center pushers of DriftKineticElectrostaticAdiabatic move the markers and use the potential. With the old propagator order [gc_poisson, push_gc_bxe, push_gc_para], a Strang step (StruphyModel.integrate: all but the last with dt/2, the last with dt, then the others reversed) was

P, bxe(dt/2), para(dt), bxe(dt/2), P

so para(dt) and the closing bxe(dt/2) pushed with a potential solved before a drift; the two back-to-back Poisson solves at the step boundary were redundant.

This PR reorders to [push_gc_bxe, gc_poisson, push_gc_para]:

bxe(dt/2), P, para(dt), P, bxe(dt/2)
  • para(dt) and the closing bxe(dt/2) use a freshly solved potential; the two bxe(dt/2) at a step boundary act as one bxe(dt) with the potential solved before it.
  • Same cost: two Poisson solves per Strang step.
  • No extra Poisson solve for saving data (too expensive): since a time step ends with a push, the saved phi and en_phi belong to the marker positions before that last push (bxe(dt/2) with Strang, para(dt) with Lie-Trotter); this is stated in doc_discretization.
  • doc_discretization explains the order; the class link now points to GyrokineticPoissonSolve.

Note on Lie-Trotter

Both examples (itg_cylindre, cyclone) use split_algo="LieTrotter". There the reorder only moves the stale potential from para to bxe (one push per step uses the potential of the previous step in either order), so their results may change slightly without becoming more accurate. The gain is for Strang splitting.

Tests

  • pytest -m single --model-name DriftKineticElectrostaticAdiabatic src/struphy/models/tests/default_params/ (serial)
  • both examples with --test-run
  • ruff, doc/check_models_doc.py

Found while implementing the full-orbit counterpart VlasovPoissonElectrostaticAdiabatic (separate branch vlasov-poisson-adiabatic, not yet pushed), which uses the order drift, Poisson, E-kick, v x B rotation for the same reason.

🤖 Generated with Claude Code

…ding-center pushers

Reorder the propagators from [gc_poisson, push_gc_bxe, push_gc_para] to
[push_gc_bxe, gc_poisson, push_gc_para]. Both pushers move the guiding centers,
so with Strang splitting the old order pushed para(dt) and the closing bxe(dt/2)
with a potential solved before a drift. Now the Strang step is
bxe(dt/2), P, para(dt), P, bxe(dt/2) at the same cost of two Poisson solves.

Also solve the Poisson equation in update_scalar_quantities (once per saved step),
so that en_phi, en_tot and the saved phi belong to the current marker positions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread src/struphy/models/drift_kinetic_electrostatic_adiabatic.py Outdated
spossann and others added 2 commits October 20, 2026 08:01
… data

Remove the update_scalar_quantities override from the previous commit (review of #799):
solving the Poisson equation once per saved step is too expensive. The saved potential
and en_phi belong to the marker positions before the last push of a time step; this is
stated in doc_discretization.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@spossann
spossann marked this pull request as ready for review October 20, 2026 06:22
@spossann
spossann merged commit 1b88e82 into devel Oct 20, 2026
28 checks passed
@spossann
spossann deleted the dk-strang-order branch October 20, 2026 06:36
spossann added a commit that referenced this pull request Oct 20, 2026
…atic electrons) and fixes of the full-f Poisson background (#800)

## Summary

### New model `VlasovPoissonElectrostaticAdiabatic`

The full-orbit (6D) counterpart of `DriftKineticElectrostaticAdiabatic`:
Vlasov ions in the electrostatic potential and a static B0, electrons
through the linearized Boltzmann response.

    df/dt + v . grad f + (1/eps) (-grad phi + v x B0) . df/dv = 0
-(eps/alpha^2) Delta phi + n0/(Z eps T0) (phi - <phi>) = int f d^3v - n0

- New propagator `AdiabaticPoissonSolve` (subclass of
`ImplicitDiffusion`):
- `quasi_neutral=True` (default) drops the vacuum term, so the solve is
a weighted mass-matrix solve;
- `which_geometry` adds the optional flux-surface average `phi - <phi>`,
as in the drift-kinetic model;
  - `stab_mat="M0ad_withT"` (weight n0/T0).
- `ImplicitDiffusion` now allows `stab_mat="M0ad_withT"` and
`diffusion_mat=None` (no diffusion term), with option checks.
- Propagator order `push_eta, poisson, push_v_E, push_vxb`: with Strang
splitting both E-kicks use the potential of the current marker positions
(drift-kick-kick-drift). This is the same reasoning as #799 for the
drift-kinetic model.
- Tests:
- manufactured solutions for the Poisson solve, including the zonal
average;
  - option checks;
- verification: ion-acoustic Landau damping along B0, quasi-neutral
(gamma -0.168 vs -0.153) and with the vacuum term (-0.179 vs -0.194).
- Example `slab_drift_wave`: a drift wave in a density gradient.
- **Equilibrium:** the initial state must be the exact Vlasov
equilibrium f0(x + eps v_y), here a Maxwellian shifted by the ion
diamagnetic velocity.
- **Time step:** in the quasi-neutral limit the explicit splitting needs
`dt c_s pi/h <~ 2`, since the ion-acoustic branch has no Debye cutoff.
- **Result:** measured omega = 0.262, against 0.262 from the full-orbit
dispersion relation and 0.287 from the drift-kinetic one (the O(k_perp^2
rho_i^2) gap).

### Example `slab_drift_wave` for `DriftKineticElectrostaticAdiabatic`

The same physical setup as the full-orbit example (box, eps = 0.1,
kappa_n = 1, T_i = T_e = 1, mode k = (pi/2, 3, 0.05)), for a direct
comparison:

- drift-kinetic ions with the guiding-center Maxwellian n0(X) M (an
exact equilibrium);
- explicit guiding-center pushers, dt = 0.25;
- `LoadingParameters` and `GyroMaxwellian2D` with `B0 = 1` (both default
to 2);
- Same electron model as the full-orbit example: Boltzmann response for
all modes, i.e. no flux-surface average. For this,
`GyrokineticPoissonSolve` gets the new option `which_geometry=None`; the
default stays `"cylindrical"`.
- In the shearless slab the physical choice would be the average along
the field lines (over z, `"cylindrical"`), which removes the electron
response of the k_z = 0 modes.
- That is not possible in the quasi-neutral full-orbit model, where the
field equation has no polarization term to determine <phi>, and it is
not needed for the studied mode, which has k_z != 0.
- With `"cylindrical"`, the ion convective cells of the k_z = 0 modes
(omega ~ 3) also made the explicit push unstable at dt = 0.25.
- **Result:** measured omega = 0.291 against 0.287 from the
drift-kinetic dispersion relation; the full-orbit example gives 0.262
for a predicted 0.262. `pproc_slab_drift_wave.py --compare <full-orbit
run>` overlays both modes.

### Fixes in the full-f Poisson right-hand side (separate commits)

1. **`ImplicitDiffusion`: the full-f neutralizing background is `-n0`,
not `-Z n0`.**
- The H1 kernels deposit the particle density, and the charge (number
and sign) is in the coefficient (`alpha^2/eps`, with eps ~ 1/Z).
- With `-Z n0`, the right-hand side was `n - Z n0`, which is wrong for Z
!= 1. Electrons (e.g. `ColdPlasmaVlasov`) got `n + n0`.
   - All existing examples and tests use Z = 1.
2. **Initial Poisson solve: delta-f deposit without double
subtraction.**
- `VlasovAmpereOneSpecies`, `VlasovMaxwellOneSpecies` and
`ColdPlasmaVlasov` deposited the delta-f weights for the initial solve,
and `ImplicitDiffusion` added the full-f background on top: `n - 2 n0`.
In periodic examples the compatibility projection hid the offset; with
Dirichlet BCs or a non-uniform n0 the initial phi was wrong.
- New constructor argument `ImplicitDiffusion(..., delta_f_source=True)`
(also `PoissonSolve`): particle sources are deposited with delta-f
weights also for full-f markers (weights restored afterwards), and no
background source is added. The three models use it for
`initial_poisson`.
- The delta-f deposit has far less noise than full-f weights minus n0:
for a 1 % perturbation the initial phi has a relative error of 1 %
instead of 400 % (100 markers per cell).
- `DeltaFParticles6D` (`LinearVlasovAmpere/MaxwellOneSpecies`) no longer
gets the `-n0` background: their weights are delta-f already.
- `VlasovMaxwellOneSpecies` `gauss_error` for full-f markers: the
diagnostic deposits the full-f weights (whose charge the time stepping
conserves) minus a cached t = 0 offset `alpha^2/eps (rho_full(0) -
rho_delta_f(0))`. After 20 steps the residual is about 0.1 of the field
term instead of about 3 times it (f0 deposit noise).
3. **`GyrokineticPoissonSolve` docstrings corrected:**
   - `stab_mat` default `"M0ad_withT"`, not `"Id"`;
- the solved equation includes `phi - <phi>`, `1/eps` and the
`rho_coeffs`;
   - the parameters are those of the constructor.

Each of fixes 1 and 2 comes with tests that fail on the old code:
phi_rms 2.0 (Z = 2 background), 0.09 (double subtraction, background for
delta-f markers), relative error 4.1 (full-f deposit).

## Tests (local)

- `test_adiabatic_poisson.py` (serial, 2 ranks); `test_poisson.py`,
`test_gyrokinetic_poisson.py` (2 ranks, 140 passed)
- verification tests of `VlasovPoissonElectrostaticAdiabatic`,
`VlasovAmpereOneSpecies`, `VlasovMaxwellOneSpecies` (2 ranks)
- default-params model tests of the new model,
`DriftKineticElectrostaticAdiabatic`, `VlasovAmpereOneSpecies`,
`VlasovMaxwellOneSpecies`, `ColdPlasmaVlasov` (4 ranks)
- example `--test-run` + pproc, ruff, `doc/check_models_doc.py`,
`doc/check_propagator_doc.py`

## Follow-ups (not in this PR)

- In the slab drift-wave example the kinetic energy of f - f0 drifts by
1-2 % of the thermal energy over t = 15 (unexplained).

Related: #799 (propagator order of `DriftKineticElectrostaticAdiabatic`,
merged).

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

---------

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.

1 participant