feat: LPSE with SRS (and 1d support) - #320
Conversation
- New raman.py light-wave module and SRS coupling terms in epw.py - Raman seed/light-wave config in datamodel and helpers - SRS example config (configs/envelope-2d/srs.yaml) and test - Document SRS options in lpse2d config docs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The field-save interpolators assumed >=2 transverse cells: interpax.interp2d returns NaN off a single y-node, and RegularGridInterpolator fills 0 for a single-node y axis. This blanked all real-space/k-space field artifacts (fields.xr, k-fields.xr, plots/<field>/*) for ny=1 runs while the series.xr scalars stayed valid. Add an ny==1 path that interpolates in x only and keeps the single transverse row, for both the in-solve field saver and the background-density save. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- New adept/_lpse2d/diagnostics.py: laser-budget window means (names and
definitions match osiris_lpi/laser_budget.py), EPW growth fit copied
verbatim from osiris_lpi/epw_growth.py (per-w0 rates), electron energy
as cumulative EPW dissipation.
- Default save now also logs epw_energy (OSIRIS units: fields in me*c*w0/e,
lengths in c/w0), epw_dissipation (using the solver's own Landau +
collisional rates via the new module-level landau_damping_rate),
epw_boundary_loss, and discrete two-point flux probes for the laser
budget (incident/transmitted/reflected/backrefl, normalized to I0).
Probes sit at 2*boundary_width, clear of the absorber skirt; the legacy
reflectivity probe at 1.6*bw is unchanged for back-compat.
- post_process logs these scalars as MLflow metrics (previously only
write/plot times) and adds laser-budget / EPW-fit / electron-energy plots.
- terms.epw.source.{noise_amplitude,noise_seed} are config-driven; the
resolved seed is pinned into the cfg pre-log_params so runs are exactly
reproducible. Removed the dead density.noise draws that perturbed the
global RNG stream.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
terms.light.pump_depletion: true evolves the pump with the same staggered explicit FD envelope scheme as the Raman light (new core/light.py, CoupledLight), ported from the isPumpDepletion path of lpse-matlab m201805_matlabLpse_v11.m: - pump RHS: diffraction + local detuning + depletion coupling -i e/(4 w1 me) (laplacian phi) E1 (conjugate-free, partner-frequency denominator; Manley-Rowe-consistent with the E1 and EPW couplings) - two-point boundary injector at xmin + drivers.E0.offset (default 2*bw), multi-color, MATLAB amplitude calibration - both waves advance inside one staggered real/imag update (advancing them independently would break the discrete conservation) - substep limit = min over both carriers; E0 high-k filter in the EPW SRS source on the dynamic-pump path (MATLAB skips it on the static path) - budget flux probes convert the exact discrete two-point flux to physical flux via the FD group-velocity factor sin(k_grid dx)/(k dx); metrics normalize to the measured incident flux (the injector launches sin(k0 dx)/sin(k_grid dx) ~ 0.98 of nominal amplitude at 8 cells/lambda) - default off; the prescribed-pump path is untouched (verified: the three pre-existing SRS tests pass unchanged) Tests: pump injector flux+amplitude calibration against the discrete- dispersion prediction; seeded Raman-amplifier energy-budget closure (S_left - S_right vs 2x the field-only EPW energy rates -- the kinetic sloshing half doubles the electron heating; closes to ~4%, asserted <10%); R+T+absorbed==1; depletion actually reduces transmission vs the prescribed pump; epw_energy normalization; noise-seed reproducibility. Docs: overview pump-depletion note replaced, SRS diagnostics table added, config.md new keys (terms.light, probe_offset, noise_amplitude/seed, drivers.E0.offset/turn_on_time); datamodel updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lation Add terms.hpe: tail test electrons are pushed relativistically in the de-enveloped electrostatic field Re[Ex exp(-i wp0 t)], their spatially averaged velocity distribution is accumulated by exponential moving average, and the Landau damping rate applied by SpectralEPWSolver is recomputed from that evolving distribution every step (Follett et al., Phys. Plasmas 24, 102134 (2017), Eq. 4). The feedback is Im-only, so this captures trapping-induced damping reduction and hot-electron generation but not the nonlinear frequency shift. Quasi-1D (ny == 1) only. Departures from the paper, for JAX friendliness: - Tail-only loading (|v| > v_min*vte); modes whose phase velocity falls below the cutoff keep the analytic rate, blended per k-mode. - EMA histogram as a state variable instead of interval damping updates. - Per-k calibration of the histogram -> gamma_L operator, so the initial Maxwellian tail reproduces the analytic rate exactly and binning bias cancels. Calibration lands at C(k) in [0.965, 1.013] over the band. - gamma_HPE clamped >= 0. Two details the implementation needed: the damping formula requires a sgn(kx) so each propagation direction damps on its own tail, and the gather spectrally upsamples Ex by gather_refine (default 4) because linear interpolation at k*dx ~ 1-2 rad/cell attenuates the gathered field by sinc^2(k*dx/2) -- 15-30% at SRS wavenumbers, ~1% after. Trapping resonates at the Bohm-Gross v_phi, since the envelope rotation exp(-i dw(k) t) shifts the physical wave there, so omega_res defaults to bohm_gross. Also: terms.epw.damping.landau was previously ignored (damping was unconditionally on) and is now honored, including in the dissipation diagnostic, which reads the dynamic rate from the state when HPE is on. Diagnostics gain fhot_50keV, fhot_100keV, hpe_mean_energy_keV, the tail histogram hpe_hist, and the damping-reduction ratios; MLflow metrics are named to match the OSIRIS scan2 set (t_first_hot_e_50keV, hpe_damping_reduction_final) for one-to-one comparison. The headline ratio is hpe_gamma_ratio_kpeak -- the band-min is shot-noise-limited at low n_particles. Tests in tests/test_lpse2d/test_hpe.py cover free streaming, bounce frequency and carrier sign (M0), histogram normalization and damping calibration (M1), blend/clamp behavior, linear closure (M3a), O'Neil flattening (M3b), and an end-to-end SRS smoke run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # docs/source/solvers/lpse2d/config.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
HPE: kinetic inflation via hybrid particle evolution (commit fcf7295)This branch now includes a Follett-style hybrid particle evolution (HPE) model so the envelope solver can capture kinetic inflation of SRS — trapping-induced reduction of Landau damping and the associated hot-electron generation (Follett et al., Phys. Plasmas 24, 102134 (2017), Eq. 4). What it doesEnabled with
Departures from the paper (for JAX friendliness)
Implementation details that mattered
Diagnostics & metricsNew outputs: Where things live
Tests
|
|
I'm going to look into the physics details before merging to verify correctness |
Code review — 10 findings (8 correctness, 2 cleanup)Automated deep review of this PR's diff (not the whole package), with adversarial verification; six additional candidates were investigated and refuted. Parking these here to address later. Correctness1. Correlated noise ensembles — 2. EPW total-energy factor — 3. Growth-fit noise floor — 4. Silent zero-pump runs — 5. Silent SRS-source annihilation at the Raman critical density — 6. 7. Config schema is dead code; explicit nulls crash; 8. Light-substep stability bound picks the wrong density endpoint — Cleanup9. 10. KDK loop evaluates each Investigated and refuted (no action needed)
🤖 Generated with Claude Code |
Summary
Adds stimulated Raman scattering (backward SRS) to the
envelope-2d(lpse2d) solver. Until now the solver evolved the EPW potential against a prescribed pump with a TPD source only; theE1scattered-light field existed in the state vector but was never advanced. This PR evolvesE1with a paraxial finite-difference solver and closes the loop by adding the SRS source to the EPW equation, so backscatter grows self-consistently from noise (or from an optional injected seed).The physics is a direct translation of the
raman.solver = 'fd'branch of lpse-matlab (m201805_matlabLpse_v11.m); line references to the MATLAB source are kept in the code comments so the two can be diffed by hand.What's added
Raman light solver (
adept/_lpse2d/core/raman.py, new)Evolves the scattered-light envelope at
w1 = w0 - wp0:with the cross-derivative terms of the 2D paraxial operator, and the same staggered explicit update as MATLAB's
lightSplitStep(real part from the RHS att, imaginary part from the RHS att + dt/2).SRS source in the EPW equation (
core/epw.py)srsSource = i e wp0/(4 me w0 w1) · (n/n_env) · E0·conj(E1), added to the potential each step.E1is high-k filtered before the product (MATLAB'sisSuppressHighKSource, cutoff1.2 × k1_max) so only wavevectors near the light-wave envelope contribute; the pump is prescribed, so it is not filtered, matching the MATLAB static-laser path.Sub-cycling and stability (
helpers.py,datamodel.py)The light update is conditionally stable (
dt < ~dx² w1/c²), so it is sub-cycled inside each EPW step with the EPW potential held fixed. The number of sub-steps is derived from the stability bound generalized to 2D, or can be pinned withgrid.light_substeps— a value that violates the bound raises rather than silently going unstable. Absorbing boundaries are applied every sub-step, since light crosses the absorber at ~c.Optional Raman seed (
drivers.E1)A two-point antisymmetric injector at
x = xmax - offsetlaunches a-x-propagating wave at the localk1, with a configurable turn-on ramp and an optional 4th-order super-Gaussian transverse profile. The default offset (1.6 × boundary_width) keeps the injector clear of the absorber's tanh skirt, and a closer one warns. If the density at the injector is above thew1critical density the seed is evanescent, so setup fails with a message pointing at the three ways out (lowerdensity.max, moveoffset, or dropE1and run noise-seeded).Diagnostics
With SRS on, the default time series gains
e1_sqandreflectivity— the latter issqrt(eps1)·<|E1_y|²>_y / E0_source²at a probe on the low-density side, with thesqrt(eps1)factor accounting for the reduced group velocity relative to the vacuum pump.make_series_xarraysis now generic over whatever keys the save function returns instead of hard-codinge_sq/max_phi.Quasi-1D (
ny = 1) support1D SRS is the cheap configuration to run and the one the MATLAB
srs_1Dcase uses, but the field-save path assumed ≥2 transverse cells:interpax.interp2dreturns NaN off a single y-node andRegularGridInterpolatorrejects a single-node axis (filling 0). Both silently blanked every field artifact while the scalar series stayed valid. Added an x-only interpolation path forny == 1in the in-solve field saver and the background-density save, and adjusted the plotting to emit line plots vskxinstead of emptykx–kymaps.Also in passing:
density.basis: uniformnow honors avalkey instead of always returning 1.0, and the complex-dtype check inmake_field_xarraystests the actual float view rather than assumingcomplex128.Config surface
Example config:
configs/envelope-2d/srs.yaml— noise-seeded backward SRS on a 0.18–0.28 n_c linear ramp, thesrs_1Dcase.Validation
tests/test_lpse2d/test_srs.py:test_srs_growth_rate(parametrized 2D andny = 1) — noise-seeded homogeneous SRS. Fits the log-slope of the EPW energy over the late-time window and compares to the analytic backward-SRS rategamma0 = k v_os/4 · wpe/sqrt(w_ek w_s), evaluated at the phase-matchedkfrom a fixed-point solve of the Bohm–Gross/EM dispersion pair, with the pump wavenumber snapped to the FFT grid the way the solver launches it and the local density swelling folded intov_os. Agrees to 35%.test_srs_seed_propagation— pump and noise off, seed only. Checks the injected wave travels in-x, that its measured wavenumber matches the localk1to 5%, and that its amplitude matches the injector calibrationE1_source · sinc(k1 dx) / eps1^(1/4)to 30%.Notes and limitations
E0is reconstructed from the driver each step and is unaffected byE1, so reflectivities are only meaningful in the undepleted regime. Coupling depletion back intoE0is the natural follow-up.light_substepsextra RHS evaluations per EPW step, each with several FFT-free stencil passes plus oneifft2of the potential Laplacian per step. On the shipped example config this is single-digit sub-steps; shortdxat fixeddtraises it quadratically.drivers.E0.num_colors) composes with this for free — the Raman coupling consumesE0as a field, so broadband SRS works without further changes, though it is not exercised by a test here.Known issue, pre-existing and not addressed here:
drivers.E0.shape: arbitrary(the learnable amplitude/phase driver) looks broken onmain. #168 broadcast the driver output to(num_colors, ny)and switchedlaser.Light.laser_updateto index it as[i, :], butArbitraryDriver.__call__overridesUniformDriver.__call__and still returns 1-Dphases/intensities(modules/driver.py:174-178), so that path would raise on the first laser update.uniform/gaussian/lorentzianall inherit the broadcasting__call__and are fine, and no shipped config or test exercisesarbitrary, which is presumably why it has gone unnoticed. Flagging it rather than fixing it here to keep this PR to SRS — happy to fold in the one-line broadcast fix if reviewers would rather have it in the same change.Base branch
Branched from
mainand independent of the osiris wrapper (#279) — the two lines of work touch a disjoint set of files, and this one is confined toadept/_lpse2d,configs/envelope-2d,docs/source/solvers/lpse2d, andtests/test_lpse2d.🤖 Generated with Claude Code