Skip to content

feat: LPSE with SRS (and 1d support) - #320

Open
physicistphil wants to merge 4 commits into
ergodicio:mainfrom
physicistphil:lpse2d/srs
Open

feat: LPSE with SRS (and 1d support)#320
physicistphil wants to merge 4 commits into
ergodicio:mainfrom
physicistphil:lpse2d/srs

Conversation

@physicistphil

Copy link
Copy Markdown
Contributor

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; the E1 scattered-light field existed in the state vector but was never advanced. This PR evolves E1 with 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:

dE1/dt = i c²/(2 w1) ∇⊥² E1
       + i w1/2 (1 - wp0²/w1² · n/n_env) E1
       - i e/(4 w0 me) conj(∇²φ) E0
       + seed injection (optional)

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 at t, imaginary part from the RHS at t + 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. E1 is high-k filtered before the product (MATLAB's isSuppressHighKSource, cutoff 1.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 with grid.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 - offset launches a -x-propagating wave at the local k1, 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 the w1 critical density the seed is evanescent, so setup fails with a message pointing at the three ways out (lower density.max, move offset, or drop E1 and run noise-seeded).

Diagnostics

With SRS on, the default time series gains e1_sq and reflectivity — the latter is sqrt(eps1)·<|E1_y|²>_y / E0_source² at a probe on the low-density side, with the sqrt(eps1) factor accounting for the reduced group velocity relative to the vacuum pump. make_series_xarrays is now generic over whatever keys the save function returns instead of hard-coding e_sq/max_phi.

Quasi-1D (ny = 1) support

1D SRS is the cheap configuration to run and the one the MATLAB srs_1D case uses, but the field-save path assumed ≥2 transverse cells: interpax.interp2d returns NaN off a single y-node and RegularGridInterpolator rejects a single-node axis (filling 0). Both silently blanked every field artifact while the scalar series stayed valid. Added an x-only interpolation path for ny == 1 in the in-solve field saver and the background-density save, and adjusted the plotting to emit line plots vs kx instead of empty kxky maps.

Also in passing: density.basis: uniform now honors a val key instead of always returning 1.0, and the complex-dtype check in make_field_xarrays tests the actual float view rather than assuming complex128.

Config surface

terms:
  epw:
    source:
      srs: true          # new; default false, existing configs unaffected

grid:
  light_substeps: 8      # new, optional; derived from the stability limit if omitted

drivers:
  E1:                    # new, optional; without it SRS grows from the EPW noise source
    intensity: 1.0e+12W/cm^2
    delta_omega: 0.0     # fraction of w1
    turn_on_time: 10fs
    offset: 5um          # defaults to 1.6 * boundary_width
    yw: 20um             # omit for uniform in y

Example config: configs/envelope-2d/srs.yaml — noise-seeded backward SRS on a 0.18–0.28 n_c linear ramp, the srs_1D case.

Validation

tests/test_lpse2d/test_srs.py:

  • test_srs_growth_rate (parametrized 2D and ny = 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 rate gamma0 = k v_os/4 · wpe/sqrt(w_ek w_s), evaluated at the phase-matched k from 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 into v_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 local k1 to 5%, and that its amplitude matches the injector calibration E1_source · sinc(k1 dx) / eps1^(1/4) to 30%.

Notes and limitations

  • The pump is prescribed, so there is no pump depletion. E0 is reconstructed from the driver each step and is unaffected by E1, so reflectivities are only meaningful in the undepleted regime. Coupling depletion back into E0 is the natural follow-up.
  • Cost. Sub-cycling means light_substeps extra RHS evaluations per EPW step, each with several FFT-free stencil passes plus one ifft2 of the potential Laplacian per step. On the shipped example config this is single-digit sub-steps; short dx at fixed dt raises it quadratically.
  • The multi-color pump (drivers.E0.num_colors) composes with this for free — the Raman coupling consumes E0 as a field, so broadband SRS works without further changes, though it is not exercised by a test here.
  • The 2D SRS path shares the noise-seeded growth test with the 1D one; there is no dedicated test of the transverse (cross-derivative) terms yet.

Known issue, pre-existing and not addressed here: drivers.E0.shape: arbitrary (the learnable amplitude/phase driver) looks broken on main. #168 broadcast the driver output to (num_colors, ny) and switched laser.Light.laser_update to index it as [i, :], but ArbitraryDriver.__call__ overrides UniformDriver.__call__ and still returns 1-D phases/intensities (modules/driver.py:174-178), so that path would raise on the first laser update. uniform/gaussian/lorentzian all inherit the broadcasting __call__ and are fine, and no shipped config or test exercises arbitrary, 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 main and independent of the osiris wrapper (#279) — the two lines of work touch a disjoint set of files, and this one is confined to adept/_lpse2d, configs/envelope-2d, docs/source/solvers/lpse2d, and tests/test_lpse2d.

🤖 Generated with Claude Code

physicistphil and others added 4 commits July 27, 2026 16:31
- 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>
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