Skip to content

VlasovMaxwellCoupling: implicit energy-conserving Vlasov-Maxwell step with quasi-neutral limit; model VlasovMaxwellQuasiNeutral - #821

Merged
spossann merged 24 commits into
develfrom
vlasov-maxwell-quasi-neutral
Oct 23, 2026
Merged

spossann merged 24 commits into
develfrom
vlasov-maxwell-quasi-neutral

Conversation

@spossann

@spossann spossann commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Follow-up of #819 (merged); based on devel.

Normalization (latest change)

VlasovMaxwellQuasiNeutral uses the cyclotron velocity scale ($\hat v = \hat\Omega_c \hat x$, $\hat t = 1/\hat\Omega_c$, $\varepsilon = 1$) with two new/redefined species equation parameters, passable at model initialization:

$$\lambda = \frac{\hat\Omega_c \hat x}{c},,\qquad \kappa = \frac{\hat\Omega_p \hat x}{c} = \alpha\lambda,,\qquad \lambda^2 \partial_t \mathbf E = \nabla\times\mathbf B - \kappa^2 \mathbf J,,$$

with no coefficients in the Vlasov equation and Faraday's law; the quasi-neutral limit is lam=0. (kappa was $\hat\Omega_p \hat t$ before, unused; in the light scale both definitions agree.) For a general velocity scale the coefficients are $\varepsilon^2\lambda^2$, $\kappa^2\varepsilon$, $1/\varepsilon$ and Gauss $\kappa^2/(\varepsilon\lambda^2) = \alpha^2/\varepsilon$; they reduce to $1, \alpha^2/\varepsilon, 1/\varepsilon$ for the light scale. Documented under Equation parameters in models-normalization.rst (which also had a spurious $2\pi$ in the cyclotron velocity, removed). doc_normalization is now directly below doc_pde (rule added to models/AGENTS.md).

Consequences in this PR: the propagator option kappa (displacement current) is renamed lam (coefficient $\lambda^2$), defaults $\lambda = \varepsilon\lambda_s$, $c_2 = \kappa_s^2\varepsilon$, $c_3 = 1/\varepsilon$; the model's scalars en_E, en_B, en_f are the parts of the conserved energy and en_tot is the conserved energy (en_H removed); the initial Poisson solve uses Gauss' law $\lambda^2 \nabla\cdot\mathbf E = c_2 \rho$ of the coupling step and is skipped for $\lambda = 0$ (gauss_error NaN). Example and verification test use lam, kappa=1, epsilon=-1, which are the old equations: the verification growth rate is unchanged (0.02770). Signed cyclotron units for a negative bulk charge: issue #825.

The sections below describe the propagator in its own coefficients; where they say kappa for the displacement-current coefficient, read lam (with $\lambda^2$), and en_H is now en_tot.

Summary

New propagator VlasovMaxwellCoupling: the steps of MaxwellWeakAmpere and VlasovAmpereCoupling in one implicit Crank–Nicolson step, for

$$\kappa, \mathbb M_1 \dot{\mathbf e} = c_1, \mathbb C^\top \mathbb M_2 \mathbf b - c_2, \mathbf j(\mathbf V),\qquad \dot{\mathbf b} = -\mathbb C \mathbf e,\qquad \dot{\mathbf V} = c_3, \overline{DF^{-\top}} \mathbf\Lambda^1 \mathbf e$$

(marker positions fixed), with options kappa (default 1), c1 (default 1), c2, c3 (default $\alpha^2/\varepsilon$, $1/\varepsilon$ of the species). $\kappa = 0$ drops the displacement current (quasi-neutral limit, $c_1 \nabla\times\mathbf B = c_2 \mathbf J$).

  • Schur system for the mid-point field: $\big(\kappa \mathbb M_1 + \tfrac{\Delta t^2}{4}(c_1 \mathbb C^\top \mathbb M_2 \mathbb C + c_2 c_3 \mathbb A)\big), \mathbf e^{\mathrm{mid}} = \kappa \mathbb M_1 \mathbf e^n + \tfrac{\Delta t}{2}(c_1 \mathbb C^\top \mathbb M_2 \mathbf b^n - c_2 \mathbf j^n)$ with the accumulated $\mathbb A$, $\mathbf j$ of the vlasov_maxwell kernel. Then $\mathbf b^{n+1} = \mathbf b^n - \Delta t, \mathbb C \mathbf e^{\mathrm{mid}}$, velocities pushed with $\mathbf e^{\mathrm{mid}}$, $\mathbf e^{n+1} = 2\mathbf e^{\mathrm{mid}} - \mathbf e^n$ for $\kappa > 0$ (exactly CN) and $\mathbf e^{n+1} = \mathbf e^{\mathrm{mid}}$ for $\kappa = 0$ (avoids the undamped even–odd oscillation of the implicit mid-point rule for the algebraic variable $\mathbf e$).
  • Well-posed for $\kappa \to 0$: the curl-curl term stays; its kernel (discrete gradients, harmonic fields) is absorbed by the plasma term $c_2 c_3 \mathbb A \approx c_2 c_3 \mathbb M_{1,n}$. Needs positive weights: with the control variate, $\kappa = 0$ is rejected (assert), $\kappa > 0$ warns. At $\kappa = 0$ the mid-point current is weakly divergence-free ($\mathbb G^\top \mathbf j^{\mathrm{mid}} = 0$); $\mathbf E$ is its Lagrange multiplier.
  • Exact energy conservation of $H = \frac\kappa2 \mathbf e^\top \mathbb M_1 \mathbf e + \frac{c_1}{2} \mathbf b^\top \mathbb M_2 \mathbf b + \frac{c_2}{2c_3} \sum_p w_p |\mathbf v_p|^2$ (method energy()), for full-f weights.
  • Preconditioner (default): StiffnessPreconditioner(mass_ops, "curl", sigma=kappa + dt^2 c2 c3 n_ref/4, mid=dt^2 c1/4 M2, stab="M1"), built in prepare_time_step and rebuilt when dt changes (as in MaxwellWeakAmpere: StiffnessPreconditioner as default; document the discretization of MaxwellWeakAmpere and VlasovAmpereCoupling #819). The stabilization stays positive at $\kappa = 0$. n_ref defaults to the density average $\mathbf 1^\top \mathbb A \mathbf 1 / \mathbf 1^\top \mathbb M_1 \mathbf 1$.
  • Matrix-free option matrix_free (default False), as in the other 6D couplings of Matrix-free particle matrices (MarkerOperator) in the 6D coupling propagators #817: the particle matrix in the Schur operator is a MarkerOperator (vlasov_maxwell_mf + marker_operator_symm_dot), with the fillings $w_p G^{-1}$ stored per marker.
  • Initial projection at κ = 0 (option initial_projection, default True): the constraint holds only at the mid-point, so its residual $\mathbf r^n = c_1 \mathbb C^\top \mathbb M_2 \mathbf b^n - c_2 \mathbf j(\mathbf V^n)$ obeys $\mathbf r^{n+1} = -\mathbf r^n$; initial data that violate $c_1 \nabla\times\mathbf B = c_2 \mathbf J$ (e.g. marker noise in $\mathbf J$) would oscillate with period 2 without damping. In the setup (first prepare_time_step, before the initial data are saved; not for a restored restart state), $(\mathbf b, \mathbf V)$ is replaced by the mid-point state of one step (implicit Euler of dt/2), which satisfies the constraint exactly and does not depend on dt; $\mathbf e$ (initial Poisson) is kept. The particle pushers of the splitting still change $\mathbf r$ by $O(\Delta t)$ per step.
  • No polar splines yet (asserted).

New model VlasovMaxwellQuasiNeutral: subclass of VlasovMaxwellOneSpecies with the splitting coupling → push_eta → push_vxb and the extra scalar en_H. VlasovMaxwellOneSpecies got a _link_propagator_variables hook and a default-params hook that keys on the last propagator and the class name (output for VlasovMaxwellOneSpecies unchanged).

Verification test models/tests/verification/test_verif_VlasovMaxwellQuasiNeutral.py: Weibel instability at κ = 0 (1e5 full-f markers with sobol_antithetic loading, dt = 1, t = 160); the growth rate fitted to en_B for 80 < t < 150 must be within 7 % of the analytical 0.02784. Fitted: 0.02770 / 0.02784 / 0.02782 on 1 / 2 / 4 ranks (~45 s serial, ~20–27 s on 2 or 4 ranks). With pseudo-random markers the fit is noise-limited (about 10 % low, ~7 % seed-to-seed scatter), hence the antithetic Sobol loading.

Example examples/VlasovMaxwellQuasiNeutral/weibel_quasi_neutral (--kappa 1|0, dt = 1, i.e. 20× the dt of the VlasovMaxwellOneSpecies Weibel example).

Results

PCG iterations of one step (8×4×1 cells, p = (2,2,1), 1280 full-f markers, tol 1e-13; energy error ≤ 3e-16 relative in every case):

dt kappa Stiffness Mass
0.1 1 13 10
0.1 0 20 72
2 1 19 58
2 0 22 77
20 1 23 80
20 0 23 80

$\kappa \to 0$: $|\mathbf b - \mathbf b_0| + |\mathbf V - \mathbf V_0|$ against the $\kappa = 0$ solution decreases linearly (8.4, 1.9, 0.22, 0.023 for $\kappa = 10^{-1}, \dots, 10^{-4}$).

Weibel example with sobol_antithetic loading (current version; 10⁵ full-f markers, 32 cells, t = 400, serial, ~2 min per run): γ = 0.02805 for κ = 1 and 0.02758 for κ = 0 (analytical 0.02784), max rel. change of en_H 2e-9 / 6e-9.

Earlier runs with pseudo-random markers:

run fitted γ max rel. change of en_H
κ = 1, dt = 1 0.0249 1.4e-9
κ = 0, dt = 1 0.0249 9.0e-10
κ = 0, dt = 0.25 0.0253 2.4e-10
κ = 0, dt = 1, with initial projection 0.0251 3.1e-9

The first three κ = 0 rows are without the initial projection (it was added later): there en_B shows a period-2 oscillation band (≈ 2e-7) for t < 80, which the projection removes (smooth en_B from t = 0, en_E 10–20× lower).

Analytical γ = 0.02784 for both κ (κγ² ≈ 8e-4 ≪ k² + 1). With pseudo-random markers the fitted rates were 10 % low, hardly dependent on dt: marker noise, removed by the antithetic Sobol loading above.

Known limitations / follow-ups

  • Existing code, found in review: with clones, sobol_standard/sobol_antithetic loadings have no clone offset in the Sobol index, so all clones draw the same markers (effective sample Np / nclones). Not used by the tests here; follow-up issue.

  • Strict quasi-neutrality is not enforced over the full split step: the coupling step makes $\mathbf J(\mathbf V^{\mathrm{mid}})$ divergence-free at frozen positions, but PushEta then moves the markers with $\mathbf V^{n+1}$.

  • Polar splines not supported (tensor-product V1 buffers; VlasovAmpereCoupling has the same limitation).

  • With the control variate (κ > 0 only) the energy is not exactly conserved and n_ref falls back to 1.

Tests run locally

(Updated after the matrix-free option and the initial projection. The branch has devel merged via #819, which also got devel merged, so the diff here is only this PR.)

  • test_vlasov_maxwell_coupling.py + test_matrix_free_6d.py: serial 40 passed, mpirun -n 4 39 passed + 1 mpi_skip. New: matrix-free vs assembled steps (Colella, κ = 1 and 0, incl. n_ref), discrete equations with both variants, test_initial_projection (sign flip of r without; r = 0, dt independence, e unchanged and stationary steps with the projection). test_maxwell_weak_ampere.py on the merged state: serial and 2 ranks.
  • Separate review agents for the matrix-free and the projection commits: no bugs in the matrix-free path; the projection review found that writing e_mid (an impulse of size 1/dt) into e replaced the Poisson field in the saved t = 0 data, fixed (only b and V are projected).

Earlier (first version):

  • src/struphy/propagators/tests/test_vlasov_maxwell_coupling.py: serial (15 passed) and mpirun -n 4 (14 passed, 1 mpi_skip): exact energy conservation (κ = 1, 0; both preconditioners; species and explicit c1 ≠ |c2| ≠ |c3|), residual of the discrete CN Ampère and Faraday laws (with the pushed V), weakly divergence-free mid-point current at κ = 0, O(dt²) agreement with MaxwellWeakAmpere + VlasovAmpereCoupling, linear convergence in κ with bounded iterations, rebuild on dt change, option validation, control-variate rejection. A mutation check (pusher constant c2 instead of c3) makes 6 of these fail.
  • Default-params model tests -m single for VlasovMaxwellQuasiNeutral and VlasovMaxwellOneSpecies (serial and 4 ranks); test_examples.py for the new example; doc/check_models_doc.py, doc/check_propagator_doc.py; ruff.
  • Separate review agent on the branch: no correctness bugs; its low-severity findings (polar splines, test weaknesses) are fixed in the second commit.

🤖 Generated with Claude Code

spossann and others added 9 commits October 23, 2026 08:13
…ling

Semi-discrete systems in struphy operator notation (M1, M2, C, Lambda^1, W),
Crank-Nicolson update, SchurSolver blocks A, B, C, resulting Schur system,
accumulated matrix/vector and conserved quantities.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…w docs]

The Schur operator M1 + dt^2/4 C^T M2 C is preconditioned by
StiffnessPreconditioner(curl, mid=dt^2/4 M2, stab=M1), built per time step in
prepare_time_step. Iterations hardly depend on dt (hollow cylinder 16x16x4,
p=3: 39 instead of 411 PCG iterations at dt = 100 h); for dt <~ h the mass
preconditioner stays slightly better. algo="explicit" falls back to the mass
preconditioner, solver="cg" drops the preconditioner with a warning.
New test file test_maxwell_weak_ampere.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nt preconditioners and solvers [preview docs]

- remove the iteration-count sentence from the MaxwellWeakAmpere docstring
- full-path cross-references (Derham.curl, SchurSolver, prepare_time_step,
  kernel functions in their *_kernels modules); feectools inverse as literal
- dev-feec.rst: new sections Preconditioners (struphy.feec.preconditioner,
  struphy.polar.kron_2dx1d, multigrid preconditioner) and Linear solvers
  (schur_solver, solver)
- dev-pic.rst: vlasov_maxwell accumulation kernel, new Pusher kernels section
  with push_v_with_efield

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The raw docstrings of SchurSolver, SchurSolverFull and SchurSolverFull3 used
\\text and \\right (a LaTeX line break in a raw string), which broke the
math; rewritten with pmatrix. Markdown links replaced by plain names, and the
SchurSolver Parameters section now matches the signature (precond,
solver_params).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n [preview docs]

Lynch, Rice and Thomas (1964) for the method, Sangalli and Tani (2016) for
preconditioning spline stiffness matrices; short description of the
generalized eigendecomposition used by KroneckerSumSolver.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nalization [preview docs]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One implicit Crank-Nicolson step for Ampere (kappa dE/dt), Faraday and the
electric force on the markers, with free coefficients c1, c2, c3. The Schur
system kappa M1 + dt^2/4 (c1 C^T M2 C + c2 c3 A) is solved for the mid-point
field; it stays well-posed for kappa -> 0 (no displacement current), where the
plasma term absorbs the kernel of the curl-curl operator. Exact energy
conservation (full-f), stiffness preconditioner with stabilization
kappa + dt^2 c2 c3 n_ref / 4.

The model subclasses VlasovMaxwellOneSpecies (propagator linking factored out),
adds the scalar en_H and a Weibel example for kappa = 1 and 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…general coefficients

- assert no polar splines in VlasovMaxwellCoupling (the V1 buffers are tensor-product)
- test_discrete_equations: residual of the CN Ampere and Faraday laws with c1, c2, c3
  all different (catches wrong coefficients in the Schur system or the pusher)
- energy test also with explicit coefficients; test the solver rebuild on dt change
  with a given n_ref; drop the tautological e^n-independence test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@spossann
spossann added this pull request to stack #822 October 23, 2026 08:58
spossann and others added 11 commits October 23, 2026 11:05
… other 6D couplings

With matrix_free=True the vector j and the fillings w_p G^-1 per marker are
accumulated by vlasov_maxwell_mf (AccumulatorVector) and the particle matrix in
the Schur operator is a MarkerOperator (marker_operator_symm_dot). Tests: same
steps as assembled (test_matrix_free_6d.py, kappa = 1 and 0), discrete
equations with both variants.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kappa = 0

At kappa = 0 the constraint holds at the mid-point only, so its residual
r = c1 C^T M2 b - c2 j(V) changes sign in every step: initial data that
violate it (e.g. marker noise in J) oscillate with period 2 without damping.
The option initial_projection (default True) replaces (b, V) once in the setup
(first prepare_time_step) by the mid-point state of one step, an implicit Euler
step of dt/2 that satisfies the constraint exactly and does not depend on dt.

Tests: sign flip without, vanishing residual, dt independence and stationary
steps with the projection; step-level tests at kappa = 0 run without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
e_mid of the projection is an impulse of size 1/dt; writing it into e replaced
the initial Poisson field in the saved t = 0 data (en_E, gauss_error). At
kappa = 0, e does not enter the steps, so only b and V are projected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… kappa = 0

1D-3V Weibel instability (k = 1.25, 1e5 full-f markers, dt = 1, t = 160) in the
quasi-neutral limit; the growth rate fitted to the magnetic energy for
80 < t < 150 must be within 15 % of the analytical 0.02784 (fitted: 0.02506,
the same on 1, 2 and 4 ranks). About 45 s serial, 25 s on 2 or 4 ranks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… with the same loading

With pseudo-random markers the fitted growth rate is noise-limited (about 10 % low,
7 % seed-to-seed scatter at 1e5 markers). With the antithetic Sobol loading the
current noise mostly cancels: gamma = 0.02770 / 0.02784 / 0.02782 on 1 / 2 / 4
ranks against the analytical 0.02784, so the tolerance is now 5 %. Review fixes:
comments on the kappa = 0 dispersion relation and on reading the scalars,
rmtree(ignore_errors=True). The example (Tend = 400) now gives gamma = 0.02805
(kappa = 1) and 0.02758 (kappa = 0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eed, 7 % tolerance

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@spossann
spossann marked this pull request as ready for review October 23, 2026 10:53
Base automatically changed from doc-weak-ampere to devel October 23, 2026 11:12
spossann and others added 2 commits October 23, 2026 14:43
…parameters lam and kappa

- species equation parameters lam = Omega_c x / c (new) and kappa = Omega_p x / c
  (redefined, was Omega_p t, unused); overridable in the model constructor
- VlasovMaxwellQuasiNeutral: velocity scale cyclotron, constructor arguments lam,
  kappa; Ampere's law lam^2 dE/dt = curl B - kappa^2/eps J; doc_normalization
  directly below doc_pde; energies en_E, en_B, en_f and en_tot = conserved energy
  of the coupling step; Gauss' law lam^2 div E = c2 rho for the initial Poisson
  solve, which is skipped for lam = 0 (gauss_error NaN)
- VlasovMaxwellCoupling: option kappa (displacement current) renamed to lam (with
  coefficient lam^2); defaults lam = eps lam_s, c2 = kappa_s^2 eps, c3 = 1/eps,
  i.e. lam_s, kappa_s^2, 1 for the cyclotron and 1, alpha^2/eps, 1/eps for the
  light velocity scale; energies() parts
- docs: lam and kappa under Equation parameters; cyclotron velocity without the
  spurious 2 pi; models/AGENTS.md: doc_normalization directly below doc_pde
- example, verification test (gamma unchanged, 0.02770) and tests with lam, kappa

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sign of lam, AGENTS.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e paragraph under Equation parameters, fix the energy reference in VlasovMaxwellCoupling

The :meth: reference to VlasovMaxwellCoupling.energy was inside a math block (not rendered); moved into the text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
spossann added a commit that referenced this pull request Oct 23, 2026
…rs lam and kappa; safeguard the parallel force

- Model DriftKineticMaxwell: velocity scale "thermal" (as DriftKineticElectrostaticAdiabatic), default
  BaseUnits(kBT=1.0) (also in the generated default parameter file), constructor overrides epsilon, lam, kappa;
  Ampere's law eps^2 lam^2 dE/dt + eps^2 kappa^2 n0/|B0|^2 dE_perp/dt = curl B - eps kappa^2 J_gc; doc_pde with
  doc_normalization below it (thermal scale only); scalars en_E, en_B, en_f from the parts of the conserved energy.
- DriftKineticMaxwellCoupling: option kappa (displacement current) renamed to lam (coefficient lam^2), defaults
  lam = eps lam_s, chi = (eps kappa_s)^2, c2 = kappa_s^2 eps, c3 = 1/eps (#821 equation parameters); energies();
  the internal E x B scaling of the assembled matrix renamed to skew (also in the dk_maxwell kernel).
- The parallel electric force b* . E / eps is applied only by the coupling step: the model asserts that push_para
  gets no potential and no electric field, doc_discretization lists which propagator applies each term, and a test
  checks that push_para leaves v_par unchanged for E != 0 (and changes it when given E).
- Tests, verification test and example in the new parameters (eps = 0.5, lam = 2, kappa = 6 give the same
  coefficients as before; same frequencies). Raw docstrings for the pusher constructors (LaTeX).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@spossann
spossann merged commit c5b225a into devel Oct 23, 2026
30 checks passed
@spossann
spossann deleted the vlasov-maxwell-quasi-neutral branch October 23, 2026 14:04
spossann added a commit that referenced this pull request Oct 23, 2026
… Maxwell step with polarization and quasi-neutral limit; model DriftKineticMaxwell (#823)

**Model docs (preview):**
https://struphy-hub.github.io/docs-preview/pr-823/sections/subsections/models-all.html#struphy.models.DriftKineticMaxwell

The drift-kinetic counterpart of `VlasovMaxwellCoupling` /
`VlasovMaxwellQuasiNeutral` (#821, merged).

## What

- **New propagator `DriftKineticMaxwellCoupling`**: Maxwell's equations,
the parallel acceleration of the guiding centers ($\dot v_\parallel =
\frac1\varepsilon \mathbf b^* \cdot\mathbf E$) and, as an option, the
$E\times B$ current in one Crank–Nicolson step at frozen positions.
Gauge-invariant formulation: the guiding-center current $\int f^{\rm
vol}\dot{\mathbf X}$ with $\mathbf E^* = \mathbf E -
\varepsilon\mu\nabla|B_0|$, not $-\nabla\phi$. The Schur system for
$\mathbf e^{\rm mid}$:
$$\Big(\mathbb M_E + \tfrac{\Delta t^2}{4}(c_1\mathbb C^\top\mathbb
M_2\mathbb C + c_2c_3\mathbb A_\parallel) + \tfrac{\Delta
t}{2}c_2\mathbb S\Big)\mathbf e^{\rm mid} = \dots,\qquad \mathbb M_E =
\lambda^2\mathbb M_1 + \chi\mathbb M_1^{\rm gyro}$$
- $\mathbb A_\parallel$ (rank one per marker) is the parallel response.
$\mathbb S$ (antisymmetric) is the $E\times B$ current.
  - Energy is conserved exactly in any geometry.
- The particle matrices are assembled (`dk_maxwell`) or matrix-free
(`dk_maxwell_mf` with the existing rank-1/antisymmetric `MarkerOperator`
kernels).
- Preconditioner: `StiffnessPreconditioner`. Initial projection for
$\lambda = 0$, as in #821.
- **Polarization term** $\chi\,\frac{n_0}{|B_0|^2}\partial_t\mathbf
E_\perp$, using the existing `M1gyro`. It is needed because the
drift-kinetic ordering removes the polarization drift. Without it the
quasi-neutral limit $\lambda = 0$ is singular for flute modes
($k_\parallel = 0$), and with only the displacement current the shear
Alfvén wave travels at the speed of light. With it, $\lambda = 0$ gives
the inertial Alfvén wave. `lam**2 + chi > 0` is asserted.
- **`PushGuidingCenterBxEstar` / `PushGuidingCenterParallel`** accept
either `phi` (the existing path, unchanged) or a new `e_field` (Hcurl),
with $\mathbf E^* = \mathbf E - \varepsilon\mu\nabla|B|$. The existing
kernels already read the E field.
- With `e_field` only the explicit algorithm is consistent: the discrete
gradients conserve a Hamiltonian with a potential, and without one they
would remove the work of E along the step.
- A discrete-gradient `algo` therefore falls back to `explicit` with a
warning, so the default options keep working.
- **New model `DriftKineticMaxwell`**, splitting `coupling` →
`push_para` → `push_bxe(e_field)`, and the example
`inertial_alfven_wave`.
- Normalized with the **ion thermal velocity**, as
`DriftKineticElectrostaticAdiabatic`, using the equation parameters
$\varepsilon$, $\lambda$, $\kappa$ of #821. Ampère's law becomes
$$\varepsilon^2\lambda^2\,\partial_t\mathbf E +
\varepsilon^2\kappa^2\,\tfrac{n_0}{|B_0|^2}\,\partial_t\mathbf E_\perp =
\nabla\times\tilde{\mathbf B} - \varepsilon\kappa^2\,\mathbf J_{gc},$$
    and the parallel force has the coefficient $1/\varepsilon$.
- Propagator option `lam` (coefficient $\lambda^2$ of the displacement
current; 0 is the quasi-neutral limit). Its defaults are
$\varepsilon\lambda_s$, $\chi = (\varepsilon\kappa_s)^2$, $c_2 =
\kappa_s^2\varepsilon$, $c_3 = 1/\varepsilon$.
- For deuterium, 1 m, 5 T, $10^{20}$ m$^{-3}$, 10 keV:
$\varepsilon^2\lambda^2 = 5\cdot10^{-6}$, $\varepsilon^2\kappa^2 =
8\cdot10^{-3}$, $\varepsilon\kappa^2 = 2.8$.
- The constructor takes `epsilon`, `lam` and `kappa` as overrides. The
default is `BaseUnits(kBT=1.0)`, also in the generated params file.
- **The parallel force $\frac1\varepsilon\mathbf b^*\cdot\mathbf E$ is
applied only by the coupling step.**
- The model gives `push_para` no `phi` and no `e_field`, and asserts
this.
  - `doc_discretization` lists which propagator applies each term.
- A test checks that `push_para` leaves $v_\parallel$ unchanged under
$\mathbf E \neq 0$, and changes it when given $\mathbf E$.
- The coupling propagator has no CUDA kernels yet (asserted).

Analysis note with the derivations (dispersion relation, well-posedness,
energy across the splitting): `notes/drift_kinetic_maxwell.md` (local,
not in the repo).

## Energy across the splitting

The coupling step conserves energy exactly. The full splitting does so
only when $\nabla|B_0| = 0$ and either $\mu = 0$ or $\tilde B_\parallel
= 0$. Two pieces are missing:
- the grad-$B$ drift current, which pairs with the $E\times B$ motion of
the positions (nonlinear);
- the magnetization current, which pairs with $\mu\nabla\tilde
B_\parallel$ in the pushers.

Both are follow-ups.

## Tests

Separate review agents went over the branch before each push. The one
real finding (discrete gradients with `e_field`) is fixed in the second
commit; the review of the normalization commit found no bugs.


- `test_drift_kinetic_maxwell_coupling.py` (1 and 4 ranks):
- energy conserved to $10^{-10}$ of the exchanged energy, for $\lambda =
1, 0$, with and without $E\times B$, assembled and matrix-free, and in a
Colella mapping;
  - assembled results equal matrix-free results;
- $\mathbb A_\parallel$ is symmetric PSD and $\mathbb S$ antisymmetric;
  - the discrete equations hold;
  - the initial projection removes the period-2 oscillation;
  - dt change; invalid options.
- `test_push_guiding_center_e_field.py`: `e_field = -grad phi`
reproduces the `phi` path exactly, for both pushers, also with the
default (discrete-gradient) `algo` through the fallback.
- `test_verif_DriftKineticMaxwell.py`: frequency of the cold-slab shear
mode against the roots of the dispersion determinant:

  | case | measured | analytic |
  |---|---|---|
  | $\lambda = 0$ | 0.3579 | 0.3616 |
  | $\lambda = 0$ with $E\times B$ | 0.2637 | 0.2643 |
  | $\lambda = 1$ | 0.3405 | 0.3428 |

The deviations are marker noise (−0.35 % with 4× more markers). Energy
over the whole splitting is conserved to $10^{-9}$.
- Generic model test, example test run, kernel catalog counts bumped.
Regression: the generic tests of `DriftKineticElectrostaticAdiabatic`,
`GuidingCenter`, `ToyDrift`, `LinearMHDDriftkineticCC` and
`test_current_coupling_5d.py` pass.

🤖 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