Repository navigation
feat(lpse2d): parity with the original LPSE (deck translator, term/solver/kinetic parity, cross-checks) - #372
physicistphil wants to merge 55 commits into
Conversation
…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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
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 %).
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
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.
| 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"]), |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
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.
…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>
…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>
There was a problem hiding this comment.
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.
| @@ -25,15 +26,42 @@ def __init__(self, cfg): | |||
| self.wp0 = cfg["units"]["derived"]["wp0"] | |||
| self.epw = epw.SpectralEPWSolver(cfg) | |||
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
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).
| 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) |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
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.
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>
…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>
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
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 nonu_coll; - plus the x-space collisional loss
(1 − exp(−2 ν_R (n/n_env + δn)² dt))/dton 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
lwheating used2 ν_coll n/n_envin combined mode as well. It now uses the combined damping2 ν_R (n/n_env)², theramanterm's coefficient on the same field, and requiresterms.light.absorption. - On plan2, where the combined field has its own absorbing layer,
epw_boundary_lossnow usesgrid.combined_absorbing_boundariesin 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.lwstays 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 against2(2 − n/n_env)·L_c·epw_energyplus 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.
| 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 |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
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.
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>
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>
There was a problem hiding this comment.
Request changes for two correctness issues:
- The previously reported combined-mode dissipation defect remains:
get_default_save_funcuses the separate EPW collision model instead of the combined field's Raman absorption and density dependence. A longitudinal combined wave can lose energy whileepw_dissipationreports 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_Lto 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.
| 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 |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
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>
…) 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>
…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>
There was a problem hiding this comment.
Request changes:
- The previously reported HPE rate-limit defect remains at this head.
HybridParticleEvolution.__call__.feedbackmultiplies the already-clipped rate by the energy-conservation multiplier without enforcing the bounds again. Withenergy_conservationenabled, thegamma_Lconsumed 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_flatteningslow regression: its manually constructed state omits those entries, causing aKeyErrorbefore 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.
| # ---------------------------------------------------------------- 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"]) |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
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_hpehad 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.5check 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.
- Its all-finite check rejected the t = 0 sample of
test_linear_closurefailed on plan2/feat(lpse2d): spatially windowed electron distribution for HPE (stacked on #378) #387. It measured against the MATLABlandau_damping_rateinstead of the configuredanalytic_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.
There was a problem hiding this comment.
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.
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>
There was a problem hiding this comment.
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.
…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>
…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>
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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.
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: LPSElpse.parmstranslator (translate_parms,parse_parmswith#include), ZAK unit system,read_metrics/read_framesfor LPSE output files, CLI.terms.epw.energy_ledger) with per-operation channels and closure.Term parity (B, B')
tpd_form: lpse), transverse TPD depletion, Landau-damping options (form, threshold, multiplier, relativistic 2-D Bessel rate), LPSE exponential absorbers,max_wavenumbercaps, rectangular dealiasing, LPSE density profiles (lpse-<shape>: linear, exp, gaussian, inverse-power, quadratic, qd, gd, file).terms.iaw.thermal_filamentation).Solvers (C)
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).terms.epw.solver: combined).drivers.E0.angle), beamlets (beams), transverse super-Gaussian, Kubo-Anderson bandwidth, pulse-shape file.Kinetics, workflow, diagnostics (D)
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.save.checkpoint,restart.file), bit-exact resume.adept._lpse2d.threshold).save.fields.poynting) and Thomson probes (save.thomson).Cross-checks against LPSE reference runs (translated decks)
v_min: 0.05)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