Skip to content

feat(lpse2d): 2-D hybrid particle evolution, exact-rotation light coupling, TPD pump depletion - #368

Draft
physicistphil wants to merge 2 commits into
ergodicio:mainfrom
physicistphil:lpse2d/srs
Draft

feat(lpse2d): 2-D hybrid particle evolution, exact-rotation light coupling, TPD pump depletion#368
physicistphil wants to merge 2 commits into
ergodicio:mainfrom
physicistphil:lpse2d/srs

Conversation

@physicistphil

@physicistphil physicistphil commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Two commits on lpse2d/srs since #320.

1. 2-D hybrid particle evolution (e7b0903)

Lifts the ny == 1 restriction on terms.hpe. The Follett-2017 test-particle Landau-damping feedback runs in a genuinely 2-D box; quasi-1D is the n_angles == 1 special case of the same code path.

  • The resonance surface ω = k·v is a line in (vx, vy); its integral equals the 1-D formula applied to the projection of f onto each mode's direction. Particles are binned along n_angles directions in [0, π) and each mode interpolates between its two bracketing directions.
  • Gather indices are static and all angles are binned in one fused scatter, so the per-step cost is flat in n_angles; no particle-count increase over 1-D is needed.
  • Tail cut on speed in 2-D (projection matches the 1-D truncated Maxwellian above the cutoff); thermalizing x wall gets a cosine-weighted emission direction and tabulated speed law; y_thermal_frac thermalizes a fraction of transverse crossings.
  • New terms.hpe.hist_smooth (binomial filter on the histogram before the slope; default 2 in 2-D, 0 in quasi-1D). Motivation: the γ ≥ 0 clamp pins shot-noise-steep modes to exactly zero, and a zero-damped mode grows relative to the band. Fraction of band modes clamped at hist_smooth 0/1/2/4: 0.061/0.035/0.019/0.004 at 20k particles.
  • Latent 1-D fixes: fhot/hpe_mean_energy used a per-component u instead of |u|; HPE smoke tests now honour the documented NaN sentinel of hpe_gamma_ratio_kpeak and take a median over the tail of the run.
  • Tests: tests/test_lpse2d/test_hpe_2d.py (11 tests: isotropic tail matches the analytic Landau rate with no angular bias; the ky = 0 column of a 2-D box reproduces quasi-1D) plus the updated test_hpe.py. Design record in docs/dev/lpse2d-hpe-plan.md §7; config configs/envelope-2d/srs-hpe-2d.yaml.

2. Exact-rotation light coupling, TPD pump depletion, light filter (abe1b9e)

Three opt-in terms.light options for the coupled (pump-depletion) light solver, all default off:

  • coupling: rotation. The MATLAB staggered real/imaginary light update is a leapfrog only for operators of the form i × (real operator). The E0↔E1 coupling through the complex, spatially rotating ∇²φ is not one, so on that term the scheme is forward Euler on a rotation: per-sub-step amplification 1 + sin²(arg ∇²φ)(Ω dt_l)²/2 with Ω = e|∇²φ|/(4mₑ√(ω₀ω₁)), and with propagation in the loop a growth of about 0.04 Ω per unit time independent of dt_l. With ∇²φ frozen over the sub-steps the coupling matrix satisfies M² = −Ω²I, so exp(τM) = cos(Ωτ)I + sin(Ωτ)/Ω·M is applied exactly, Strang-split around the existing propagation update, and conserves ω₁|E0|² + ω₀|E1|² pointwise.
  • tpd_depletion: true. Adds i e/(4ωₚ₀mₑ)·e^{i(ω₀−2ωₚ₀)t}·ρE_y to dE0_y/dt, the energy-conserving partner of the TPD potential source (the MATLAB original depletes the pump through SRS only, so the EPW energy could exceed the absorbed laser energy).
  • filter: <fraction of π/dx>. Optional isotropic low-pass on E0, E1 once per EPW step (diagnostic).

RamanLight.rhs gains a couple flag so the E1 RHS can drop the exchange term when the rotation handles it. Tests in tests/test_lpse2d/test_light_coupling.py: explicit-scheme growth for a complex Laplacian, rotation action conservation to 1e-12, small-angle agreement with the explicit scheme, dW_pump = −dW_epw for the TPD pair.

Evidence from controlled runs (srs-campaign sims/lpse/srs-2d-testbed/NOTES.md, "Blowup investigation"; MLflow lpse_2d_testbed / lpse_2d_testbed_blowup): the first 2-D SRS+TPD testbed runs went non-finite at 4–5 ps with the explicit scheme (halving dt_l only moved the NaN from 4.50 to 4.85 ps); coupling: rotation runs stay finite to 20 ps; with TPD on but no depletion the EPW held 4× the absorbed laser energy at 4 ps, and tpd_depletion holds that ratio at ≤ 0.53.

Behaviour of existing configs

Unchanged: quasi-1D HPE runs use hist_smooth: 0 and the n_angles == 1 path; the three light options default off.

Test status

  • tests/test_lpse2d/test_light_coupling.py: 4 passed (CPU, laptop).
  • Full tests/test_lpse2d suite (pytest tests/test_lpse2d -x, CPU jax 0.9.0.1, laptop): 60 passed, 1 skipped, 4 deselected in 5 min 35 s.

Not in this PR

  • The 2-D testbed still shows an unexplained late light-budget effect (reflected flux exceeding the incident from ~4.6 ps, present with TPD off and unchanged by any of the options above). Campaign-side investigation.
  • Whether coupling: rotation and tpd_depletion should become defaults. The rotation scheme still has narrow resonant windows above Ω·dt_l ≈ 0.009 in the periodic model problem; a unitary (spectral or Crank–Nicolson) light propagator would remove them.

🤖 Generated with Claude Code

physicistphil and others added 2 commits September 4, 2026 16:53
Lift the `ny == 1` restriction on `terms.hpe`. The Follett-2017 test-particle
Landau-damping feedback now runs in a genuinely 2-D box, with quasi-1D falling
out as the `n_angles == 1` special case of the same code path.

The physics: Eq. 4 integrates d<F>/dv over the resonance surface omega = k.v,
which is a point in 1-D but a line in (vx, vy). That line integral is exactly
the 1-D formula applied to the projection of f onto the mode's own direction,

    P_khat(v) = int d^2v' f(v') delta(v'.khat - v)
    gamma_L(k) = -(pi/2) (wp0^3/|k|^2) dP_khat/dv |_{omega_res/|k|}

so the extraction bins particles along `n_angles` directions spanning [0, pi)
and interpolates between the two bracketing directions per mode. Opposite
directions are mirror images, which is precisely what the existing sgn(kx)
encoded -- the 1-D sign convention was the d=1 shadow of this bookkeeping.

Implementation notes:
- angle/velocity gather indices are static (v_phi comes from the k grid), so the
  per-step extraction is 4 gathers per mode from a flat (n_angles+1, nv) table,
  with no (n_modes, nv) intermediate
- all angles are binned in one fused scatter, which keeps the cost flat in
  n_angles rather than linear
- the tail cut is on the speed in 2-D; the projection of that speed-truncated
  Maxwellian equals the true 1-D Maxwellian exactly above the cutoff, i.e.
  everywhere the resonance reads it. tail_fraction() is now dimension-aware and
  shared with the fhot diagnostics.
- thermalizing x wall gains a cosine-weighted emission direction and a tabulated
  s^2 exp(...) speed law in 2-D; the 1-D closed form is untouched
- `y_thermal_frac` thermalizes a fraction of transverse *crossings*, counted in
  the substep loop where the wrap happens
- state is (Np, ndim); epw_hist is (n_angles, nv) and is saved/plotted per angle

Also fixes, all latent while ndim == 1:
- fhot/hpe_mean_energy used a per-component u instead of |u|
- the smoke tests asserted every series value finite, contradicting the
  documented NaN sentinel that hpe_gamma_ratio_kpeak emits before the EPW has
  band energy (this failed on lpse2d/srs before this branch)
- the same tests asserted a single-mode damping ratio in one sample; kpeak is
  shot-noise-limited per mode (frac < 0.5 is 0.039 at 20k particles, 0.016 at
  200k) and the EPW noise seed is redrawn per run, so they now take a median
  over the tail of the run

Found while validating the 2-D smoke run, and mitigated: the rate is read from
df/dv at one velocity bin per mode, so with finite particles some modes draw a
slope steep enough that the gamma >= 0 clamp sends them to exactly zero -- and a
mode pinned at zero is *undamped*, so it grows relative to the band instead of
the error averaging out. The clamp selects for its own errors. 2-D meets this far
more often (6644 band modes vs 668 in the smoke configs). New
`terms.hpe.hist_smooth` (binomial filter over the histogram before the slope,
default 2 in 2-D and 0 in quasi-1D so existing 1-D results stay reproducible) is
the mitigation, and is bias-free because the per-k calibration is derived through
the same operator. Fraction of band modes clamped to zero at hist_smooth 0/1/2/4:
0.061/0.035/0.019/0.004 at 20k particles, 0.0075/0.0021/0/0 at 200k, with the
band-mean ratio staying at 1.02-1.06.

Validation: tests/test_lpse2d/test_hpe_2d.py (11 tests). The load-bearing ones are
that an isotropic tail gives a rate matching the analytic Landau rate with no
angular bias, and that the ky = 0 column of a 2-D box reproduces the quasi-1D
result -- i.e. this is a strict generalization. Also covered: the angle-
interpolated dP/dv against a directly binned line integral for off-axis modes,
2-D loading/free-streaming/transverse wrap, the Ey gather branch, transverse
thermalization, and the absorbing-x wall in both geometries.

Cost (CPU, 400x120, Np = 5e5): the 2-D add-on is ~8x the 1-D add-on and flat in
n_angles (386/357/402 ms at 16/32/64). Scaled by the measured 3090 1-D add-on
that is roughly 2-4x a fluid run. n_angles is an accuracy knob, not the cost
knob, and 2-D needs no particle-count increase over 1-D.

Design record and measurements in docs/dev/lpse2d-hpe-plan.md Sec. 7; reference
docs in docs/source/solvers/lpse2d/config.md; production config in
configs/envelope-2d/srs-hpe-2d.yaml.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t filter

Three opt-in terms.light options for the coupled (pump-depletion) light solver,
all default off so existing configs are unchanged:

- coupling: rotation -- Strang-split each light sub-step as [exact E0<->E1
  coupling over dt_l/2] [staggered propagation with the coupling off] [exact
  coupling over dt_l/2]. With laplacian phi frozen the coupling matrix satisfies
  M^2 = -Omega^2 I, so exp(tau M) = cos(Omega tau) I + sin(Omega tau)/Omega M is
  applied exactly and conserves w1|E0|^2 + w0|E1|^2 pointwise. The MATLAB
  staggered real/imaginary update is a leapfrog only for i x (real operator);
  on the complex, spatially rotating laplacian phi it is forward Euler on a
  rotation and grows at ~0.04 Omega per unit time independent of dt_l, which
  is what drove the 2D SRS+TPD testbed runs non-finite once delta n/n ~ 0.3.
- tpd_depletion: true -- add i e/(4 wp0 me) exp(i (w0 - 2 wp0) t) rho E_y to
  dE0_y/dt, the energy-conserving partner of the TPD potential source (the
  MATLAB original depletes the pump through SRS only, so the EPW energy could
  exceed the absorbed laser energy).
- filter: <fraction of pi/dx> -- optional isotropic low-pass on E0, E1 once
  per EPW step (diagnostic).

RamanLight.rhs gains a couple flag so the E1 RHS can drop the exchange term
when the rotation handles it. Tests: explicit-scheme growth for a complex
laplacian, rotation action conservation to 1e-12, small-angle agreement with
the explicit scheme, and dW_pump == -dW_epw for the TPD pair.

Evidence: srs-campaign sims/lpse/srs-2d-testbed/NOTES.md, "Blowup
investigation" (MLflow experiments lpse_2d_testbed / lpse_2d_testbed_blowup).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ergodic-ludwig

ergodic-ludwig Bot commented Sep 7, 2026

Copy link
Copy Markdown

Comparison with the 2D HPE implementation currently on main

I compared the two implementations directly:

  • current main (9f6edb9), introduced through PR#363 (b7787b8 plus the wall-law correction in c48e2ed)
  • this PR's HPE commit e7b0903

Recommendation

Use this PR's signed half-plane formulation as the mathematical foundation, but keep current main as the canonical implementation until the formulation is ported into main's safer infrastructure. Do not replace main's HPE with e7b0903 unchanged.

The key mathematical improvement here is recognizing that projected distributions are associated with axes rather than independent oriented vectors:

P_{-n}(v) = P_n(-v)

Consequently, only projection axes in [0, pi) are independent. Folding each wavevector onto that half-plane and retaining a sign,

k_hat = s n_hat(theta),  s = +/-1
v_phi = s omega_res / |k|
gamma_L proportional to -s P'_theta(v_phi),

is continuum-equivalent to main's [0, 2pi) implementation, but removes redundant opposite-direction histograms and makes the original 1D sgn(kx) formula the exact one-dimensional specialization. This is the cleaner fundamental HPE abstraction.

Merge-blocking issue: periodic field gather

This PR wraps particle positions periodically in y, but _accel always clips its interpolation indices:

j0 = jnp.clip(jnp.floor(idy).astype(jnp.int32), 0, self.ny_f - 2)

It therefore never interpolates periodically between the last and first field cells at the transverse seam. A particle crossing the periodic boundary can see an artificial force discontinuity. Current main's _linear_indices(..., periodic=True) uses modular i0/i1 indices and handles this correctly.

Please restore modular interpolation independently on every periodic axis and add a periodic-seam force-continuity test. The existing transverse-force test samples the interior and does not cover the seam.

Detailed comparison

Area Current main / PR#363 PR#368 / e7b0903
Angular representation Oriented directions over [0,2pi) Independent axes over [0,pi) plus sign/mirroring
1D/2D structure Separate pusher, boundary, and damping branches Unified (Np, ndim) state and shared operator
Histogram lax.map of one histogram per direction Matrix projection plus one fused scatter
Damping interpolation Angle/velocity indices formed during extraction Static four-point angle/velocity gather indices
Smoothing None hist_smooth, default 2 passes in 2D
Initial positions Full flattened background_density[ix,iy] background_density[:,0] for x; uniform y
Transverse boundaries Periodic or thermalizing y, including corner handling Periodic y only, optionally rethermalizing a fraction of crossings
Tail cap Properly truncated Rayleigh draw below 0.99c Unbounded draw followed by minimum(..., 0.99c)
Quasi-1D raw state Historical (Np,), (nv,) shapes (Np,1), (1,nv) internally
Default angular resolution 32 directions over 2pi = 16 independent axes 64 independent axes over pi

What should be ported from e7b0903

  • [0, pi) projection axes
  • signed resonance and mirrored angular closure
  • dimension-generic resonance/damping operator
  • precomputed angle/velocity gather indices
  • fused histogram implementation, after GPU peak-memory measurement
  • exact-bin expected projected Maxwellian
  • y_thermal_frac
  • axial-reduction, direct-projection, smoothing, and transverse-crossing tests

What should be retained from current main

  • correct modular periodic field gathering
  • full 2D density-weighted position loading
  • cap-safe tail sampling and associated input checks
  • independent x/y boundary handling and corner behavior
  • existing state/artifact compatibility
  • current TPD/SRS/IAW integration
  • the synthetic directional test that flattens one projection and verifies that only aligned modes are undamped

Histogram performance and scaling

The fused histogram is a substantial implementation improvement. In an illustrative local CPU benchmark with 100,000 particles, I measured:

PR#368 fused histogram
  8 axes:   3.9 ms
 16 axes:   8.6 ms
 32 axes:  26.5 ms
 64 axes:  69.0 ms

current main, full-circle lax.map
 16 directions: 279 ms
 32 directions: 497 ms
 64 directions: 798 ms

These are not production GPU measurements, but they show that the fused implementation is much faster on this backend.

The description that its cost is "flat in n_angles" should nevertheless be qualified. It forms and processes (Np, n_angles) projected samples and indices, so histogram work and temporary storage still scale with Np * n_angles. The total HPE step can look approximately flat because hundreds of particle substeps dominate. At Np=500,000, n_angles=64, the projected-value and index temporaries may be material on GPU; peak memory and runtime should be measured there, with chunking considered if necessary.

Also, the meaning of n_angles changes. Main's default 32 directions over 2pi represents only 16 independent axes, whereas this PR's default 64 over pi represents 64 independent axes—four times the unique angular resolution. A directly comparable setting would be about 16 axes here. The default should be chosen by nonlinear angular convergence rather than silently inherited.

Histogram smoothing

The clamp-selection problem identified here is real: noisy positive slopes are clamped to zero damping, and accidentally undamped modes preferentially grow. hist_smooth is a useful mitigation.

However, per-k calibration makes smoothing unbiased only for the calibration distribution:

smooth_operator[f_Maxwell] -> analytic gamma_L

It does not make the smoothed operator identical to the unsmoothed operator for an evolved plateau or sharp tail feature. Smoothing changes nonlinear velocity resolution. Please document it as estimator regularization and require convergence over hist_smooth, nv, n_angles, and particle count. Logging a non-applied unsmoothed diagnostic alongside the smoothed applied rate would also be useful.

The statement that no particle-count increase over 1D is needed should also be softened. Every projection receives all particles, so particles are not divided among angles, but the many-mode extreme-value problem remains. The 2D smoke test itself raises n_particles from 20,000 to 200,000 because thousands of resonant modes otherwise produce clamp-selected zero-damping outliers.

Validation performed

I ran the non-slow HPE tests on both versions:

current main: 10 passed, 3 deselected
PR#368:      17 passed, 4 deselected

The sign/mirror bookkeeping also passed a synthetic anisotropic check: flattening the x projection undamped both +x and -x modes while leaving +/-y modes at the analytic rate.

The new test suite is stronger in several areas, especially axial reduction and direct off-axis projection. It should be combined with main's directional-flattening and full boundary tests. A quantitative nonlinear oblique-wave trapping test is still needed; the isotropic Maxwellian tests primarily validate geometry and the calibrated linear limit.

Proposed resolution

Use current main as the integration base and port the mathematical/operator improvements from e7b0903, rather than merging this branch wholesale. This PR is currently merge-conflicted with main and also bundles a separate light-coupling/TPD-depletion commit that overlaps work already merged through PR#363; the HPE and light reconciliations should be split.

Bottom line: PR#368 has the better fundamental mathematics; current main has the safer implementation. The target should be PR#368's half-plane/unified resonance operator inside main's density loading, periodic gather, boundary, compatibility, and integration framework.

physicistphil added a commit that referenced this pull request Sep 8, 2026
…lter

Two opt-in terms.light options for the coupled (pump-depletion) light solver,
both default off so existing configs are unchanged:

- coupling: rotation -- Strang-split each light sub-step as [exact E0<->E1
  SRS exchange over dt_l/2] [staggered propagation with the exchange off]
  [exact exchange over dt_l/2]. With laplacian phi frozen the exchange matrix
  satisfies M^2 = -Omega^2 I, so exp(tau M) = cos(Omega tau) I +
  sin(Omega tau)/Omega M is applied exactly and conserves w1|E0|^2 + w0|E1|^2
  pointwise. The MATLAB staggered real/imaginary update is a leapfrog only for
  i x (real operator); on the complex, spatially rotating laplacian phi it is
  forward Euler on a rotation and grows at ~0.04 Omega per unit time
  independent of dt_l (Omega = e|laplacian phi|/(4 me sqrt(w0 w1))), which is
  what drove 2D SRS+TPD pump-depletion runs non-finite once delta n/n ~ 0.3.
  The TPD pump term and the IAW detuning stay in the staggered RHS under both
  settings; with SRS off there is no exchange and the settings coincide.
- filter: <fraction of pi/dx> -- optional isotropic low-pass on E0, E1 once
  per EPW step (diagnostic).

RamanLight.rhs and CoupledLight.pump_rhs gain a `couple` flag so the RHS can
drop the exchange when the rotation handles it. terms.light is now validated
through LightModel in get_solver_quantities (as terms.iaw/hpe already are),
and the two options are refused without pump_depletion.

Tests (tests/test_lpse2d/test_light_coupling.py): explicit-scheme growth
1 + sin^2(arg L)(Omega dt_l)^2/2 for a complex laplacian, rotation action
conservation to 1e-12, small-angle agreement with the explicit kick, rotation
== explicit with phi = 0 over a full EPW step, action conservation of the
rotation halves over a full sub-step loop, E1 stays zero with SRS off, filter
mask semantics, and config validation.

Evidence: srs-campaign sims/lpse/srs-2d-testbed/NOTES.md, "Blowup
investigation" (MLflow experiments lpse_2d_testbed / lpse_2d_testbed_blowup).
Light-only successor to #368.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2qwF9isiJGwvF5y6mbeGC
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