Skip to content

feat: native 1D Hermite-Poisson solver (_hermite_poisson_1d)#287

Merged
joglekara merged 12 commits into
mainfrom
feat/hermite-poisson-1d
Jun 15, 2026
Merged

feat: native 1D Hermite-Poisson solver (_hermite_poisson_1d)#287
joglekara merged 12 commits into
mainfrom
feat/hermite-poisson-1d

Conversation

@joglekara

Copy link
Copy Markdown
Member

Summary

  • Adds adept/_hermite_poisson_1d/ — a native 2D (Nn, Nx) Hermite-Poisson solver that avoids the 6D Spectrax tensor overhead for strictly 1D problems
  • New BaseHermitePoisson1D(ADEPTModule) base class with Lawson-RK4 integration, WaveSolver for the vector potential, PoissonSolver1D for electrostatics, and sponge boundary support
  • Eigendecomposition-based free-streaming exponential (FreeStreamingExp1D) directly on 2D (Nn, Nx) arrays instead of the full 6D spectrax machinery
  • Exposes adept.hermite_poisson_1d.BaseHermitePoisson1D as a clean public API

Motivation

The existing Spectrax-1D path runs with 6D tensors (Np=1, Nm=1, Nn, Ny=1, Nx, Nz=1) even for degenerate 1D problems. This new module eliminates the degenerate-axis overhead, which is significant at large Nx (e.g. 20k spatial cells × 64+ Hermite modes).

Test plan

  • uv run python -c "from adept.hermite_poisson_1d import BaseHermitePoisson1D" passes
  • Downstream SRS solver (kinetic_srs/hermite_poisson.py) subclasses BaseHermitePoisson1D and imports cleanly
  • Minimal forward-pass test pending (to be added in follow-up)

🤖 Generated with Claude Code

joglekara and others added 12 commits June 10, 2026 09:24
New module `adept/_hermite_poisson_1d/` implements a clean 2D (Nn, Nx)
Hermite-Fourier × Fourier-space Poisson solver, bypassing the 6D
Spectrax tensor layout (Np, Nm, Nn, Ny, Nx, Nz) that has unnecessary
overhead for strictly 1D problems.

Key components:
- vector_field.py: FreeStreamingExp1D (eigendecomposition), DiagonalExp1D
  (hypercollision + hyper-diffusion), PoissonSolver1D, TransverseWaveDriver,
  HermitePoisson1DVectorField (Lawson-RK4 + WaveSolver, Stepper convention)
- modules.py: BaseHermitePoisson1D(ADEPTModule) lifecycle with wave-CFL
  timestep, sponge support, static-ion Poisson neutralization
- storage.py: save functions for fields {e, a, da}, hermite coefficients,
  and scalar diagnostics

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… tracer errors

float(self.x_a[-1]) inside __call__ fails under JAX tracing because x_a
is a JAX array whose elements become abstract tracers. Move all static
scalar extraction to __init__.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… config

DiagonalExp1D now accepts hou_li_strength and hou_li_col_1d; applies
exp(-strength * (n/Nn)^order * s) per Lawson substep in addition to the
existing hypercollisional nu term.

BaseHermitePoisson1D.init_diffeqsolve reads drivers.hermite_filter
{enabled, strength, order} and passes the computed profile to DiagonalExp1D.
Previously the hermite_filter config key was silently ignored.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…avenumber scale

Two physics-breaking bugs, both now locked by tests:

1. _hermite_e_coupling read C[n+1] where the AW-Hermite force term
   (Parker & Dellar 2015 eq. 3.11) requires C[n-1]. With the inversion, a
   uniform E field applied to a Maxwellian drove zero current — the
   momentum equation was structurally absent, Landau resonance did not
   exist, and E never did work on the plasma. The error was transcribed
   from spectrax1d shift_multi's then-incorrect docstring (fixed
   separately) rather than from its behavior.

2. FreeStreamingExp1D divided the wavenumber by Lx a second time
   (modules.py already passes physical kx = 2*pi*fftfreq*Nx/Lx), making
   free-streaming Lx-times too slow.

Together these explain the spurious field-free low-n instability that
killed every gradient-SRS production run at 1.2-1.4 ps regardless of
filter shape, x-resolution, or integrator.

Also adds an optional density.perturbation config (single-mode cosine on
the electron density) so dispersion tests can run the production path.

New tests (tests/test_hermite_poisson_1d/):
- uniform-E-on-Maxwellian current drive, one-sided ladder structure,
  density-never-forced, and exact match against the validated spectrax1d
  Lorentz term
- full-path EPW dispersion: frequency and Landau damping vs kinetic
  theory at klambda_D = 0.30, 0.35 — both now agree to <=0.2%

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… term with tests; cover Dopri8 path

The shift_multi code has always been correct (dn=-1 -> source at n-1),
but its docstring and an inline comment stated the OPPOSITE mapping.
Code written against that wording instead of the behavior shipped an
inverted E.dv f coupling in _hermite_poisson_1d (fixed in the previous
commit). The docs now state the verified semantics and warn against
re-implementing from the description.

New tests (test_shift_and_lorentz.py) pin behavior so docs and code
cannot silently diverge again:
- shift_multi direction semantics with distinguishable values
- uniform-E-on-Maxwellian responds only at n=1 with (q/m)*sqrt(2)/alpha*E*C0
- the force ladder is one-sided upward (C2 -> C3 only)

test_landau_damping.py: the docstring advertised explicit (Dopri8) legs
that were never parametrized — the explicit integrator path went
untested for its entire history. Added a Dopri8 static-ions leg
(passes: 1% frequency, 8% damping error) and corrected the docstring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Blown-up runs produce inf in the saved scalars/fields; matplotlib's log
tickers raise OverflowError on inf, which aborted post_process before the
netCDF/metric upload — losing diagnostics for exactly the runs that need
them most. Sanitize non-finite values to nan (masked by matplotlib) and
wrap each figure in try/except so plotting can never block the upload.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The E/ponderomotive coupling multiplies two fields in real space; without
truncating both factors to |k| <= Nx//3 and masking the result,
beyond-Nyquist products alias back into the resolved band. The spectrax1d
base applies its mask23 to every Lorentz product and the vlasov1d
reference runs a grid-scale Hou-Li filter in x for the same reason; the
HP transcription had dropped both.

Suspected trigger for the trapping-onset blowups in gradient-SRS runs:
n-space filter escalation (o8s36 -> o4s64 -> o4s256 -> o3s8) only delayed
death while the death amplitude stayed pinned at the trapping threshold
(Ex ~ 1.5e-3), i.e. the detonation tracks the appearance of spatial
harmonics, not ladder truncation.

EPW dispersion tests unchanged to 4 digits (single-k linear physics is
alias-free); all HP tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The default hypercollision profile (cubic, normalized to the truncation
edge) provides no dissipation in the Landau-resonance band — the
collisionless truncated AW-Hermite hierarchy then detonates at trapping
onset (amplitude-triggered; insensitive to filter shape/strength, Nx,
2/3 dealiasing, and a 4x dt range; see kinetic-srs gradient-SRS campaign
notes). LB is the principled regularization: Hermite functions are exact
LB eigenfunctions, so col[n] = n gives gamma_n = nu*n — smooth velocity
diffusion at all kinetic scales including the resonance/vortex region,
the Hermite analog of the grid-scale velocity dissipation the vlasov1d
reference gets from its spline advection.

Damping is gated off n = 0, 1, 2 (standard conserving truncation, cf.
Parker & Dellar 2015) so collisions conserve density, momentum, and
energy exactly and impose no drag on the EPW's fluid content.

Config: physics.collision_model = "hypercollision" (default, unchanged)
| "lenard-bernstein". Unit tests verify exact exp(-nu*n*t) decay for
n >= 3 and exact conservation of the first three moments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The vlasov1d reference seeds SRS with continuous white-in-time Gaussian
force noise on Ex (kinetic_srs StochasticVlasovMaxwell): drawn fresh each
step via fold_in(seed, round(t/dt)), density-weighted, injected into the
v-advection force only (never Poisson or the wave solver). The HP module
only had a one-shot initial density perturbation, which velocity-space
dissipation (LB collisions) can starve before SRS amplifies it.

Same schema (stochastic_noise: {enabled, amplitude, seed}), same
determinism, same density weighting from the initial electron state.
Lawson-specific detail: the realization is drawn once at the step head
and frozen across the four RK4 stages (like a_frozen) — per-stage draws
would round t/dt inconsistently mid-step.

Tests: draw determinism + amplitude scaling, stage-freezing, and a
drives-kinetic-response/no-spurious-source pair.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…file=knee)

Scan 17 produced the first genuine seeded SRS (gamma~3.7e-3, intensity-
ordered) but exposed a scale-separation problem: the LB nu that regularizes
the n~40-300 filamentation cascade also damps the resonant EPW mode
(n~vphi^2/2~24) at nu*24, pushing the SRS threshold above 1.442e14 (vlasov
has 17% there). The (n/Nn)^order Hou-Li profile can't separate them — at
Nn=1024 the resonance and cascade sit at n/Nn=0.023 vs 0.05.

Add profile="knee": col(n) = 0.5*(1+tanh((n-n_knee)/width)), an absolute-n
step ~0 below n_knee and ~1 above. Paired with a low LB nu it preserves the
resonance while strongly damping the cascade. Config: drivers.hermite_filter
{profile: knee, strength, n_knee, width}; default profile "houli" unchanged.

Tests: profile shape (resonance<0.02, cascade>0.95, monotonic), houli default
intact, and a dynamic k=0 run showing n=24 preserved (>0.95) while n=80
decays (<0.2) over 20 wp^-1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a LongitudinalElectricFieldDriver that injects a prescribed Ex into
the velocity-space force (alongside the Poisson field, ponderomotive
force, and stochastic noise), matching adept._vlasov1d's driver:

    E_drive(x,t) = Σ env(x,t) (w0+dw0) a0 sin(k0 x − (w0+dw0) t)

Reads the flat cfg["drivers"]["ex"] pulse dict (same format as the ey
driver), evaluated at each Lawson-RK4 substep so its time variation is
captured within a step. Exposed as the "de" diagnostic field in the
state and the "fields" save.

Also add α to ruff allowed-confusables (physics notation, like the
existing γ) and drop a useless static_ions ternary surfaced by RUF034.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Formatting-only (line-wrapping); these files were committed unformatted
and were failing the Code linting CI job. No logic changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@joglekara joglekara merged commit 7a15f69 into main Jun 15, 2026
7 of 8 checks passed
@joglekara joglekara deleted the feat/hermite-poisson-1d branch June 15, 2026 04:32
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