Skip to content

Total magnetic field in B* = B0 + B~ + eps v_par curl b0 (PushGuidingCenterParallel, DriftKineticMaxwellCoupling) - #828

Merged
spossann merged 33 commits into
develfrom
dk-maxwell-full-bstar
Oct 28, 2026
Merged

spossann merged 33 commits into
develfrom
dk-maxwell-full-bstar

Conversation

@spossann

@spossann spossann commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

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

Follow-up of #823 (DriftKineticMaxwell, merged).

What

Use the total magnetic field in the first term of $\mathbf B^*$:

$$\mathbf B^* = \mathbf B_0 + \tilde{\mathbf B} + \varepsilon v_\parallel \nabla\times\mathbf b_0, \qquad B^*_\parallel = \mathbf b_0\cdot\mathbf B^*. $$

The curvature term keeps the equilibrium $\mathbf b_0$. Guiding centers now stream along the perturbed field lines (the magnetic flutter term $v_\parallel\tilde{\mathbf B}_\perp/B^{*}_\parallel$).

  • PushGuidingCenterParallel: with b_tilde, $\hat{\mathbf B}^{*2}$ is built from $\mathbf B_0 + \tilde{\mathbf B}$ (tensor-product 2-form, refreshed together with $B_\parallel$ and $\nabla B_\parallel$). Before, the kernels got the equilibrium b2 only, although the docstring already said $\mathbf B = \mathbf B_0 + \tilde{\mathbf B}$, and CurrentCoupling5DCurlb already used the total field.
    • This changes the results of LinearMHDDriftkineticCC (it passes b_tilde). Models without b_tilde (GuidingCenter, DriftKineticElectrostaticAdiabatic) are unchanged.
  • DriftKineticMaxwellCoupling: the parallel response $\mathbf a_p = \hat{\mathbf B}^{*2}/\hat B^{*3}_\parallel$ uses the same total field at $\mathbf b^n$. So the current, the parallel force and the streaming of push_para use the same $\mathbf b^*$.
    • The step remains exactly energy conserving, since $\mathbf a_p$ is frozen in the step.
    • With $\mathbf B^* = \nabla\times\mathbf A^*$, phase-space conservation is now fully consistent.
    • The kernels are unchanged (they take the 2-form as an argument).
  • Docs: b_tilde is documented as Hdiv (it was "Hcurl", but all callers pass the 2-form); the DriftKineticMaxwell docs show the new $\mathbf B^*$.

Tests

test_push_guiding_center_full_b.py (1 and 4 ranks), with $\mathbf B_0 = \mathbf e_z$ and the uniform $\tilde{\mathbf B} = 0.1,\mathbf e_x$:

  • push_para moves every marker in $x$ by exactly $0.1\times$ its displacement in $z$ (discrete-gradient and explicit algorithms). Before the change: no $x$ motion.
  • the stored $\mathbf a_p$ of the coupling step equals $(0.1/L_x, 0, 1/L_z)$.

The existing tests of #823, the verification test (inertial Alfvén wave), and the generic model tests of DriftKineticMaxwell, LinearMHDDriftkineticCC, DriftKineticElectrostaticAdiabatic and GuidingCenter pass.

Review

A separate review agent found no correctness bugs. It checked:

  • that all pusher algorithms get the total field, that the buffer is filled before use, and the ghost regions and extraction (also with polar splines);
  • that the coupling holds $\mathbf b^n$ during the push, and that exact energy conservation is kept;
  • the test expectations and their MPI safety.

Its one nit is fixed: the tests drop previously registered kernels before re-allocating a pusher.

Open points it raised, outside this PR:

  • CurrentCoupling5DCurlb adds b_tilde to the tensor-product b2 without the extraction operator E2T, which looks wrong with polar splines.
  • With the flutter term, the energy balance of LinearMHDDriftkineticCC should be checked: is the field-side counterpart of the $f v_\parallel \tilde{\mathbf B}$ current present?

🤖 Generated with Claude Code

spossann and others added 30 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>
… 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>
…nergy-conserving drift-kinetic Maxwell coupling with quasi-neutral limit

- New propagator DriftKineticMaxwellCoupling: Maxwell's equations, the parallel acceleration of the guiding
  centers and (option exb_current) the E x B current in one Crank-Nicolson step at frozen positions; Schur system
  for e_mid with M_E = kappa M1 + chi M1gyro (linearized polarization current, chi = alpha^2 by default), parallel
  response A_par (rank one per marker) and antisymmetric E x B operator S. Exact energy conservation, well-posed
  for kappa = 0 (requires kappa + chi > 0). Assembled (dk_maxwell) and matrix-free (dk_maxwell_mf with
  marker_operator_rank1_dot / marker_operator_asym_dot) particle matrices, push_vpar_with_efield kernel,
  StiffnessPreconditioner, initial projection for kappa = 0.
- PushGuidingCenterBxEstar / PushGuidingCenterParallel accept either phi (unchanged) or a new e_field (Hcurl):
  E* = E - eps mu grad|B| (gauge-invariant formulation).
- New model DriftKineticMaxwell: coupling -> push_para -> push_bxe(e_field).
- Tests: energy conservation, assembled = matrix-free, operator symmetries, discrete equations, initial projection,
  options; e_field = -grad phi reproduces the phi path; verification against the cold-slab dispersion relation
  (inertial Alfven wave, with and without E x B current, kappa = 0 and 1).
- Example inertial_alfven_wave.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t algorithm; no CuPy; docs

- With e_field (no potential), the discrete-gradient correction removes the work of E along the step (O(1)
  inconsistency, e.g. no E x B drift across grad|B|): a discrete-gradient algo now falls back to "explicit" with a
  warning (keeps the default options working); the phi path is unchanged. Test with the default algo added.
- DriftKineticMaxwellCoupling asserts a non-CuPy backend (no CUDA kernels yet); document that chi uses the
  MHD-equilibrium density and the missing f0 response with the control variate.
- Example: explicit E* x b0 push.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
…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>
# Conflicts:
#	doc/sections/subsections/propagators-avail.rst
…idingCenterParallel, DriftKineticMaxwellCoupling)

- PushGuidingCenterParallel with b_tilde: the kernels get the 2-form of B0 + b_tilde (tensor-product coefficients,
  refreshed with B_parallel and grad B_parallel) instead of the equilibrium b2 only, as its docstring already stated
  and as CurrentCoupling5DCurlb does: streaming along the perturbed field lines (flutter). Changes the results of
  LinearMHDDriftkineticCC; models without b_tilde are unchanged.
- DriftKineticMaxwellCoupling: a_p = B*2 / B*3_par with the same total field at b^n (_update_b_star), consistent
  with push_para; the kernels are unchanged.
- Docs: b_tilde is in Hdiv (was documented as Hcurl); DriftKineticMaxwell doc_pde with the new B*.
- Tests: with B~ = 0.1 e_x on B0 = e_z, push_para moves the markers by 0.1 dz in x, and the coupling stores a_p
  along the total field (both fail on the previous code).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from drift-kinetic-maxwell to devel October 23, 2026 16:50
spossann and others added 2 commits October 28, 2026 08:44
# Conflicts:
#	src/struphy/models/drift_kinetic_maxwell.py
#	src/struphy/propagators/drift_kinetic_maxwell_coupling.py
…ting a pusher in the tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@spossann
spossann marked this pull request as ready for review October 28, 2026 11:52
@spossann
spossann merged commit 26233de into devel Oct 28, 2026
28 checks passed
@spossann
spossann deleted the dk-maxwell-full-bstar branch October 28, 2026 12:04
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