Skip to content

feat(lpse2d): parity with the original LPSE (deck translator, term/solver/kinetic parity, cross-checks) - #372

Open
physicistphil wants to merge 55 commits into
ergodicio:mainfrom
physicistphil:lpse2d/lpse-parity
Open

physicistphil wants to merge 55 commits into
ergodicio:mainfrom
physicistphil:lpse2d/lpse-parity

Conversation

@physicistphil

Copy link
Copy Markdown
Contributor

Summary

Feature parity of lpse2d (envelope-2d) with the original LPSE C++ code (deck v5.1.2), following the plan in the research vault (Notes/adept/2026-09-14-144422-claude-lpse2d-parity-plan-*). Everything is opt-in and default-preserving unless stated.

Foundations (A)

  • adept/_lpse2d/lpse_deck.py: LPSE lpse.parms translator (translate_parms, parse_parms with #include), ZAK unit system, read_metrics / read_frames for LPSE output files, CLI.
  • EPW energy ledger (terms.epw.energy_ledger) with per-operation channels and closure.

Term parity (B, B')

  • Thermal (fluctuation-dissipation) EPW noise, LPSE TPD source form (tpd_form: lpse), transverse TPD depletion, Landau-damping options (form, threshold, multiplier, relativistic 2-D Bessel rate), LPSE exponential absorbers, max_wavenumber caps, rectangular dealiasing, LPSE density profiles (lpse-<shape>: linear, exp, gaussian, inverse-power, quadratic, qd, gd, file).
  • IAW: spectral exact damped-oscillator propagator, Krall-Trivelpiece ion Landau rate, flow Doppler, stride, IAW noise, thermal filamentation (terms.iaw.thermal_filamentation).

Solvers (C)

  • Spectral light propagator with L/T projection and smooth injectors (terms.light.solver: spectral), collisional light absorption (NRL), separate light absorber rate (terms.light.boundary_max_rate), transverse projection of the SRS light sources (terms.light.transverse_source), optional SRS source k-filter (terms.epw.source.srs_k_filter).
  • Combined TPD+SRS solver (terms.epw.solver: combined).
  • Pump injector generalisation: oblique incidence (drivers.E0.angle), beamlets (beams), transverse super-Gaussian, Kubo-Anderson bandwidth, pulse-shape file.

Kinetics, workflow, diagnostics (D)

  • HPE controls and instruments (terms.hpe.*): gamma limits, per-direction thermalization probability with LPSE particle-wall semantics, uniform B field, energy-conservation multiplier, wall-flux and cone-power instruments.
  • Checkpoint / restart (save.checkpoint, restart.file), bit-exact resume.
  • Absolute-threshold bisection driver (adept._lpse2d.threshold).
  • Poynting-flux fields (save.fields.poynting) and Thomson probes (save.thomson).

Cross-checks against LPSE reference runs (translated decks)

deck physics result
homogeneous SRS control SRS from noise, periodic energy growth 25.2/ps vs LPSE 25.3/ps
homogeneous TPD control TPD from noise, periodic 28.7/ps vs 28.9/ps
test_006 SRS, static pump, gradient 5.96/ps vs 5.80/ps, same growth location (n_c/4)
test_029 TPD + IAW, static pump 12.3/ps vs 13.0/ps, saturation at 1.2 ps in both
test_010 TPD + IAW, oblique evolving pump same modes and saturation physics; growth 9.5 vs 11.8/ps (open)
test_032 combined TPD+SRS+IAW, 10 ps same phases, saturation at 3.3 ps; growth 12-16 % below LPSE (open)
test_013 everything, spectral light half-max at 2.36 vs 2.35 ps, IAW clip in both
test_022 everything, LPSE FD light half-max at 2.30 vs 2.69 ps, IAW clip in both
test_036 HPE walls, field-free wall flux per keV bin within 6 % (with v_min: 0.05)
test_037 everything + HPE feedback linear stage agrees; kinetic feedback over-damps after HPE starts (open, see vault M5 note)

Five adept defects were found and fixed by the cross-checks: the always-on SRS source k-filter removed the resonant Raman mode at densities below the envelope density; the longitudinal part of the SRS light source accumulated in E1 (spurious two-wave instability); the light fields used the EPW absorber (injected pump reflected into a standing wave, 1.45x amplitude) in the separate and in the combined solver; the spectral injectors ran without sub-cycling when the deck gave no light dt. The term coefficients matched LPSE in every homogeneous control.

Tests

tests/test_lpse2d/: test_lpse_parity.py, test_lpse_deck.py, test_spectral_light.py, test_combined.py, test_iaw_spectral.py, test_energy_ledger.py, test_oblique_pump.py, test_lpse_density.py, test_threshold.py, test_restart.py, test_diagnostics_lpse.py, test_thermal_filamentation.py, test_hpe.py (extended). Docs: docs/source/solvers/lpse2d/config.md.

🤖 Generated with Claude Code

physicistphil and others added 21 commits September 14, 2026 15:20
…rms, spectral IAW

Closes the term-level gaps against the original LPSE C++ code (gap audit
2026-09-14, tiers A.1, B and B' of the parity plan):

- TPD pump depletion: coefficient i e/(2 me w0) (twice the EPW-side source, the
  form settled against Follett's equation document; main carried half of it),
  acting on both pump components, with LPSE's transverse (divergence-free)
  projection of E_h div E_h in k-space (terms.light.tpd_projection) and the
  optional lw.kFilter (tpd_k_filter). The pair test now checks
  d/dt[|E0|^2 + (2 wp0/w0)|E_h|^2] = 0.
- TPD source: every pump component and LPSE's exact coefficient i e/(4 me w0)
  with the (w0/wp0 - 1) charge-density factor (terms.epw.source.tpd_form: lpse,
  default; matlab keeps the prototype's w0 -> 2 wp0 form; identical at envelope
  density 0.25).
- EPW noise: terms.epw.source.noise_model: thermal -- LPSE's fluctuation-
  dissipation source D_k = N A/sqrt(1 + k^2 lambda_D^2) sqrt(1 - e^{-2 gamma dt})/|k|
  added after the damping sub-step, relaxing every mode to the Cerenkov spectrum;
  noise_calibrate sets A from Te by equipartition; noise_max_wavenumber. flat
  (the MATLAB kick) stays the default. The kick spectrum is shared with the
  expected-noise-floor diagnostic.
- Landau damping: landau_form matlab | lpse (sqrt(1+3x^2) prefactor) |
  relativistic / relativistic_3d (LPSE's Maxwell-Juettner Bessel forms),
  landau_lower_threshold, landau_multiplier; one analytic_landau_rate() used by
  the EPW step, HPE calibration, dissipation diagnostic and thermal noise.
- terms.epw.max_wavenumber (lw.maxWavenumber) band cap.
- Absorbing layers: grid.boundary_profile: exp (LPSE absorbingBoundaries.cpp
  profile, lambda = 7, peak rate 200/ps, IAW layer at half), tanh stays default.
- IAW: terms.iaw.solver: spectral -- LPSE's exact per-mode damped-oscillator
  propagator (unconditionally stable) with uniform background flow (Doppler
  phase), stride (advance every N EPW steps), LPSE's fluctuation-dissipation
  noise on div v, and damping.landau_form: full (Z-generalized Krall-Trivelpiece
  rate). The MATLAB explicit stepper is unchanged as solver: explicit.

The light-side n_b/n0 factor turned out to be hard-wired to 1 in LPSE too
(LightSolver.cpp:4217), so no change was needed there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GimrKCs3NPtxjbZED63HDo
Accumulates in the state the change of sum_k k^2 |phi_k|^2 attributed to each
operation of the EPW split step (dispersion, damping, dealias, noise, detuning,
boundary, reprojection, tpd, srs, driver) and reports the cumulative channels
in the default series in epw_energy units, with epw_ledger_closure =
epw_energy - sum(channels). This is the per-step energy-budget check the
original LPSE asserts to 0.1 %; here it closes to round-off by construction
and the channels are the diagnostic. Off by default (no state change).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GimrKCs3NPtxjbZED63HDo
terms.light.solver: spectral -- the original LPSE {laser|raman}.solver = spectral
path: per light sub-step the x-space scattering potential exp(i dt Vo)
(detuning + absorption), the EPW couplings with forward Euler, and the exact
k-space propagator exp(-i dt c^2 k^2/(2 w)) on the transverse part of the field
(longitudinal part left unpropagated, as LPSE), band-limited. No CFL limit
(light_substeps defaults to 1), no grid dispersion; with pump depletion the
SRS exchange is the exact local rotation Strang-split around the propagation.
Injectors become smooth Gaussian sources (drivers.E0/E1.injector_width, half a
local wavelength by default like LPSE's injectorWidth) that launch the
requested amplitude to 0.2 % with negligible wrong-direction leakage.
terms.light.max_wavenumber caps the retained light band.

terms.light.absorption -- inverse-bremsstrahlung absorption at nu (n/nc_w)^2
per wave (LPSE calculateScatteringPotential), nu from the NRL formula as coded
in LPSE (true) or a user rate at nc (1/ps); applied per sub-step on both light
solvers, IAW density perturbation included.

The laser-budget flux correction knows the spectral solver has no grid
dispersion.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GimrKCs3NPtxjbZED63HDo
The original LPSE lw.solver = combined formulation, which LPSE requires
whenever TPD and SRS are both on: one wp0-enveloped field E1 carries the Raman
light as its transverse part and the EPW as its longitudinal part, and the
potential is derived from it every step for the diagnostics, IAWs and HPE.

Per light sub-step (LightSolver::evolveSpectral order): density detuning and
collisional damping on the whole field; LPSE's unified source
-i e/(4 me w0) e^{-i(w0-2wp0)t} [grad(E0.E1*) + (1 - w0/wp0) E0 (div E1)*]
(its longitudinal part is the TPD + SRS source, its transverse part the
Raman-light generation -- verified against the separate solver's terms to 1e-9
at n_c/4); the 2x2 longitudinal/transverse propagator (Bohm-Gross + Landau on
k k/k^2 E1, exp(-i dt c^2 k^2/(2 wp0)) on the rest); EPW noise on the
longitudinal part; absorbers. With pump depletion the pump advances in the
same sub-step with the unified depletion i e/(2 me w0) e^{+i dw t} E1 div E1.
Requires terms.light.solver: spectral and tpd == srs (LPSE's rule); the
separate solver now warns when both instabilities are on, quoting LPSE.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GimrKCs3NPtxjbZED63HDo
…ensity/dealias options

- adept/_lpse2d/lpse_deck.py: ZakUnits (LPSE ZAK unit system), parse_parms (with
  #include), translate_parms (LPSE lpse.parms -> adept cfg + report), read_metrics,
  read_frames, CLI entry point
- density bases `lpse-linear` / `lpse-exp` (min/max at min_location/max_location)
- `dealias: rectangular` (LPSE aa_range convention)
- noise_debye_factor toggle (LPSE isCalculated branch)
- tests/test_lpse2d/test_lpse_deck.py

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GimrKCs3NPtxjbZED63HDo
…y default there)

The high-k filter on E1 (and the evolved pump) entering the SRS source cut at
1.2 k1(n_min) for zero detuning. In a box below the envelope density the resonant
back-scattered Raman wavenumber lies above that cutoff, so the filter removed the
SRS mode entirely: a uniform 0.2 nc box with envelope density 0.25 showed no growth
at all, while LPSE (kFilter disabled by default) grows at 25.3/ps in energy.

- terms.epw.source.srs_k_filter (default true, previous behaviour) and
  srs_k_filter_scale (default 1.2), honoured by the separate and combined solvers
- translator sets them from lw.kFilter.enable / lw.kFilter.scale
- band-empty validation only when the filter is on
- test: default filter masks the 0.2 nc resonance; off / scale 1.5 pass it

With the filter off adept's homogeneous SRS energy growth is 25.2/ps vs LPSE 25.3/ps
(theory 2 gamma0 = 27.1/ps), onset times equal to 0.02 ps.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GimrKCs3NPtxjbZED63HDo
…ed longitudinal light

No light propagator moves the longitudinal part of E1, so the longitudinal part of the
SRS source accumulated there and paired with the EPW in a spurious two-wave instability:
on the translated LPSE test_006 deck adept's EPW energy grew at 41/ps (E1 100 %
longitudinal at |k| ~ 37/um, outside the light band) against LPSE's 5.8/ps.

- terms.light.transverse_source (default true; LPSE takeTransversePartOfSourceTerms):
  the SRS source (FD and spectral Raman solvers) and the SRS pump-depletion source
  (FD coupled solver) are projected onto their transverse part; the coupled spectral
  solver keeps the fields transverse instead (its exchange is an exact local rotation)
- the spectral propagators now drop every component outside the retained light band,
  as LPSE does (LightSolver.cpp:3720-3735); before, only the transverse part was masked
- translator maps raman/laser.takeTransversePartOfSourceTerms (LPSE default false)
- tests: longitudinal light kept only inside the band; a purely longitudinal SRS source
  yields no E1 with the projection (spectral and FD)

M1 cross-check (test_006, SRS from noise in a 0.20-0.28 nc linear profile): adept EPW
energy growth 5.96/ps vs LPSE 5.80/ps, Raman 5.96 vs 5.80, growth peaks at the same
positions (potential 13.0 vs 13.0 um, Raman 11.3 vs 11.4 um).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GimrKCs3NPtxjbZED63HDo
- static pump (laser.Light): each color is the grid mode nearest to k0 (cos a, sin a),
  both components snapped as in LPSE makeStaticField, polarised perpendicular to the
  snapped k; swelling as before
- spectral pump injector: transverse wavenumber snapped to the periodic y grid, kx from
  the local dispersion per color, x group velocity sets the injected flux, field
  perpendicular to k; launched amplitude 1.009 (20 deg) vs 0.993 (0 deg) of target
- the FD two-point injector and speckle refuse a non-zero angle with a clear error;
  only in-plane (p) polarisation is representable
- pump k-support for the dealiasing band includes k0 sin(angle)
- translator maps laser.N.direction (in-plane, rightward) to the angle; multi-beam
  decks with differing directions and out-of-plane polarisation are reported
- tests: static mode/polarisation at 0 and 20 deg, launched oblique wave (ky row,
  |k| = k0, transverse), FD refusal, test_010 direction mapping

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GimrKCs3NPtxjbZED63HDo
linear, exp, gaussian, inverse-power, quadratic, qd (parabolic dip), gd (super-Gaussian
dip) and file, ported from ZakharovSolver::backgroundDensityShape: N_min/N_max at two
locations (x and optional y), cartesian or spherical distance, sg_order, quadratic
central density, dip depth/width/offset about an origin (LPSE's box centre), the
maxBackgroundDensity clip (max_density, 1.25). lpse-file reads .npy, text tables or LPSE
grid files. The translator maps densityProfile.shape/geometry/sgOrder/dip.*/
quadratic.centralDensity/maxBackgroundDensity/loadFilename.

Tests: endpoints and midpoints of every shape against the LPSE formulas, dips on the
ramp, spherical geometry, the clip, file round trip, translator key mapping.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GimrKCs3NPtxjbZED63HDo
Ported from ElectronTracker.cu / ZakharovSolver::getLD_multiplierForEnergyConservation:
- gamma_limit_damping / gamma_limit_growth / allow_growth: clip of the applied kinetic rate
- thermalization_probability per wall direction: thermalize (re-inject from the tail) or
  reflect specularly
- magnetic_field: uniform out-of-plane B rotates the momentum in the 2-D push
- energy_conservation (+ energy_conservation_steps): global multiplier on the applied rate
  so the expected EPW loss (linearised 2 gamma dt per mode) equals the particles' gain,
  clamped to [0.1, 10] and running-averaged; reported as hpe_ld_multiplier
- wall-flux instrument (flux_bins, keV) and cone-power instrument (cone_angle,
  cone_direction): cumulative energy leaving each wall per bin / inside the cone, in the
  default series (hpe_wall_energy_<wall>_bin<i>, hpe_cone_energy)
- push / push_2d return the wall instruments and the kinetic-energy change
- translator maps hpe.nElectrons/velocityGrid/startEvolutionAt/thermalizationProbability/
  magneticField/gammaLimit.*/allowGrowth/enforceEnergyConservation/
  numStepsToAverageEnergyChange/metrics.flux|power
- tests for each control and the translator mapping; existing HPE tests adapted

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GimrKCs3NPtxjbZED63HDo
find_threshold bisects the pump intensity between a stable and an unstable bracket
(geometric midpoints), one ergoExo run per point, deciding on the run's metrics (default:
measurable positive EPW growth); bracket check as in LPSE AbsoluteThreshold; MLflow
logging of the bisection. Tests with a fake runner: convergence to a known threshold and
bracket validation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GimrKCs3NPtxjbZED63HDo
…boundary_max_rate)

LPSE damps the light fields at {laser|raman}.evolution.abc.maxDampingRate (default 5e3/ps)
in its exp layers while the EPW layer uses 200/ps. adept applied the EPW layer to the light
too: light crosses a 3 um layer in 0.01 ps, so the layer removed only ~25 % of the amplitude
per pass and an injected pump reflected off both walls into a coherent standing wave -- on
the LPSE test_010 deck the pump in the box was 1.36-1.46x the launched amplitude (found by
bisecting the M2 cross-check, where adept's TPD grew twice as fast as LPSE).

grid.light_absorbing_boundaries (5e3/ps by default with the exp profile, the tanh profile
unchanged) is now what the FD/spectral light solvers apply; the translator maps the LPSE
key. Test on the rate profiles and the transit-integrated attenuation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GimrKCs3NPtxjbZED63HDo
…'s IAW step rule

- save.checkpoint: true|path writes the full solver state at grid.tmax (.npz, in the run's
  binary/ folder when true); restart.file resumes from it: state replaces the initial
  condition, integration and the series/fields axes start at the checkpoint time (snapped to
  the step grid), mismatched state shapes are refused. The per-step noise and wall keys are
  folded in from the time index, so a split run reproduces the unbroken one to round-off
  (test: 10 fs + 10 fs vs 20 fs, fields and energy equal to 1e-9)
- translator: the IAW stride follows Lpse.cpp (own dt capped by maxLightStepsPerStep light
  steps and maxLwStepsPerStep (default 2) EPW steps): test_010 now steps the IAW every EPW
  step as LPSE printed, test_029 every second

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GimrKCs3NPtxjbZED63HDo
…l filamentation

D.6: save.fields.poynting adds s0_x/s0_y/s1_x/s1_y = (c^2/w) Im(E* . grad E) (LPSE
laser/raman.save.S0); save.thomson probes add the complex amplitude and power of the EPW
potential (or IAW density) in a k-window to the default series (LPSE thomsonScattering.N).

D.7: terms.iaw.thermal_filamentation {laser, raman, lw, nonlocal, conductivity_multiplier}
ports ZakharovSolver::getThermalFilamentationSource: inverse-bremsstrahlung heating of the
spatially varying intensity of each wave, balanced by Spitzer conduction, drives the ion
velocity divergence through the electron pressure; the nonlocal k^(4/3) correction with
lambda_nl = 30 (k_B T)^2 / (4 pi e^4 sqrt(Z+1) lnLambda n_e). The source form is LPSE's, the
normalization adept's own (derived from the heat and momentum equations). Translator maps
thermalFil.*.

Tests: plane-wave Poynting = v_g |E|^2, Thomson window selection, thermal source vanishing
for uniform intensity and matching the derived coefficient, nonlocal k^(4/3) scaling,
requirement errors, translator mapping.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GimrKCs3NPtxjbZED63HDo
…ulse-shape file (C.4)

- drivers.E0.beams: LPSE laser.N.* beamlets (intensity fraction, angle, phase, delta_omega)
  for the static pump and the spectral injector; every beam carries every color
- drivers.E0.beam_width / beam_sg_order / beam_offset: super-Gaussian transverse profile of
  the injected beams (LPSE laser.N.width, sgOrder, offset)
- drivers.E0.kap_bandwidth / kap_seed: Kubo-Anderson phase jumps every 2 pi / (dW), with a
  deterministic hash of the interval and beam (LPSE laser.bandwidth.KAP.frequency)
- drivers.E0.pulse_file: (t_ps, amplitude) table multiplied onto the pump (LPSE laser.pulse.file)
- translator maps multi-beam decks and the beam-shape / bandwidth / pulse keys
- tests: two beams launch both transverse wavenumbers with equal power; transverse profile,
  pulse scaling and KAP jump statistics; translator mapping

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GimrKCs3NPtxjbZED63HDo
…en the deck omits it

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GimrKCs3NPtxjbZED63HDo
…ot the EPW one)

Found by the M4 cross-check on test_013 (combined solver + evolving spectral pump): the pump
reflected in the 200/ps EPW layers into a standing wave (transmitted flux 2.17 of nominal).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GimrKCs3NPtxjbZED63HDo
…c.N keys (MeV -> keV)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GimrKCs3NPtxjbZED63HDo
…PSE's fluxMetric/powerMetric keys

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GimrKCs3NPtxjbZED63HDo
…verVminPhase mapping

ElectronTracker.cu counts every crossing of a box side in the flux/power instruments, then
thermalizes the particle with the per-direction probability or lets it pass through
periodically, independently of the field boundaries. adept wrapped silently at periodic field
boundaries and reflected non-thermalized particles at absorbing ones. Now: crossings on all
four sides are counted; thermalize-or-wrap; the default probability keeps the earlier
behaviour (1 at absorbing field boundaries, 0 at periodic ones). The translator's HPE flux
bins are keV (the decks bin a 2 keV plasma at 0.01-20 keV) and hpe.VminOverVminPhase = 0
maps to a whole-Maxwellian tail cutoff (v_min = 0.05 vte).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GimrKCs3NPtxjbZED63HDo
… its injectors

The exact propagator needs no CFL limit, but the smooth injectors add their source with an
Euler step; with light crossing 16 cells per step (translated test_022, no laser.dt in the
deck) the launched pump was 3x its nominal flux. Default light_substeps = ceil(c dt / dx) for
the spectral solver (LPSE's spectral decks use raman.dt ~ dx/c); an explicit smaller value
warns. The translator no longer maps hpe.VminOverVminPhase = 0 to a whole-Maxwellian tail
(adept's kinetic feedback is calibrated for a tail); it reports the option instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GimrKCs3NPtxjbZED63HDo

@ergodic-reviewer ergodic-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes for the source-level defects documented in the review comments: combined-solver noise uses the wrong timestep, its evolved pump ignores supported driver settings, combined Raman-flux maps include the EPW, LPSE density files are transposed twice, quasi-1D periodic particle crossings bypass the new wall handling, and a default test depends on an unavailable developer-local deck.

Postprocessing is implemented and reachable through BaseLPSE2D.post_process and the existing ergoExo artifact lifecycle. The additions connect ledger/HPE/Thomson series, optional flux fields, and checkpoints to that path; the combined-field flux incompatibility needs correction before those maps are scientifically usable.

Validation: reviewed the complete diff, relevant implementations/call sites, tests, and repository guidance at 3c429904ed81e4b75e19e5b02763e22388f3feec. GitHub reports test-lpse2d as failing. I did not execute tests, benchmarks, or postprocessing, and did not access the author's local LPSE reference artifacts.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Scale the noise kick to the timestep at which it is applied

CombinedSolver.__init__ builds self.noise_kick with noise_kick_spectrum(cfg), which uses grid.dt. However, substep calls add_noise on every light sub-step, while propagation/damping advance by self.dt_l. With light_substeps > 1, this repeatedly injects an outer-step kick against sub-step damping. In particular, thermal noise no longer equilibrates to the configured fluctuation-dissipation spectrum, and changing light subcycling changes the physical seed level. diagnostics.expected_noise_energy still assumes one outer-step kick, so its growth-fit floor also disagrees with the simulation. Build the kick for the actual noise timestep (and keep the diagnostic consistent), or apply the outer-step source only once per outer step. Add a combined-solver thermal-noise/subcycling regression; the current equilibrium test exercises only SpectralEPWSolver.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 59e01d0. epw.noise_step(cfg) returns the step at which the active solver applies the kick (the light sub-step for the combined solver) and the rate it damps with; CombinedSolver builds its kick from it and diagnostics.expected_noise_energy uses the same values, so the growth-fit floor matches. The flat model's kick is now amplitude * sqrt(dt_EPW * dt) (unchanged for the separate solver), so its seed level does not depend on the sub-step count either. New tests: the combined steady state is independent of light_substeps for thermal and flat noise (8 vs 2 sub-steps gave 3.96x / 3.98x on the old kick), and equals the expected_noise_energy floor (15 %).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve pump-driver settings on the combined-solver path

CombinedSolver.calc_pump_source only constructs a normal-incidence scalar source, and substep adds it only to E0[..., 1]. It never consumes the derived beam angles/fractions/phases/frequency offsets, transverse profile, KAP bandwidth, or pulse table implemented by SpectralCoupledLight. Setup nevertheless accepts these settings with epw.solver: combined because light.solver is spectral, and SplitStep.combined_step bypasses self.coupled_light. Thus switching an oblique/multi-beam or shaped-pulse run to the solver required for simultaneous TPD/SRS silently changes the pump to a different physical problem. Share the existing spectral injector implementation with the combined solver, or explicitly reject unsupported combinations rather than ignoring them; cover an oblique or multi-beam combined run.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1e3cf4a by sharing the implementation: the spectral light solver's injector (beam angles, fractions, phases, frequency offsets, transverse profile, KAP bandwidth, pulse table) moved into SmoothPumpInjector, which SpectralCoupledLight and CombinedSolver both use; the combined step adds the full vector source. Normal incidence is bit-identical to the previous combined source, so the combined parity decks (one normal-incidence beam each) are unaffected. New tests: the combined and spectral sources are identical for two oblique beams with a profile, KAP and a pulse table, and a combined run launches both +-20 deg beams (> 95 % of the power in the two ky rows, balanced to 5 %); both fail on the old solver.

Comment thread adept/_lpse2d/helpers.py Outdated
derived = cfg["units"]["derived"]
for name, arr, w in (
("s0", np.array(state["E0"]).view(_complex), derived["w0"]),
("s1", np.array(state["E1"]).view(_complex), derived["w1"]),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Remove the EPW component before computing Raman flux maps

For epw.solver: combined, saved state["E1"] is the full longitudinal EPW plus transverse Raman field. This loop treats it all as Raman light and applies the light-flux coefficient, so even a purely longitudinal EPW traveling mode produces a nonzero s1_x/s1_y map despite there being no scattered light. The default-series saver already handles this distinction with combined.transverse_part, but that local conversion does not change the separately saved fields consumed here. Apply the same separation to the field-output path, preferably before spatial downsampling, and test a pure longitudinal combined field. Otherwise the new persisted Raman-flux diagnostics misidentify plasma-wave energy as escaping light.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f6b1e1a. helpers._raman_light_part(cfg) takes the transverse part of E1 in combined mode; the default series and both field-save paths now use it, before any spatial interpolation, so the saved E1 fields and the s1 maps are the Raman light (the EPW is saved as epw). Test: a pure longitudinal combined field saves E1 = 0 and s1 = 0, and a mixed field's s1 equals its transverse part's.

Comment thread adept/_lpse2d/helpers.py Outdated
else:
from adept._lpse2d.lpse_deck import read_frames

arr = np.real(read_frames(path)[-1][1]).T # LPSE stores x fastest: (ny, nx) -> (nx, ny)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Do not transpose LPSE frames a second time

lpse_deck.read_frames already converts the x-fastest file layout to an (Nx, Ny) array with reshape((nz, ny, nx)).transpose(2, 1, 0)[:, :, 0]; its reader test explicitly checks that shape. The .T here reverses it again. Consequently, a rectangular LPSE density file is rejected by the following (nx, ny) shape check, while a square file silently rotates an x-dependent density profile into the y direction. Use the reader's returned orientation directly and add a non-square LPSE-file density round trip; the current density-file test only exercises .npy.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 27418ef: the loader uses read_frames' orientation directly. Test: a non-square (200 x 8) profile written in LPSE's binary layout round-trips through read_frames and the lpse-file loader; the old loader raised the shape error. The only shipped LPSE deck with a density file (test_018, 1800 x 1800) had not been run in adept.

Comment thread adept/_lpse2d/core/hpe.py Outdated
x = x + self.dtp * u_half / gamma_rel
if self.periodic_x:
x = jnp.mod(x - self.xmin, self.Lx) + self.xmin
x = jnp.mod(x - self.xmin, self.Lx) + self.xmin # sub-step gather needs in-box positions

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Retain periodic crossings in the quasi-1D particle push

This wraps the carried x back into the box on every quasi-1D sub-step. By the time push invokes the new _apply_boundaries, neither out_left nor out_right can detect a periodic crossing. For ny == 1 with periodic x field boundaries, the new wall/cone energy instruments therefore miss outgoing particles, and an explicitly nonzero thermalization_probability[0] is silently ignored. The 2-D implementation now preserves the unwrapped position and wraps only the gather coordinates for exactly this reason. Apply that pattern here as well, with a 1-D particle crossing test that checks both the instrument and thermalization.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f6c7429 with the 2-D pattern: the quasi-1D _substep wraps only the gather coordinate and carries the unwrapped x to _apply_boundaries; with the default pass-through probability the trajectories are unchanged. Test through push(): 10 keV electrons crossing the right wall of a periodic quasi-1D box are counted at that wall, then wrap (p = 0) or are re-injected inward at x_max (p = 1); the old push recorded no flux.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Make the beam-direction translation test portable

test_translator_maps_the_test_010_beam_direction unconditionally passes /home/phil/Desktop/Ergodic-projects/original-lpse/examples/testRuns/test_010/lpse.parms to parse_parms, which immediately calls Path.read_text(). That external checkout is not provided by the repository or the test-lpse2d workflow, so this normal, unmarked test raises FileNotFoundError in CI and ordinary contributor checkouts. Use a checked-in/minimal temporary deck for this translation assertion, or guard the external-reference case as the tests in test_lpse_deck.py already do. This is a concrete portability failure, not a request to reproduce the inaccessible reference simulations.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 278fda7: the test writes test_010's beam lines (laser_include.txt) and the main-deck keys the translator needs to a temporary directory, so parse_parms (including the #include) and translate_parms run without an LPSE checkout.

physicistphil and others added 6 commits October 8, 2026 13:35
…step

PR ergodicio#372 review (P1). CombinedSolver applied a kick built for the EPW step on every light
sub-step, so the seeded power scaled with grid.light_substeps (8 vs 2 sub-steps: 3.96x
thermal, 3.98x flat) and diagnostics.expected_noise_energy, which assumed one EPW-step
kick, disagreed with the simulation.

- epw.noise_step(cfg): the step at which the active solver applies the kick (the light
  sub-step for the combined solver, as LPSE's addNoise_combinedSolver_spectral(dt)) and
  the collisional rate it damps with; the solver and the growth-fit floor both use it.
- noise_kick_spectrum(cfg, dt, nu_coll) (the signature of the plan-2 branch). The flat
  model's kick is amplitude * sqrt(dt_EPW * dt): unchanged at the EPW step, and the same
  injected variance per unit time when kicked every sub-step.
- tests: combined steady state independent of n_sub (thermal and flat, 15 %) and equal
  to the expected_noise_energy floor (15 %); both ratio tests fail on the old kick.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PR ergodicio#372 review (P1). CombinedSolver.calc_pump_source built only a normal-incidence scalar
source on E0_y and ignored beam angles, fractions, phases, frequency offsets, the
transverse profile, KAP bandwidth and the pulse table, which setup accepted because the
light solver is spectral. Switching an oblique / multi-beam / shaped run to the combined
solver (required for simultaneous TPD + SRS) silently changed the pump.

- SmoothPumpInjector (spectral_light.py): the injector state and calc_pump_source /
  kap_phase moved out of SpectralCoupledLight unchanged; SpectralCoupledLight and
  CombinedSolver both inherit it, and the combined step adds the full (x, y) source.
- Normal incidence is bit-identical to the previous combined source (checked at three
  times), so the parity decks (test_013/022/037/083: one normal-incidence beam, no
  profile) are unaffected.
- tests: the combined source equals the spectral source exactly for two oblique beams
  with profile, KAP and a pulse table; a combined run launches both +-20 deg beams
  (> 95 % of the power in the two ky rows, balanced to 5 %). Both fail on the old solver.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e EPW

PR ergodicio#372 review (P2). With terms.epw.solver: combined the saved E1 is the whole field --
the EPW longitudinal part plus the Raman light -- and make_field_xarrays applied the
light-flux coefficient to all of it, so a pure plasma wave produced nonzero s1_x / s1_y
maps. The default series already took the transverse part.

- helpers._raman_light_part(cfg): one transverse projection, used by the default series
  and now by both field save paths, before spatial interpolation; saved E1 fields and s1
  maps are the Raman light (the EPW is saved as epw). Identity for the separate solver.
- test: a pure longitudinal combined field saves E1 = 0 and s1 = 0 (< 1e-9 of v_g |A|^2),
  and a mixed field's s1 equals its transverse part's; the old code saved the full field.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PR ergodicio#372 review (P2). lpse_deck.read_frames already returns LPSE's x-fastest frames as
(Nx, Ny); the density loader transposed them again, so a rectangular density file was
rejected by the shape check and a square one was silently rotated from x into y. The
one shipped LPSE deck with a density file (test_018, 1800 x 1800) has not been run in adept.

Test: a non-square (200 x 8) x-ramp with a y modulation written in LPSE's binary layout
round-trips through read_frames and the lpse-file loader (float32, rtol 1e-6); the old
loader raised "has shape (8, 200), the grid is (200, 8)".

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

PR ergodicio#372 / ergodicio#378 review (P2). The quasi-1D sub-step wrapped the carried x into the box after
every drift, so by the time push() called _apply_boundaries no periodic crossing was
visible: the wall / cone energy instruments missed outgoing particles and a nonzero
thermalization_probability was ignored for ny == 1 with periodic x. The sub-step now wraps
only the gather coordinate and carries the unwrapped x, as the 2-D push does; with the
default (pass-through) probability the trajectories are unchanged.

Test, through push(): 10 keV electrons crossing the right wall of a periodic quasi-1D box
are counted at that wall (10 keV each, rtol 1e-6) and then wrap (p = 0) or are re-injected
inward at x_max (p = 1); the old push recorded no flux.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cked-in deck

PR ergodicio#372 review (P2): the test read test_010's deck from a developer-local LPSE checkout and
raised FileNotFoundError in CI (the only test-lpse2d failure on this PR). It now writes the
deck's beam lines (laser_include.txt) and the main-deck keys the translator needs to a
temporary directory, so parse_parms (including the #include) and translate_parms run
everywhere.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
physicistphil added a commit to physicistphil/adept that referenced this pull request Oct 20, 2026
…ew round 6) into feat/lpse2d-hpe-spatial-fv

Brings the combined-field Raman absorption on ergodicio#372, the threshold zero-bound checks, the
quasi-1D RA filter passband, prefix-stable KAP histories, the restart rebase of the
light-spectrum probes and the batch files' quoted multi-window specs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@ergodic-reviewer ergodic-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes for two correctness defects detailed inline: combined thermal-noise setup can be rejected by the unused separate EPW solver, and FD rotation coupling bypasses the new transverse SRS-source projection.

Postprocessing is implemented and reachable through BaseLPSE2D.post_process and the existing ergoExo artifact lifecycle, including field/series datasets, plots, derived metrics, optional flux/Thomson/ledger/HPE diagnostics, and checkpoints. The latest combined-absorption and threshold-bracket fixes are present with regression tests in source.

Validation: source review of the diff, relevant implementations/call sites, tests, and repository guidance at 672418056145997380be2b8c032ce264f7f86002. The last GitHub check read reported test-lpse2d in progress. I did not execute tests, benchmarks, or postprocessing, or inspect the external LPSE reference artifacts.

Comment thread adept/_lpse2d/core/vector_field.py Outdated
@@ -25,15 +26,42 @@ def __init__(self, cfg):
self.wp0 = cfg["units"]["derived"]["wp0"]
self.epw = epw.SpectralEPWSolver(cfg)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Skip separate EPW initialization for combined runs

SplitStep constructs SpectralEPWSolver before selecting the combined solver. With terms.epw.solver: combined, source.noise: true, noise_model: thermal, damping.landau: false, damping.collisions: false, and positive terms.light.absorption, this unused constructor calls noise_kick_spectrum(cfg) without the combined rate. It therefore sees zero damping and raises thermal needs EPW damping before CombinedSolver is reached. The combined solver itself correctly uses noise_step(cfg) and the Raman absorption rate, so this is a valid damped thermal-noise configuration that cannot run through BaseLPSE2D/ergoExo.

Only initialize the separate EPW solver on the separate path (or otherwise avoid its irrelevant noise validation). Add a regression through SplitStep or module setup; constructing CombinedSolver alone does not cover this failure.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2577b05. SplitStep determines epw_solver first and builds epw.SpectralEPWSolver(cfg) only on the separate path; in combined mode self.epw is None (nothing else reads it there — the other builds in SplitStep.__init__ are already skipped or used by the combined step).

Test test_combined.py::test_combined_thermal_noise_needs_only_the_raman_absorption: combined, noise_model: thermal, Landau and collisions off, Raman absorption > 0 → SplitStep(cfg) sets up, step.epw is None, noise_step(cfg) returns ν_R, and the combined noise kick is nonzero. Before the fix it raised the quoted "thermal needs EPW damping" error. The docs row for terms.epw.solver now says the separate EPW solver is not built in combined mode.

Merged into plan2 (3a096b8; the plan2 version of the test uses terms.light.raman_absorption) and #387 (5f7b67b). On plan2, test_gradients.py used step.epw.energy as its objective on the combined paths too; 7081182 computes the same measure Σₖ k²|φₖ|² from the grid (tolerances unchanged, 8/8).

Comment on lines +231 to +233
depletion = (self.srs_depletion_coeff0 * laplacian_phi)[..., None] * E1
if self.transverse_source:
depletion = transverse_part(depletion, self.kx_arr, self.ky_arr, self.one_over_k_sq)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Honor transverse-source projection in the rotation coupling path

The new projection is only applied inside the couple-guarded RHS. For the supported FD evolved-pump configuration coupling: rotation, CoupledLight.__call__ sets couple_in_rhs = False and instead uses couple(), whose pointwise exchange never checks self.transverse_source. Consequently transverse_source: true is silently ignored for both SRS light sources on that path.

In 2-D, even a transverse input pump multiplied by an EPW mode with nonzero transverse wavenumber produces a longitudinal component in the Raman source. The rotation path retains that component, which then enters the EPW SRS feedback instead of evolving the projected light equations. This defeats the longitudinal-source correction implemented here. Make the rotation path compatible with the projection, or explicitly reject the unsupported combination, and cover it with a nonzero-ky regression rather than only the pointwise exchange invariant.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 55fd66f by refusing the combination rather than adding a projection to the rotation: the exact exchange is a pointwise rotation of (E0, E1), and the k-space projection of its sources has no counterpart in it (the spectral solver keeps the fields themselves transverse instead, which is why it is unaffected).

CoupledLight.__init__ now raises for coupling: rotation + SRS on + transverse_source: true with terms.light.solver: fd, naming the alternatives: coupling: explicit or solver: spectral to keep the projection, or transverse_source: false — which reproduces what fd rotation runs did so far. On this branch transverse_source defaults to true, so an fd rotation deck that leaves it unset is now refused. FDCombinedSolver inherits this __init__, but combined mode already refuses any non-explicit coupling in helpers.

Test test_light_coupling.py::test_fd_rotation_refuses_the_transverse_source_projection_it_cannot_apply (did not raise before). The rotation configs of test_light_coupling.py and the srs+pump_depletion case of test_energy_ledger.py were built with the default and are refused without it; they now set transverse_source: false explicitly, and their behaviour is unchanged. Docs: coupling and transverse_source rows.

On plan2 transverse_source defaults to false (LPSE takeTransversePartOfSourceTerms), so there only an explicit true is refused; 4223609 adapts the message, docs and test, and names transverse_fields: true (applied after the sub-steps under either coupling) as the fd way to keep the light transverse with rotation.

physicistphil and others added 3 commits October 19, 2026 22:45
SplitStep built SpectralEPWSolver(cfg) before branching on terms.epw.solver.
In combined mode it is never stepped, but its setup still ran: a combined
deck with thermal noise, EPW Landau damping and collisions off and
terms.light.absorption > 0 (the combined field's damping, which the
combined noise is balanced against) was refused with "thermal needs EPW
damping". self.epw is now None in combined mode; nothing else reads it
there.

Test: test_combined_thermal_noise_needs_only_the_raman_absorption
(fails before with that error).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With terms.light.coupling: rotation the fd CoupledLight takes the SRS
exchange out of the staggered RHS and applies it as a pointwise rotation;
couple() never applied terms.light.transverse_source, whose default is
true, so the projection was silently skipped. The fd solver now refuses
rotation + SRS + transverse_source: true and names the alternatives:
coupling: explicit or terms.light.solver: spectral keep the projection,
transverse_source: false (set explicitly) reproduces what fd rotation
runs did so far. The spectral solver keeps its fields transverse and is
unaffected.

Tests: test_fd_rotation_refuses_the_transverse_source_projection_it_cannot_apply
(fails before: did not raise). The rotation configs of
test_light_coupling.py and test_energy_ledger.py's srs+pump_depletion case
now set transverse_source: false explicitly (their behaviour is unchanged;
they were built with the default before and are refused without it).

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

With save.fields.x.dx / save.fields.y.dy set, the interpolated field save
(get_save_quantities -> save_func, make_field_xarrays) was wrong in four
ways (measured on ce5d3fe: 6.4 x 1.6 um TPD box, 0.05 um grid, 0.08 um
save spacing, 20 fs, compared against the full-grid save of the same run
sampled at the save points):

1. The save grid was not coarser. It kept the simulation's nx x ny
   points, spread between xmin + dx_save/2 and xmax - dx_save/2, so
   dx_save only set the margins.
2. The saved epw state is the k-space potential phi_k (FFT-ordered), but
   it went through the same x-space interpolator as the real-space
   fields, and make_field_xarrays then read the result as k-space on
   kx = fftfreq(nx, d=dx_save). The saved phi, ex and ey differed from
   the full-grid save by 68 %, 67 % and 73 % of their maximum, and the
   saved k axis was scaled by dx / dx_save (x0.625).
3. With dx_save below the grid spacing, the outermost save points fell
   outside the outermost cell centres, where interpax returns NaN.
4. E0, E1 and the IAW fields were interpolated correctly, but onto the
   wrong (non-coarsened) grid.

Fix:
- The save grid has n_save = round(L / dx_save) cell-centred points per
  axis, with spacing L / n_save (_interpolated_save_axis). A save grid
  with more points than the simulation grid raises a ValueError at
  setup. Giving only one of save.fields.x / save.fields.y raises
  NotImplementedError (x alone already did; y alone was silently
  ignored).
- The save function (_interpolated_field_save) transforms phi_k to
  x-space with the solver's convention (phi = ifft2(phi_k),
  E = ifft2(-i k phi_k)) on the full grid and saves phi, ex and ey on the
  save grid. E0, E1 and the real fields are interpolated as before. ex
  and ey come from the full grid rather than from a spectral derivative
  on the save grid, because that derivative would scale the
  interpolation error by k and assign the wrong k to content above the
  save grid's Nyquist wavenumber.
- make_field_xarrays, interpolated branch: the x-space ex and ey carry
  the full-grid save's 4 / (nx * ny) factor with the simulation nx, ny.
  The k-space fields (phi, ex, ey, IAW) are the DFT on the save grid, on
  its own k axes, scaled by (nx * ny) / (nx_save * ny_save) so that a
  resolved mode keeps its full-grid coefficient.
- The full-grid save path is unchanged. fields.xr and k-fields.xr of a
  full-grid run are bitwise identical to ce5d3fe (x64 and float32, 2D and
  quasi-1D, TPD+SRS+IAW box).

Behavior change: interpolated saves now have n_save points per axis
instead of nx x ny, and the raw diffrax "fields" save holds phi, ex and
ey (x-space, on the save grid) instead of epw.

Tests (tests/test_lpse2d/test_field_save.py; all 6 fail on ce5d3fe):
- test_interpolated_save_matches_full_grid_save[2d, 1d]: noise-seeded
  TPD+SRS+IAW box with a static pump, 6 fs, save spacing 1.6 x the grid
  spacing. Every x-space field equals the full-grid field linearly
  interpolated to the save points, within 2000 eps of its maximum
  (round-off budget derived in the file; measured <= 2.3 eps). Shapes are
  80 x 20 (2D) / 80 x 1 (quasi-1D), save and k axes match the save grid
  to 1e-13, and all values are finite.
- test_interpolated_save_of_band_limited_state[2d, 1d]: a single-mode
  state checked against analytic values: the x-space phi, ex, ey, E0, E1
  and IAW fields and the mode's k-space coefficient, each within the
  bilinear-interpolation bound sqrt(2) (q h)^2 / 8 plus round-off.
- test_interpolated_save_spacing_bounds[x, y]: half the grid spacing
  raises. Equal spacing reproduces the grid axis bitwise and gives finite
  output equal to linear interpolation within 2000 eps. Giving x or y
  alone raises.

Docs: docs/source/solvers/lpse2d/config.md describes the save grid, the
spacing bound, the interpolation and the normalizations.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
physicistphil added a commit to physicistphil/adept that referenced this pull request Oct 20, 2026
…fault

On plan2 terms.light.transverse_source defaults to false (LPSE
takeTransversePartOfSourceTerms), so the refusal merged from ergodicio#372 only
fires for an explicit true; the message and the coupling docs no longer
call true the default, and they name terms.light.transverse_fields: true
(applied after the sub-steps under either coupling) as the fd way to keep
the light transverse with rotation. The test checks the plan2 default is
accepted, true refused, rotation + transverse_fields and explicit + true
accepted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@ergodic-reviewer ergodic-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes for two postprocessing correctness defects detailed inline: combined-mode EPW dissipation uses the separate solver's collision model, and HPE inverse-Landau growth produces NaNs in the default energy diagnostics.

Postprocessing is implemented and reachable through BaseLPSE2D.post_process and the ergoExo artifact lifecycle, including field/series datasets, plots, derived metrics, optional flux/Thomson/ledger/HPE diagnostics, and checkpoints. The findings affect the scientific interpretation of the saved heating and energy-budget results. The latest separate-EPW initialization and FD rotation-validation fixes are present with regression tests in source.

Validation: source review of the diff, relevant implementations/call sites, tests, and repository guidance at 55fd66fad6e042749b99888ff7c6ca4b25ad41ad. The last GitHub check read reported test-lpse2d in progress. I did not execute tests, benchmarks, or postprocessing, or inspect the external LPSE reference artifacts.

Comment thread adept/_lpse2d/helpers.py
gamma_total = np.zeros_like(k_sq)
gamma_total = gamma_total + derived.get("nu_coll", 0.0) * zero_mask
# the same static rate (form, threshold, multiplier) the solver applies
gamma_total = np.array(analytic_landau_rate(cfg)) + derived.get("nu_coll", 0.0) * zero_mask

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Use the combined solver's damping in the dissipation diagnostic

get_default_save_func still builds epw_dissipation from the separate EPW collision rate, but CombinedSolver explicitly ignores nu_coll and damps the longitudinal field with nu_raman * (n_over_env + iaw_density)**2. For a combined run with EPW Landau damping/collisions off and terms.light.absorption > 0 (already a supported case in test_combined.py), this diagnostic reports zero dissipation even while a nonzero longitudinal wave loses energy. Conversely, enabling EPW collisions can report absorption that the combined solver never applies. diagnostics.series_metrics integrates this series into electron_energy_final and uses it for laser_absorbed_frac_epw, so the exported heating/budget results are incorrect. Please select the active solver's damping here, including its density dependence, and cover a longitudinal combined-field absorption case through the save function.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9119fd2 (#372); merged into plan2 (d9f0d46, with the plan2 adaptations below) and #387 (1467202).

In combined mode, get_default_save_func now builds epw_dissipation from the damping the combined solver actually applies:

  • the Landau rate in k-space (y["gamma_L"] with HPE), with no nu_coll;
  • plus the x-space collisional loss (1 − exp(−2 ν_R (n/n_env + δn)² dt))/dt on the EPW energy density.

combined_collision_rate(cfg) is now the single source of ν_R for both CombinedSolver and the diagnostic. On the test box, the pre-fix diagnostic reported 1.67×10⁷ against 2.95×10⁹.

Same class, also fixed:

  • The IAW thermal-filamentation lw heating used 2 ν_coll n/n_env in combined mode as well. It now uses the combined damping 2 ν_R (n/n_env)², the raman term's coefficient on the same field, and requires terms.light.absorption.
  • On plan2, where the combined field has its own absorbing layer, epw_boundary_loss now uses grid.combined_absorbing_boundaries in combined mode (8bd768f; the EPW layer gave 1.23×10⁹ against 7.10×10⁹).

Plan2 adaptations in the merge:

  • ν_R follows raman_absorption.
  • δn enters through the Raman class's IAW feedback factor, as plan2's combined solver does.
  • thermal_filamentation.lw stays dropped in combined mode (LPSE); the test there checks that the heating does not depend on the EPW collisions.

Tests:

  • test_combined.py::test_combined_dissipation_diagnostic_uses_the_combined_damping, through the save function, with Landau off (EPW collisions on) and on. Exact identities against 2(2 − n/n_env)·L_c·epw_energy plus the Parseval Landau term, rtol 1e-12.
  • test_thermal_filamentation.py::test_lw_heating_in_combined_mode_uses_the_combined_damping.
  • On plan2, test_combined_boundary_loss_diagnostic_uses_the_combined_layer.

All fail before the fix.

Comment thread adept/_lpse2d/core/hpe.py
gamma_hpe = jnp.maximum(self.calibration * self._gamma_raw(hist), 0.0)
# LPSE gammaLimit: clip(-growth, gamma, damping); growth only with allow_growth
gamma_hpe = jnp.clip(
self.calibration * self._gamma_raw(hist), -self.gamma_limit_growth, self.gamma_limit_damping

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Make the default energy diagnostics support inverse-Landau growth

The new allow_growth path permits negative gamma_L (as test_gamma_limits_and_allow_growth demonstrates), but helpers.get_default_save_func still computes loss_factor = (1 - exp(-2 * (gamma_L + nu_coll) * dt)) / dt followed by the real jnp.sqrt(loss_factor). With collisions off, any negative-rate mode therefore introduces NaNs into the inverse FFTs and epw_dissipation, even when the simulation fields remain finite. Those NaNs propagate to the saved series, electron-energy integral and absorbed-energy metrics. Please handle signed energy transfer in the diagnostic, rather than feeding negative rates into the positive-loss square-root construction, and extend the growth regression through the default save/postprocessing path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was already fixed when this comment was posted. ca6ced8 on this branch splits the loss factor into L⁺ = max(L, 0) and L⁻ = max(−L, 0); the density is |ifft(√L⁺ Eₖ)|² − |ifft(√L⁻ Eₖ)|², so epw_dissipation is the signed net transfer and its box integral is exactly Σₖ Lₖ|Eₖ|². The windowed branch on #387 got the same treatment in 359f9cd.

Regressions through the default save path: test_hpe.py::test_dissipation_diagnostic_is_signed_with_allow_growth and test_hpe_windows.py::test_windowed_dissipation_is_signed_with_allow_growth. They use mixed-sign rates against the Parseval closed form at rtol 1e-12, and check that non-negative rates are bitwise identical with and without allow_growth. Both were NaN before; see also the #387 thread on the same defect.

physicistphil and others added 2 commits October 19, 2026 23:38
get_solver_quantities intersects grid.low_pass_filter_grid with
terms.epw.max_wavenumber (LPSE lw.maxWavenumber), and
SpectralRamanLight._light_band (also SpectralCoupledLight's band) started
from that mask, so an EPW-only cap zeroed pump/Raman modes above it on
every propagation sub-step. LPSE's LightSolver::evolveSpectral uses
lw.maxWavenumber only in combined mode (min of the two caps,
LightSolver.cpp:3689). The grid now keeps light_low_pass_filter_grid (the
anti-aliased band before the EPW cap) and the separate light solvers build
their band from it, then apply terms.light.max_wavenumber. The combined
solver keeps the EPW-capped band, as LPSE.

Test: test_light_band_is_independent_of_the_epw_cutoff (both separate
spectral light classes; EPW cap 0.5 k0 changes no light mode, the light
cap still acts). Before the fix 182 of 400 light-band modes differed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
terms.hpe.allow_growth lets the applied rate gamma_L + nu_coll go
negative (inverse Landau damping), but get_default_save_func built the
dissipation density from sqrt((1 - exp(-2 rate dt)) / dt), which is NaN
there; the NaN reached epw_dissipation, electron_energy_final and the
absorbed-energy metrics while the fields stayed finite. The loss factor is
now split into its positive and negative parts, the gain modes entering
with a minus sign, so the box integral stays sum_k L_k |E_k|^2 and
epw_dissipation is the signed net transfer. The negative-part transform
runs only with allow_growth; every other run's series is unchanged.

Test: test_dissipation_diagnostic_is_signed_with_allow_growth (mixed-sign
rates through the default save function vs the Parseval closed form,
rtol 1e-12; non-negative rates bitwise identical with and without
allow_growth). NaN before the fix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
physicistphil added a commit to physicistphil/adept that referenced this pull request Oct 20, 2026
The windowed branch of get_default_save_func took
sqrt((1 - exp(-2 gamma_w dt)) / dt) of each window's applied rate, which
is NaN for the negative rates terms.hpe.allow_growth permits. It now
splits each window's loss factor into its positive and negative parts as
the single-window branch does (merged from ergodicio#372), so the box integral is
sum_j sum_k L_jk |chunk_jk|^2 and epw_dissipation is the signed net
transfer; the negative-part transforms run only with allow_growth.

Test: test_windowed_dissipation_is_signed_with_allow_growth (3 windows,
mixed-sign rates vs the Parseval closed form, rtol 1e-12; non-negative
rates bitwise identical with and without allow_growth). NaN before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@ergodic-reviewer ergodic-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes for two correctness issues:

  • The previously reported combined-mode dissipation defect remains: get_default_save_func uses the separate EPW collision model instead of the combined field's Raman absorption and density dependence. A longitudinal combined wave can lose energy while epw_dissipation reports zero, corrupting the derived heating/budget metrics. The existing thread retains the requested fix; no duplicate inline comment added.
  • HPE energy-conservation rescaling can exceed the configured damping/growth limits before handing gamma_L to the solver; detailed inline.

The signed inverse-Landau dissipation fix and separation of the light/EPW wavenumber caps are present with regression tests in source.

Postprocessing is implemented and reachable through BaseLPSE2D.post_process and the ergoExo artifact lifecycle, including field/series datasets, plots, derived metrics, optional flux/Thomson/ledger/HPE diagnostics and checkpoints. The combined dissipation issue is an output-correctness problem, not an absent analysis path.

Validation: source review at ca6ced826cfedc9815253f6d578d876b55fefc8d. The last GitHub check read reported test-lpse2d in progress. I did not execute tests, benchmarks or postprocessing, or inspect the external LPSE reference artifacts.

Comment thread adept/_lpse2d/core/hpe.py
gamma_hpe = jnp.maximum(self.calibration * self._gamma_raw(hist), 0.0)
# LPSE gammaLimit: clip(-growth, gamma, damping); growth only with allow_growth
gamma_hpe = jnp.clip(
self.calibration * self._gamma_raw(hist), -self.gamma_limit_growth, self.gamma_limit_damping

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Apply the HPE rate limits after energy-conservation rescaling

These bounds only constrain the rate returned by damping(). In HybridParticleEvolution.__call__, feedback() subsequently multiplies that rate by mult[0] when energy_conservation is enabled, with no final clipping. ld_multiplier() permits values above one (its regression test explicitly exercises this), so a resonant mode already at gamma_limit_damping is written to y["gamma_L"] above that limit; the negative bound can likewise be exceeded with allow_growth. Both EPW solvers consume this state directly in their damping exponential. Thus enabling the energy-conservation control defeats the configured bounds on the applied kinetic damping/growth, changing the wave evolution despite those limits.

Please enforce the configured bounds on the final feedback rate, preserving the intended treatment of non-resonant modes, and cover the combined controls through __call__. The current limit test exercises damping() alone and the multiplier test exercises ld_multiplier() alone, so neither checks the rate actually handed to the solver.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 971f999 (#372); merged into plan2 (a694ba0, inside plan2's running-average multiplier) and #387 (56e4c86).

After the energy-conservation multiplier, HybridParticleEvolution.limit_resonant clips the resonant modes again to [−gamma_limit_growth, gamma_limit_damping]. The non-resonant modes keep the multiplier times their analytic rate, with no clip. The rate handed to the solver therefore respects the configured limits.

This departs from LPSE, which clips only before its LD_multiplier (ElectronTracker.cu:2792; LwSolver.cpp:823 and LightSolver.cpp:3707 apply LD_multiplier · LDgammaE). Where a limit binds, the energy balance the multiplier enforces no longer holds exactly; the docs rows for both limits say so. Windowed HPE is unaffected: n_windows > 1 with energy_conservation is already refused at setup.

Regression through __call__: test_hpe.py::test_gamma_limits_bound_the_rate_after_the_energy_conservation_multiplier. The multiplier is held at 5 by a 10⁶-step running average started from 5, so the limit binds. The test checks:

  • the applied rate is clip(5 × rate) on the resonant modes and 5 × rate elsewhere (rtol 1e-12);
  • the limit holds on the resonant modes.

Before the fix 63 of the 160 modes exceeded the limit. A test added earlier in this round pinned LPSE's order instead; it is replaced.

…bined damping

The combined solver damps its field with the Raman absorption
nu_R (n/n_env + dn)^2 in x-space (plus the Landau rate on the
longitudinal part) and ignores the EPW collisions, but
get_default_save_func built epw_dissipation from the separate solver's
Landau + nu_coll in k-space, so a combined run with EPW collisions off
reported only the Landau loss and one with collisions on an absorption
the solver never applies (measured on the test box: 1.67e7 reported vs
2.95e9). In combined mode the diagnostic now uses the Landau rate (or the
HPE gamma_L) in k-space plus the x-space collisional loss
(1 - exp(-2 nu_R (n/n_env + dn)^2 dt)) / dt on the EPW energy density.
combined_collision_rate(cfg) is the one source of nu_R for the solver and
the diagnostic.

Same class: the IAW thermal-filamentation lw heating used
2 nu_coll n/n_env in combined mode too; it now uses the combined
damping 2 nu_R (n/n_env)^2 (the raman term's coefficient on the same
field) and needs terms.light.absorption.

Tests: test_combined_dissipation_diagnostic_uses_the_combined_damping
(through the save function, Landau off and on, exact identities, rtol
1e-12), test_lw_heating_in_combined_mode_uses_the_combined_damping. Both
fail before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
physicistphil added a commit to physicistphil/adept that referenced this pull request Oct 20, 2026
…) into lpse2d/lpse-parity-plan2

Resolved toward plan2, with these plan2 adaptations:
- CombinedSolver.nu_raman (plan2's raman_absorption semantics) now comes
  from combined_collision_rate, the diagnostic's source too.
- The combined dissipation term takes the IAW perturbation through the
  Raman class's feedback factor (n/n_env + dn f_R), as plan2's combined
  solver; epw_boundary_loss uses the combined field's own absorbing layer
  (combined_absorbing_boundaries) in combined mode.
- thermal_filamentation.lw stays dropped in combined mode (LPSE); the
  merged ergodicio#372 test is replaced by one that checks the combined heating
  does not depend on the EPW collisions.
- The combined dissipation test uses terms.light.raman_absorption.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
physicistphil and others added 2 commits October 20, 2026 12:30
…rvation multiplier

With terms.hpe.energy_conservation the solver applies the multiplier
times the clipped HPE rate, so the applied rate can exceed
gamma_limit_damping (or -gamma_limit_growth) when the multiplier is above
one. That is LPSE's order: ElectronTracker.cu:2792 clips LDgammaE, and
LwSolver.cpp:823 / LightSolver.cpp:3707 apply LD_multiplier * LDgammaE
with no second clip; clipping again would undo the energy balance the
control enforces. The docs called the limits clips on the "applied" rate;
they now say what is clipped and in which order.

Test: test_energy_conservation_multiplier_scales_the_clipped_rate_like_lpse
pins the order through __call__ (gamma_L handed to the solver = the
reported multiplier, here non-trivial, times the clipped rate; rtol 1e-12).

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

With terms.hpe.energy_conservation the rate handed to the EPW solvers is
the multiplier times the clipped HPE rate; with a multiplier above one a
resonant mode at gamma_limit_damping (or -gamma_limit_growth with
allow_growth) was applied beyond the configured limit. USER decision
2026-10-20 (PR ergodicio#372 review): the limits bound the applied rate, so the
resonant modes are clipped again after the multiplier
(HybridParticleEvolution.limit_resonant); the non-resonant modes keep the
multiplier times their analytic rate. This departs from LPSE, which clips
only before its LD_multiplier (ElectronTracker.cu:2792, LwSolver.cpp:823),
and the energy balance the multiplier enforces then holds only where the
limits do not bind. Docs updated (supersedes 3e8f4f9's wording).

Test: test_energy_conservation_multiplier_scales_the_clipped_rate_like_lpse
(added in 3e8f4f9 to pin LPSE's order) is replaced by
test_gamma_limits_bound_the_rate_after_the_energy_conservation_multiplier:
through __call__ with the multiplier held at 5 (a 1e6-step running
average from 5), the applied rate is clip(5 x rate) on the resonant modes
and 5 x rate elsewhere, and the limit binds (rtol 1e-12). Fails before
(63/160 modes above the limit).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@ergodic-reviewer ergodic-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes:

  • The previously reported HPE rate-limit defect remains at this head. HybridParticleEvolution.__call__.feedback multiplies the already-clipped rate by the energy-conservation multiplier without enforcing the bounds again. With energy_conservation enabled, the gamma_L consumed by the wave solver can exceed the configured damping/growth limits. Please enforce the limits on the final feedback rate; the existing thread retains the details, without a duplicate inline comment.
  • The new required HPE instrument entries also break the existing test_oneil_flattening slow regression: its manually constructed state omits those entries, causing a KeyError before it exercises nonlinear feedback. Details and the caller update are inline.

The combined-mode dissipation and thermal-filamentation coefficient fixes are present, with regression tests in source. Postprocessing is implemented and reachable through BaseLPSE2D.post_process and the ergoExo artifact lifecycle, including field/series datasets, derived metrics, optional flux/Thomson/ledger/HPE diagnostics and checkpoints.

Validation: source review at 9119fd2ab19f077c2aeb0e4d0b7f4ee7a9dd7189. I did not execute tests, benchmarks or postprocessing, or inspect the external LPSE reference plots/artifacts.

Comment thread adept/_lpse2d/core/hpe.py
# ---------------------------------------------------------------- driver --

def __call__(self, t: float, y: dict[str, Array]) -> dict[str, Array]:
instruments = (y["hpe_wall_flux"], y["hpe_cone_energy"], y["hpe_ld_multiplier"])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Update the existing trapping regression for the new required state

tests/test_lpse2d/test_hpe.py::test_oneil_flattening still constructs y with only epw, x_e, u_e, epw_hist, and gamma_L, then calls jax.jit(hpe.__call__). These new unconditional lookups therefore raise KeyError('hpe_wall_flux') on its first call, before the test can exercise trapping or check damping reduction. Unlike the other HPE test callers updated for the expanded push results, this caller was not migrated to the expanded state contract.

Initialize that test from the complete load_particles(cfg) state (including hpe_wall_flux, hpe_cone_energy, and hpe_ld_multiplier) so the existing slow nonlinear-feedback regression remains runnable. This conclusion is from tracing the caller and callee; I did not execute the test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 86606c8: test_oneil_flattening now starts from the complete load_particles state (wall-flux, cone-energy and multiplier instruments included) with the driven wave. Its O'Neil assertion is unchanged and holds (~19 s).

Running the whole slow set (deselected by the default -m 'not slow', and so by CI) turned up two more:

  • test_srs_smoke_with_hpe had failed since 1989b5e, also on ergodicio/main.
    • Its all-finite check rejected the t = 0 sample of hpe_gamma_ratio_kpeak, which is NaN by design while the EPW has no band energy; the check now exempts only that sample (86606c8).
    • Its ratio[-1] > 0.5 check was a coin flip at 20k particles with an unseeded EPW noise source: seeds 1–10 gave 0.0–2.39, with 4/10 passing. Per the repo's rule against picking seeds after seeing results, the criterion stayed > 0.5, and 1M particles plus noise seed 7919 were fixed before validation (df43c9b). On fresh seeds 7919 and 11–16 the final ratio was 0.59–1.33 on this branch and 0.74–1.26 on plan2, 7/7 and 6/6 passing.
  • test_linear_closure failed on plan2/feat(lpse2d): spatially windowed electron distribution for HPE (stacked on #378) #387. It measured against the MATLAB landau_damping_rate instead of the configured analytic_landau_rate(cfg), which HPE calibrates to (4d193ad); tolerance unchanged.

The slow set now passes on #372 (3/3), plan2 (3/3) and #387 (5/5).

Recorded, not changed: test_linear_closure's margin is thin. Over HPE seeds 42/1/2/3 the measured/analytic ratio was 0.91/0.83/0.89/1.08 here and 0.87/0.80/0.81/0.96 on plan2. Seed 42 at 4M particles gave 0.866, the same as at 1M (0.868), so the shortfall is not particle shot noise.

@ergodic-reviewer ergodic-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes for the remaining HPE test-state regression (P2). At this head, tests/test_lpse2d/test_hpe.py::test_oneil_flattening still builds y without hpe_wall_flux, hpe_cone_energy, or hpe_ld_multiplier, while HybridParticleEvolution.__call__ reads all three unconditionally. Its first jitted call therefore raises KeyError('hpe_wall_flux') before exercising nonlinear trapping/feedback. Initialize the test from the complete load_particles(cfg) state and override epw, as the new feedback regression does. The existing inline thread retains the finding; no duplicate comment added.

The HPE rate-limit fix is present: feedback now calls limit_resonant after energy-conservation rescaling, with a regression through __call__ and documented treatment of non-resonant modes.

Postprocessing is implemented and reachable through BaseLPSE2D.post_process and the existing ergoExo lifecycle: field/series datasets, plots, derived metrics, optional flux/Thomson/ledger/HPE diagnostics, and checkpoint persistence. Combined-field projection, carrier selection, and dissipation handling have implementation and regression coverage in source.

Validation: source review at 971f999d9bae2f349b8f29ce182f8e669398130c; oversized helper changes were inspected through constituent commit patches. I did not execute tests, benchmarks, or postprocessing, or inspect external LPSE reference plots/artifacts.

physicistphil and others added 5 commits October 20, 2026 13:23
make_field_xarrays wrote the x-space EPW fields of fields.xr as
ex = ifft2(-i kx phi_k) / nx / ny * 4 (ey likewise), while the saved phi
is ifft2(phi_k) unscaled and the solver's field is E = ifft2(-i k phi_k)
(SpectralEPWSolver.phi_k_to_e_fields; the IAW ponderomotive term, the TPD
pump depletion and the e_sq / epw_energy / epw_dissipation series use the
same unscaled E). fields.xr ex/ey were therefore smaller than the field
the solver evolves, and than -grad of the saved phi, by 4 / (nx ny):
1/1024 on a 128 x 32 grid, 1/32 on 128 x 1, ~1/14000 on 512 x 108.
k-fields.xr (phi_k, -i kx phi_k, -i ky phi_k) was already unscaled, so
the x- and k-space datasets disagreed by the same factor.

History: the factor dates from 02e112f (PR ergodicio#20), where the first EPW
solver stored phi in k-space with fft_norm = nx ny / 4 (its get_eh_x
multiplied by nx ny / 4; the save used the reciprocal 4 / (nx ny)).
f77784f ("new units and solver based on matlab code", squashed into
df1510a / PR ergodicio#41) moved the state to the plain numpy FFT convention but
kept the factor on ex/ey, and c641495 (PR ergodicio#142) moved the state to the
k-space phi_k and kept it again. The MATLAB prototype has no such factor;
in the current code it has no physical meaning.

Fix: drop the factor. The x-space ex/ey are ifft2(-i k phi_k), i.e.
-grad(phi) of the saved phi. Both branches of make_field_xarrays lose
it: the full-grid one, and the interpolated (save.fields.x / y) one of
the previous commit, which had applied the same factor with the
simulation nx, ny to its x-space ex/ey so the two saves agreed.
k-fields.xr, phi, the light fields and the IAW fields are unchanged.
Plots in plot_fields / plot_kt use autoscaled colour limits, no
normalization assumed the scaled values. docs/source/solvers/lpse2d/
overview.md now states what the saved EPW entries are.

Tests (tests/test_lpse2d/test_saved_epw_fields.py, 4 tests, all fail on
ce5d3fe; 128 x 32 and quasi-1D 128 x 1 boxes, full-grid save, a single
grid mode plus a random band-limited phi_k, round-off bound 200 eps fixed
before the first run):
- test_saved_epw_fields_single_mode[2d, 1d]: fields.xr phi/ex/ey and
  k-fields.xr phi/ex/ey equal the analytic single mode. On ce5d3fe ex is
  off by 5.9e-3 of a 5.9e-3 field.
- test_saved_epw_fields_are_the_solver_field[2d, 1d]: saved ex/ey equal
  -grad(saved phi) (spectral), SpectralEPWSolver.phi_k_to_e_fields(phi_k)
  and ifft2 of the k-fields.xr ex/ey, and dx dy sum |E|^2 of the saved
  fields equals the e_sq series. On ce5d3fe the relative L2 error is
  0.99902 = 1 - 4/4096 (2D) and 0.96875 = 1 - 4/128 (quasi-1D).
- test_field_save.py::test_interpolated_save_of_band_limited_state now
  expects ex = -i kx phi, ey = -i ky phi on the save grid (factor
  removed from its expected values and docstring; tolerances unchanged).
  test_interpolated_save_matches_full_grid_save needs no change: it
  compares the interpolated save with the full-grid save.

Cherry-picked from fix/lpse2d-saved-epw-field-scale (6b6d278, off
ce5d3fe); the conflict in make_field_xarrays is resolved as above.

Behavior change: fields.xr ex and ey grow by nx ny / 4 relative to every
earlier save (all configs, full-grid and interpolated). phi, k-fields.xr,
series.xr and the logged metrics are unaffected (none read fields.xr
ex/ey).

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

The overview stated that k-fields.xr holds phi_k, -i kx phi_k, -i ky phi_k
and that fields.xr is their ifft2, with ex, ey = -grad(phi). That holds
for a full-grid save only. An interpolated save (save.fields.x / y)
stores the full-grid fields interpolated to the save points, and its
k-fields.xr is their DFT on the save grid scaled by
(nx ny) / (nx_save ny_save); neither ifft2(k-fields.xr) nor a derivative
of the coarse phi reproduces its fields.xr. The paragraph now says so and
links to the save.fields reference in config.md.

Review thread r4331320857 (PR ergodicio#388). Docs only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Both slow-marked tests were broken and unnoticed (the default addopts
deselect them, and so does CI):
- test_oneil_flattening built the HPE state from five keys only;
  HybridParticleEvolution.__call__ reads the wall-flux, cone-energy and
  multiplier instruments unconditionally, so the first call raised
  KeyError('hpe_wall_flux') (PR ergodicio#372 review). It now starts from the
  complete load_particles state with the driven wave; its O'Neil
  assertion is unchanged and holds.
- test_srs_smoke_with_hpe asserted every series finite, but since
  1989b5e hpe_gamma_ratio_kpeak is NaN by design while the EPW has no
  energy in the resonant band (the reduction metrics nanmin over it); the
  EPW starts at zero, so the t = 0 sample was NaN. The check now exempts
  that series' first sample only; every other sample and series must be
  finite, as before. Also failing on ergodicio/main ce5d3fe.

Slow set on this branch: 3 passed (test_linear_closure 55 s, smoke 25 s,
O'Neil 18 s).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…andau rate

test_linear_closure (slow) compared the measured decay with the MATLAB
landau_damping_rate, a hard-coded form. HPE calibrates to, and the solver
applies without HPE, analytic_landau_rate(cfg) (terms.epw.damping.
landau_form and its threshold / multiplier). On this branch the two agree
(32.1498 vs 32.1498 /ps at the driven mode); on plan2 the default LPSE
form gives 21.50 /ps and the test failed against 32.15 (measured 18.68).
The reference is now analytic_landau_rate(cfg); tolerance unchanged (15 %).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
test_srs_smoke_with_hpe (slow) asserted the final hpe_gamma_ratio_kpeak
> 0.5 with 20k particles and an unseeded EPW noise source. That statistic
is shot-noise limited: over noise seeds 1-10 it spanned 0.0-2.39 (4/10
above 0.5), so the test passed or failed by seed (it failed in the round-9
suite run after passing the run before). Following the no-post-hoc-seed
rule: criterion unchanged (> 0.5), n_particles 1M and the noise seed 7919
were fixed before validation; on fresh seeds 7919 and 11-16 the final
ratio was 0.59-1.33 (7/7 pass). The spread at 1M is still about +-40 %;
the docstring records the numbers. Run time 25 s -> 49 s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@ergodic-reviewer ergodic-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No remaining blocking findings identified at df43c9bbd23461db466207b9adf9e0dadd743ddc. The previously reported test_oneil_flattening state regression is fixed: it now carries the complete loaded HPE state, including the instrument entries. The related slow tests use the configured analytic Landau rate and a pinned noise seed with a larger particle population, without relaxing the damping-ratio criterion.

Postprocessing is implemented and reachable through BaseLPSE2D.post_process and the existing ergoExo artifact lifecycle: field/series datasets, plots, derived metrics, optional Poynting/Thomson/ledger/HPE diagnostics, and checkpoint persistence. Combined-field projection, carrier selection and dissipation handling have implementation and regression coverage in source.

Validation: source review of the diff, relevant implementations/call sites, tests and repository guidance. The latest GitHub check read reports test-lpse2d in progress. I did not execute tests, benchmarks or postprocessing, or inspect external LPSE reference plots/artifacts; the reported cross-check results are not independently verified.

physicistphil added a commit to physicistphil/adept that referenced this pull request Oct 21, 2026
…ong statements

The departures section of the lpse2d overview was built from the last
review rounds. A sweep of the configuration reference, code comments,
commit messages and the LPSE sources (2026-10-21) adds the documented
intentional departures it missed (resonance absorption with
landau_update > 1; the QLE resonance Jacobian, Fokker-Planck form,
Landau-rate gradient and sub-cycling / implicit step; FD IAW PPM
upwinding; non-local thermal filamentation outside the IAW band and its
normalisation; spectral injectors; snap_beam_ky; one light time step;
the x-max injector plane; the HPE tracker), lists undocumented
differences found by the audit as not yet decided (KAP groups on the
evolved pump, the IAW amplitude clamp on every solver, the Landau lower
threshold with HPE / QLE, hpe.dampingRateMinFraction, the
energy-conservation gate), the defaults that differ, the translator
restrictions and the adept-only options. The section says it may still
be incomplete. HPE energy conservation is not a departure on this branch
(exponential per-mode loss, secant-solved, as LPSE; only ergodicio#372 keeps the
linearised form).

Statements corrected (checked against the code / LPSE): the default
spectral injector width is half a local wavelength, pi/k (code), not
2 pi/k; LPSE's resonanceAbsorption is the resonance_absorption option,
not unimplemented; landau_multiplier is adept-only (LPSE's LD_multiplier
is the HPE energy-conservation multiplier); KAP beam independence holds
for LPSE's static pump only; the default landau_form is relativistic
(epw.py docstring); LPSE's implicit QLE theta is 0.5 / 0.67, not 1
(qle.py docstring); the overview's absorbing mask names the exp default;
terms.qle.solver added to the QLE row. Docs and docstrings only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
physicistphil and others added 2 commits October 22, 2026 12:09
…into lpse2d/lpse-parity

Brings the interpolated field-save fix and the saved-EPW ex/ey scale fix
(and the main commits under 984f0f6). helpers.py conflicts resolved by
keeping both sides' new helpers; the interpolated save is ergodicio#388's
_interpolated_field_save, wrapped with this branch's combined-mode
Raman-light projection (raman_light_part) and the epw_ledger skip, as the
full-grid save does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…finer than the test grid

After the PR ergodicio#388 merge the interpolated field save refuses a save
spacing finer than the simulation grid (it used to write non-finite edge
rows). Five tests build a 0.1 um grid from tpd.yaml, whose save.fields
x / y spacing is 80 nm / 128 nm, and failed in setup with that refusal:
test_combined_split_step_runs_with_pump_depletion_iaw_noise_and_reports_transverse_light,
test_poynting_of_a_plane_wave_is_group_velocity_times_intensity,
test_thomson_probe_picks_the_mode_in_its_window,
test_ledger_reported_in_epw_energy_units,
test_ledger_off_leaves_state_and_series_unchanged.
They test diagnostics on full-grid states (the old interpolated save kept
nx x ny points, so those happened to fit), so their configs now drop
save.fields.x / y; no assertion changed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@ergodic-reviewer ergodic-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes for the P2 output failure detailed in the attached comment: an accepted interpolated save grid with one x point crashes Poynting postprocessing after the simulation, before series/checkpoint writing and result-artifact export. Validate that combination at setup or compute flux before resampling.

Postprocessing is implemented and reachable through BaseLPSE2D.post_process and the ergoExo artifact lifecycle: field/series datasets, plots, derived metrics, optional Poynting/Thomson/ledger/HPE diagnostics, and checkpoints. The merged field-save fixes preserve the combined-field projection and ledger exclusion; full-grid and interpolated field regressions are present in source. The finding is a specific output-compatibility failure, not an absent analysis path.

Validation: source review at a85fbd5a77d67675c3fe9f6c82f57046ac65521e, including the diff, relevant implementations/call sites, tests and repository guidance; oversized helper changes were inspected through constituent commit patches. The last GitHub check read reported test-lpse2d in progress. I did not execute tests, benchmarks or postprocessing, or inspect external LPSE reference plots/artifacts.

Comment thread adept/_lpse2d/helpers.py

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Validate singleton x save grids before computing Poynting maps

The new _interpolated_save_axis permits a single saved x point (n_save = max(1, ...)), for example when save.fields.x.dx is the box length. Combining that accepted save configuration with save.fields.poynting: true fails after the solve: make_field_xarrays unconditionally calls np.gradient(e, dxs, axis=1), which requires at least two samples along x. The dxs = 1.0 fallback does not make a singleton axis differentiable. Unlike the y derivative, the x derivative has no size guard.

This exception occurs before post_process writes the series and checkpoint, and before ergoExo uploads the result artifacts, so this output choice prevents a completed run from exporting its results. Please either reject singleton x save grids during setup when Poynting maps are enabled, or compute the flux on the simulation grid before sampling it onto the save grid. Add a regression for this combination; the existing Poynting tests exercise only the full-grid path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a62b52c. get_save_quantities now refuses save.fields.poynting: true together with an interpolated save of fewer than two x points, at setup rather than after the solve; two or more points are accepted. Computing the flux before resampling would also work, but the maps would then need their own save path.

Regression: test_diagnostics_lpse.py::test_poynting_refuses_a_single_point_x_save_grid. dx equal to the box length is refused; half the box (2 points) is accepted. Before the fix it did not raise. Docs: the save.fields.poynting text. Merged into plan2 (04b432a) and #387 (2239faa).

PR ergodicio#388's interpolated save accepts a save grid of one x point
(n_save = max(1, round(L / dx_save)), e.g. save.fields.x.dx the box
length). With save.fields.poynting the flux maps take d/dx on the save
grid, and np.gradient raised in make_field_xarrays after the solve,
before the series, checkpoint and artifacts were written (PR ergodicio#372
review). get_save_quantities now refuses that combination at setup;
two or more x points are accepted. Docs: the save.fields.poynting text.

Test: test_poynting_refuses_a_single_point_x_save_grid (dx = box length
refused, half the box accepted with 2 points). Before: did not raise.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@ergodic-reviewer ergodic-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No remaining material findings identified at a62b52c93b4a40e7fe9a57346569f207d66e4a7d. The previously reported single-point Poynting-save failure is now rejected during setup in get_save_quantities, before the solve. The regression covers rejection of one saved x point and acceptance of two, and the restriction is documented.

Postprocessing is implemented and reachable through BaseLPSE2D.post_process and the existing ergoExo artifact lifecycle: field/series datasets, plots, derived metrics, optional Poynting/Thomson/ledger/HPE diagnostics, and checkpoint persistence. The interpolated-save path retains the combined-field Raman projection and excludes the series-only energy ledger; full-grid and interpolated field regressions are present in source.

Validation: source review of the diff, relevant implementations/call sites, tests and repository guidance; oversized helper changes were inspected through constituent commit patches. The latest GitHub check read reports test-lpse2d in progress. I did not execute tests, benchmarks or postprocessing, or inspect external LPSE reference plots/artifacts; the reported cross-check results are not independently verified.

This branch has not been deployed

No deployments
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