Repository navigation
feat(lpse2d): plan 2 of LPSE parity -- three-component fields, light options, quasilinear and FD-IAW modules (stacked on #372) - #378
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
… 2 P.0, P.2) adept/_lpse2d/parity/ makes the 2026-09-14 cross-check tooling importable with every machine path an argument: - reference.py: `lpse_root()` resolves the original-lpse checkout from $LPSE_ROOT or a sibling of the repository / its worktree parents; `reference_run_dir(deck)` returns `<root>/runs/<deck>`, else the download cache, else fetches the `lpse_reference/` artifact tree of the deck's run on MLflow `lpse-parity` (tag lpse_reference_deck) with boto3 into $LPSE_REFERENCE_CACHE (~/.cache/adept/lpse-reference). - harness.py: `translate_deck` / `run_deck` (through ergoExo), `growth_rate` and `compare` (the exact log_lpse_reference.py metrics and key names), `log_reference`, `read_flux` / `hpe_wall_power` / `compare_hpe_flux` (compare_m5.py), the baseline fit windows per deck (DEFAULT_WINDOWS) and per-deck overrides. - __main__.py: `python -m adept._lpse2d.parity run|compare|log|fetch|verify`. `verify` recomputes a stored cross-check run's ratio metrics from its own binary/series.xr and lpse_reference/lpse.metrics artifacts: test_006 (bf56ee6c), test_010 (13665c22) and test_032 (c7310b80) reproduce all 40 stored values to <= 8e-15 relative. tests/test_lpse2d/test_lpse_deck.py and test_oblique_pump.py use the resolver instead of the hard-coded blackbox path; with the local tree hidden the reference-run tests run from the MLflow download. Harness tests cover the metric names/values on synthetic series, the resolver, and LPSE-vs-itself ratios of exactly 1 on the test_006 reference. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…, Ny) layout (plan 2 P.1) LPSE writes field files in C order (Nx, Ny, Nz) with the last index fastest; the reader assumed x fastest and transposed every 2-D frame (reported in the 09-15 C++ backward-pump note). Verified on runs/test_025 (40 x 5 um, 720 x 90, downSampleFactors 1 4): under the C-order layout the lpse.pots.downSample_4 frame equals pots[::4, ::4] of the full frame (normalized inner product 1.0000; 0.49 under the old layout), the 2 um lw.Labc skirts along x lower <|phi|> by 17 % in the outer 36 cells of axis 0, and the field is isotropically smooth (mean |d/dx| = mean |d/dy|) whereas the old layout was twice as rough along "y". The plan's stated x-signature (a density gradient) does not apply to this deck — NminOverNc = NmaxOverNc = 0.2 — so the absorber skirts and the downsample cross-check are the evidence. Nz > 1 now returns (Nx, Ny, Nz). The `density.basis: lpse-file` reader in helpers.py drops its compensating transpose. The 09-14 M1-M5 cross-checks used only lpse.metrics and are unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…plan 2 N.5) tests/test_lpse2d/test_gradients.py differentiates the final EPW energy of a 64 x 16 (40 nm cells, k lambda_D <= 0.37 so the SRS-resonant EPW is resolved), 50-step SplitStep run with respect to the pump intensity multiplier (args.drivers.E0.intensities, the UniformDriver control) with jax.grad, and compares with a central finite difference of the same jitted function (rtol 1e-2 as the plan asks; the measured agreement is <= 2e-7 relative on every path, FD converged between h and h/4): separate/fd, separate/spectral, separate/static (prescribed pump), combined/spectral (TPD + SRS, pump depletion), combined/spectral + spectral IAW, separate/spectral + thermal noise (noise_amplitude 1e-12 so the noise-driven energy is ~2x the seed, not 1e16x), separate/spectral + HPE. No path needed an xfail: the HPE push is differentiable as written (the histogram and the wall / energy bins are piecewise constant, so the derivative through gamma_L is identically zero and the gradient is the one with gamma_L frozen at the value the push produced). The HPE test also checks that the HPE objective differs from the analytic-damping one (0.94 %), i.e. the feedback is in the graph. The pump envelope is shifted so the pump is fully on from t = 0 (srs.yaml's would be on its rising edge); sensitivities d ln W / d ln s are 3e-4 to 2.5e-2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nt (plan 2 N.3) terms.epw.source.noise_calibrate now takes false | true | equipartition | lpse. `lpse` uses LPSE's own lw.noise.calcNoiseAmp_K0 (ParameterManager.cpp:1440-1444, ported as ZakUnits.noise_amp_k0 with the ZAK grid quantities h, deltaK3, N_zak, L_zak, Lambda_d) converted from LPSE's ZAK k-space potential kick to this code's: the per-mode kick is A calcNoiseAmp_K0 / sqrt(1 + K^2 Lambda_d^2) sqrt(1 - exp(-2 gamma dt)) / |K| times potential_zak_to_adept. `true` stays the equipartition calibration. The deck translator maps lw.noise.isCalculated to `lpse` (it used to substitute equipartition with a note). Verification without a solver run: - the formula reproduces the value LPSE prints for test_010, 1.7885e+00 (port: 1.78853); - mode by mode, noise_kick_spectrum(lpse) equals LPSE's kick expression to 1e-6, and LPSE's Debye length equals vte/wp0; - test_025 (EPW-only noise + damping, isCalculated false): the interior <|phi|^2> of LPSE's lpse.pots frames at 0.5 and 1.0 ps is 0.911 / 0.916 of adept's fluctuation-dissipation expectation for the translated deck including the finite-time transient (plan tolerance 10 %). This required a fact not previously recorded: LPSE writes its potential frames scaled by potentialNormalizationFactor (saveInNormalizedUnits is forced true), so a pots frame holds e phi / (m_e c^2), not the ZAK potential -- ZakUnits.potential_normalization_factor / rho_normalization_factor, documented on read_frames. Without that factor the same comparison reads 3e-4. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…cted TPD term (plan 2 N.4) Ledger (tests/test_lpse2d/test_tpd_depletion.py): with tpd_projection on, the TPD pair energy |E0|^2 + (2 wp0/w0)|E_h|^2 is conserved to round-off for a divergence-free pump (the projector is self-adjoint: <E0, P_T F> = <P_T E0, F> = <E0, F>), exactly as the unprojected term conserves it for any pump. For a pump with a longitudinal part the mismatch equals that part's contribution to the EPW source: the EPW is driven by the whole pump, the projected term returns energy only from its transverse part. Measurement: the FD pump propagator (curl-curl with compact 3-point second differences and a centred cross difference) has a non-zero discrete divergence. Free propagation of a transverse field over one EPW step on the 0.1 um test grid (k0 dx = 1.8) leaves a longitudinal fraction of 0.02 % (|k| < k0/4), 4-6 % (|k| <= k0), 11-13 % (band edge); an oblique plane wave at 5 deg 5 %, at 20 deg 11-14 %; a y-uniform plane wave exactly 0. The fraction saturates rather than grows. No propagator moves that part, so with the projected depletion term it is an energy source for the EPW that the pump never pays for -- a candidate mechanism (SPECULATION, needs the P1/P3 reruns) for the separate-solver FD run going non-finite with the projection on and staying finite without it. Fix, under the plan's default keep-and-fix: terms.light.transverse_fields (default true, fd solver) projects the evolved light fields -- E1 in RamanLight, E0 and E1 in CoupledLight -- onto their transverse part once per EPW step after the sub-steps, as the spectral solver keeps them by construction. A plane-wave pump is unchanged bit for bit; the random transverse pump's 4 % becomes < 1e-12. Documented in config.md; `false` restores the previous behaviour. 73 tests over the light / SRS / ledger / gradient / parity suites unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…an 2 N.2, code part) The applied-Landau-rate multiplier now follows ZakharovSolver::getLD_multiplierForEnergyConservation and getExpectedLwEnergyChange instead of the one-shot linearisation: - expected_wave_loss(M) = sum_k k^2 |phi_k|^2 (1 - exp(-2 dt M gamma_k)) over the retained modes with the HPE-derived gamma_k (the exact per-mode factor; the linear 2 dt M gamma_k over-counts the loss of strongly damped modes and so under-estimates M: at 2 gamma dt = 1 a gain of W/2 needs M = ln 2 = 0.69, the linear form gives 0.5); - M solved by secant iteration from the proportional guess to LPSE's 1 % tolerance, clipped to [0.1, 10], with LPSE's final update; a fixed iteration count with frozen updates once converged keeps it a pure, differentiable JAX function; - the gain is the running average of the per-step particle energy change over energy_conservation_steps (LPSE particleEnergyChange_TW; new state entry hpe_particle_power, reported in the series as keV per real electron per step) and the multiplier is only re-solved once more than that many feedback steps have elapsed since t_start (LPSE countParticleUpdateSteps gate); - the deck translator uses LPSE's default of 10 for hpe.numStepsToAverageEnergyChange when the deck omits it (was 1). This is the "port getExpectedLwEnergyChange literally" item; the plan's wording "wave-side loss = ledger damping channel" is not what LPSE does -- LPSE evaluates the exponential expected loss for a candidate M, which is what is ported. The test_037 verification run (saturation time within 15 % of 2.69 ps, hpe_gamma_ratio_kpeak within 2x) needs the blackbox or Perlmutter and is not part of this commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…the 2-D grid (plan 2 F.1) E0 and E1 carry (x, y, z) like LPSE's XcComplex3 on any grid. The grid stays 2-D (k_z = 0), so the z component has no longitudinal part: the new core/vector.py helpers (fft2c/ifft2c, k_dot, split_k, transverse_part, dot_conj, with_components) project the in-plane pair and pass E_z through; the FD propagators advance E_z with the plain Laplacian (-(curl curl E)_z); the combined solver's L/T propagator, unified source (E0 . E1* over all components, in-plane gradient), depletion and noise, the spectral transverse propagator, the static / oblique pump and the injectors all take the field's component count (2 or 3). The EPW potential and the TPD source see only the in-plane components; the SRS source is the full E0 . E1*, so an s-polarised pump drives SRS and no TPD. init_state_and_args allocates 3 components; a two-component checkpoint is padded with E_z = 0 on restart; the fields output gains e0_z / e1_z; reflectivity sums the transverse components at the probe. Injector components are attributes (pump_component / seed_component, default y) for F.2. Verification (tests/test_lpse2d/test_three_component_fields.py): with E_z = 0 the three-component run is bit-identical (assert_array_equal) to the two-component one on separate/fd, separate/spectral, separate/static, combined/spectral and combined/spectral+iaw; the projectors leave E_z untouched and reduce to the two-component projection in plane; a z-polarised pump gives an identically zero TPD source and the same SRS source as the y-polarised pair (crossed pairs: zero); an E0z plane wave propagates at v_g in both light solvers without acquiring in-plane parts. The two-component tests are unchanged except test_combined.py's three unpackings of longitudinal_transverse, whose return is now (L_k, T_k) arrays. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… Raman seed (plan 2 F.2) drivers.E0.polarization, beams[].polarization and drivers.E1.polarization take LPSE's laser.N.polarization / raman.N.polarization: degrees about the beam axis (or "p" / "s"). LPSE rotateBeam starts the field along y, rotates it about the beam axis by the angle and carries it onto the beam direction, so every launch is cos(psi) along the in-plane transverse direction plus sin(psi) along z: the FD two-point injectors (pump and seed), the spectral Gaussian sources (per beam, with the oblique in-plane unit vector), the combined solver's pump source and the static / oblique prescribed pump all write those two weights. At psi = 0 the arithmetic is unchanged (x 1.0, + 0). A z-polarised launch into a two-component field is refused with a clear error. The deck translator maps the angles instead of reporting them unsupported. Tests (test_three_component_fields.py): on separate/fd, separate/spectral, separate/static, static+oblique and combined/spectral the s launch puts in E0z exactly what the p launch puts in the plane (to 1e-9) and nothing in the plane; 30 deg splits as cos/sin to 1e-12; with TPD on and SRS off the EPW energy under an s-polarised pump equals the free-decay value bit for bit while the p-polarised pump drives it; the translator maps a two-beam 90/45 deg deck, an s-polarised Raman seed, and test_017 (polarization 90) with no unsupported entry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…an 2 F.4) terms.hpe.magnetic_field takes a number (B_z, as before) or [B_x, B_y, B_z] (LPSE hpe.magneticField). With an in-plane component the 2-D tracker carries a third momentum component p_z (0 at load and at thermalising re-injection; the EPW force stays in-plane; the projected histograms and the cone instrument use the in-plane velocity) and every sub-step rotates the 3-momentum about B-hat by |omega_c| dt / gamma (Rodrigues, the exact Boris rotation). With B_z only (float or [0, 0, B_z]) the (p_x, p_y) tracker and its rotation are unchanged bit for bit. The deck translator maps the full vector instead of reporting the in-plane components unsupported. Tests: a B_x-only free-streaming push over a quarter cyclotron period turns p_y into p_z (1e-9) with |p| and the kinetic energy conserved (1e-12) and the y displacement equal to the scheme's rotated-momentum sum; [0, 0, B_z] reproduces B_z bit for bit with the two-momentum shape; the translator maps "0 0 3" -> 3.0 and "1.5 0 3" -> [1.5, 0, 3]. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Request changes for the attached P2 defect: FD-IAW noise bypasses the configured IAW wavenumber cap after the damping mask is applied, repopulating excluded modes that enter density feedback and saved results. The new coarse-to-fine band mapping is present; the remaining issue is the separate noise support.
Postprocessing: Implemented and separately callable through BaseLPSE2D.post_process; the numerical module call does not force plotting or artifact writing. Inspected code and implementation patches cover field/series analysis, persisted light-spectrum and QLE VDF/Landau datasets and plots, with artifact export through ergoExo. This finding affects the simulated data feeding those outputs, not an absence of postprocessing.
Validation/scope: Static inspection only; no tests, simulations, benchmarks, or postprocessing were executed, and no external plots/artifacts were opened. Oversized API responses prevented complete inspection of helpers.py, lpse_deck.py, and configuration documentation; smaller relevant commit patches were inspected where accessible. Complete diff coverage could not be verified.
There was a problem hiding this comment.
[P2] Apply the retained IAW band to the FD noise kick too
In FDIonAcoustic.__init__, noise_band = (sqrt(fk_sq) < k_noise) & (fk_sq > 0) ignores the band constructed from iaw_low_pass_filter_grid. With terms.iaw.noise: true, positive Landau damping, and terms.iaw.max_wavenumber below the noise-disc radius, step.damp first removes excluded modes through damping_factor, then immediately repopulates them via noise_kick. Its final multiplication uses only noise_band, so the configured hard cap is bypassed on every noise update—even starting from zero fields on a periodic box. The next n -= dt * w transfers those modes into density, and IonAcousticWave._fd_step only FFT-truncates when returning the coarse density, allowing excluded coarse-grid modes into light/EPW feedback and saved IAW results.
Intersect the noise support with the mapped retained band (while retaining the additional noise-disc restriction). This follows the spectral IAW path, whose noise is constructed from self.filter, and the documented hard-cap contract. Add a regression combining noise with a restrictive IAW cap; the current noise test has no cap, while test_fd_kspace_band_is_the_coarse_mask_mode_by_mode checks only band/damping_factor, so neither exercises this bypass.
There was a problem hiding this comment.
Fixed in 85f29cc. The FD noise support is now LPSE's noise disc intersected with the retained band (the mapped coarse mask, including terms.iaw.max_wavenumber), as the spectral solver's noise is built from its filter. The kick can no longer repopulate modes that the k-space damping removes.
Regression: test_iaw_fd.py::test_fd_noise_stays_inside_the_iaw_wavenumber_cap. With max_wavenumber: 1, so the cap lies inside the disc, one k-space step from rest leaves w_k zero above the cap (to 1e-9 of the largest) and nonzero below it. Before the fix, the largest kick sat above the cap. Merged into #387 (827531b). Docs: terms.iaw.noise row.
…W band IonAcousticWave._fd_step restricted the fine fields to the EPW grid by FFT truncation only, so modes the coarse IAW mask excludes but the fine fields carry (the fine density is never masked; with landau 0 there is no k-space step at all) entered the coarse density feedback and the saved IAW fields. USER decision 2026-10-20 (PR ergodicio#378 review): the restriction is now multiplied by the coarse IAW band (restriction_band: iaw band with k = 0 kept, the mean being the FD average control's, LPSE forceAverageToZero), as the spectral solver's fields. LPSE point-samples the fine grid instead (SuperSampleGrid::copyFineToCoarse) and never masks the density (isAntiAliasNelf = false); this departs from it. The fine-grid state is unchanged. Test: test_fd_restriction_to_the_epw_grid_keeps_only_the_iaw_band (2-D rectangular mask, landau 0: an excluded corner-gap mode is removed from both coarse fields to 1e-12 of the largest coefficient, a retained mode passes). Before: the excluded mode came through at full amplitude. Disclosure: the first version also asserted that the k = 0 mean passes the whole step; it does not here (the FD average control zeroes it with thermal filamentation off), so the test now checks that the restriction mask leaves k = 0 untouched instead; the 1e-12 tolerance is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
There was a problem hiding this comment.
Request changes for the attached P2 defect: FD-combined propagation applies the small-step Landau approximation to every negative rate, understating inverse-Landau amplification outside that regime and disagreeing with exponential energy accounting.
Postprocessing: Implemented and separately callable through BaseLPSE2D.post_process; the numerical module call does not force plotting or artifact export. Inspected paths and implementation patches include field/series diagnostics, persisted light spectra and QLE VDF/Landau outputs, and plots, with artifact export through ergoExo. The finding affects the simulated fields and their damping/growth accounting, not an absence of postprocessing.
Validation/scope: Static inspection only; no tests, simulations, benchmarks, or postprocessing were executed, and no external plots/artifacts were opened. Oversized API responses prevented complete inspection of helpers.py, lpse_deck.py, and configuration documentation; smaller relevant commit patches were inspected where accessible. Complete diff coverage could not be verified.
There was a problem hiding this comment.
[P2] Restrict the small-step approximation by magnitude for signed Landau rates
In FDCombinedSolver.langmuir_step, jnp.where(rate_dt < 1.0e-3, rate_dt, 1.0 - jnp.exp(-rate_dt)) selects the linear approximation for every negative rate, however large its magnitude. This is reachable through the new QLE Landau feedback and HPE allow_growth: __call__ passes their signed y["gamma_L"] straight into this method. Inverse-Landau modes therefore receive the first-order growth factor instead of the exponential outside the small-step regime, systematically understating their amplification. The neighboring CombinedSolver.propagate_combined uses exponential propagation, and both epw_dissipation and HPE expected_wave_loss use exponential energy accounting, so FD-combined growth can also disagree with its reported exchange. Test the approximation threshold against abs(rate_dt), or use -jnp.expm1(-rate_dt) directly, and cover a retained longitudinal mode with a negative rate outside the small-step regime.
There was a problem hiding this comment.
Fixed in 85f29cc. FDCombinedSolver.langmuir_step now uses LPSE's linearised coefficient only for |γ dt| < 1e-3, and -expm1(-γ dt) otherwise. Small damping keeps LPSE's coefficient, so test_022 and similar decks are unchanged; a negative rate of any size outside that regime grows exponentially. LPSE tests the signed rate, so this departs from it for large negative rates.
Regression: test_fd_combined.py::test_langmuir_step_grows_negative_rates_exponentially, on a retained longitudinal mode at γ dt = −0.1, −5e-4, 5e-4 and 0.1, compared with exp or linear at rtol 1e-12. Before the fix the −0.1 case gave 1.1 instead of e^0.1 (0.47 % low). Merged into #387 (827531b).
…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>
…ity-plan2 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>
… noise obeys the IAW band - FDCombinedSolver.langmuir_step took LPSE's linearised coefficient gamma dt whenever gamma dt < 1e-3, i.e. for every negative rate (QLE feedback, HPE allow_growth): an inverse-Landau mode grew by 1 + |gamma| dt instead of exp(|gamma| dt) (0.47 % low at |gamma dt| = 0.1), against the spectral combined solver's exponential and the exponential energy accounting. The linear branch now needs |gamma dt| < 1e-3, otherwise -expm1(-gamma dt); small damping keeps LPSE's coefficient (LPSE tests the signed rate; PR ergodicio#378/ergodicio#387 review). - FDIonAcoustic's noise support was LPSE's noise disc alone, so with terms.iaw.max_wavenumber (or a mask edge) inside the disc the kick repopulated every step the modes the k-space damping had just removed. The support is now the disc intersected with the retained band, as the spectral solver's noise (PR ergodicio#378 review). Tests: test_langmuir_step_grows_negative_rates_exponentially (rates -0.1, -5e-4, 5e-4, 0.1 per step vs exp / linear, rtol 1e-12) and test_fd_noise_stays_inside_the_iaw_wavenumber_cap (max_wavenumber 1 inside the disc: w_k zero above the cap to 1e-9, nonzero below). Both fail before. Docs: terms.iaw.noise row. 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>
…d/lpse-parity-plan2 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
One table in the lpse2d overview with each intentional departure from the original LPSE C++ code known as of 2026-10-21: what LPSE does (with the source line), what adept does, and who decided it / the commit (HPE rate limits after the energy-conservation multiplier, the FD IAW fine-to-coarse mask, the FD combined Landau step for negative rates, the FD IAW noise support, source_window and srs_k_filter on the combined source, the quasi-1D resonance-absorption filter). USER request. The terms.iaw.noise row now says the max_wavenumber cap is LPSE's own (IawSolver::addNoise); only its intersection with the anti-aliasing mask is a departure. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Request changes for the attached P2 correctness defects:
compare_hpe_fluxaverages LPSE flux over the whole reference run while fitting ADEPT wall energy only over the requested window, producing misleading parity ratios for transient fluxes.cmd_verifycan printPASSand return zero when a previously finite stored diagnostic recomputes as NaN.
Postprocessing: Implemented and reachable, not absent. BaseLPSE2D separates the numerical call from post_process; ergoExo invokes analysis and exports artifacts. The parity harness returns datasets and can persist series.nc, fields.nc, and metrics.npy. The findings concern correctness of comparison and validation of those scientific outputs.
Validation/scope: Static inspection only; no tests, simulations, benchmarks, or postprocessing were executed, and no external plots/artifacts were opened. Oversized API responses prevented complete inspection of some large files/diffs, including helpers.py and lpse_deck.py; smaller accessible files and commit patches were inspected. Complete diff coverage could not be verified.
There was a problem hiding this comment.
[P2] Apply the requested time window to the LPSE wall flux too
In compare_hpe_flux, _, flux = read_flux(flux_path) discards the reference timestamps and np.mean(flux[:, i, ...]) averages the entire run, while hpe_wall_power(series, cfg, window) fits ADEPT's cumulative wall energy only over lo < t <= hi. For a reference containing startup or later transient flux outside that window, the reported hpe_flux_ratio_* therefore compares different physical intervals. Even matching time-dependent wall powers can be reported as a parity discrepancy, and changing window only changes one side of the comparison. Retain the LPSE time array and restrict its flux samples to the same window (with an explicit empty-window check). Cover a reference whose flux differs inside and outside the selected interval; the existing flux test only checks parsing.
There was a problem hiding this comment.
Fixed in 75d3399. compare_hpe_flux keeps LPSE's time column and averages only the samples with lo < t <= hi, in ps, the same interval hpe_wall_power fits. An empty window raises.
Regression (added in 7099c40, together with the verify test): test_parity_harness.py::test_hpe_flux_compares_the_same_time_window. A synthetic lpse.flux with flux 1 inside and 100 outside the window now gives exactly 1 (before the fix, 64), and an empty window is refused. Merged into #387 (2239faa).
There was a problem hiding this comment.
[P2] Fail verification when a recomputed metric is non-finite
In cmd_verify, a NaN computed[k] makes err NaN, but worst = max(worst, err) leaves the finite running maximum unchanged. The loop prints MISMATCH, yet the final ok = worst <= args.rtol and not missing can still print PASS and return zero. This is reachable through harness.compare: for example, a NaN in the saved iaw_density_abs_max series makes the previously finite stored iaw_max_adept recompute as NaN without removing its key or any EPW/Raman ratios. Thus a damaged/non-finite diagnostic can pass the artifact-reproduction acceptance check. Explicitly reject non-finite stored/recomputed values (and accumulate failures independently of worst); add a test where a finite stored IAW maximum recomputes as NaN and require a nonzero exit status.
There was a problem hiding this comment.
Fixed in 7099c40. The comparison is now _verify_report: a non-finite stored or recomputed value, or one outside rtol, counts as a failure per metric, alongside missing ones, so a NaN can no longer hide behind the finite running maximum.
Regression: test_parity_harness.py::test_verify_fails_when_a_recomputed_metric_is_non_finite. It drives cmd_verify with MLflow, the downloads and harness.compare stubbed. A finite stored iaw_max_adept that recomputes as NaN gives exit status 1, and matching metrics give 0. Before the fix: 0. Merged into #387 (2239faa).
…xer) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Two previously reported correctness defects remain at this head; linking the existing inline threads rather than duplicating them:
- [P2] Compare HPE wall flux over the same time window.
compare_hpe_fluxstill discards the LPSE timestamps and averages the entire reference run, whilehpe_wall_powerfits ADEPT's cumulative energy only over the requested interval. Startup or later transients therefore produce misleading parity ratios even when the wall powers agree within the selected window. Restrict the reference samples to the same interval and handle an empty window explicitly. Existing finding. - [P2] Reject non-finite recomputed diagnostics in verification. In
cmd_verify, a NaN error leavesworst = max(worst, err)unchanged, so the command can print a mismatch but still returnPASS. A finite storediaw_max_adeptthat recomputes as NaN is one reachable example. Explicitly reject non-finite values and track failures independently of the running maximum. Existing finding.
Postprocessing: Implemented and reachable: BaseLPSE2D separates simulation from its callable post_process; ergoExo invokes analysis and exports artifacts. The parity harness returns datasets and optionally saves series.nc, fields.nc, and metrics.npy. These findings affect scientific comparison and artifact validation, not the absence of an analysis path.
Validation/scope: Static inspection only; no tests, simulations, benchmarks, or postprocessing were executed, and no external plots/artifacts were opened. Tool output limits prevented complete inspection of the large helpers.py, lpse_deck.py, solver configuration documentation, and test_combined.py patches. Smaller relevant implementation patches were inspected where accessible; complete diff coverage could not be 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>
There was a problem hiding this comment.
The latest documentation/docstring update does not address these previously reported correctness defects, both confirmed in the code at this head. Linking the existing inline findings rather than duplicating them:
- [P2] Use the same window for HPE flux comparisons. In
parity/harness.py,compare_hpe_fluxdiscards the reference timestamps and averagesflux[:, i, ...]over the entire LPSE run, whilehpe_wall_powerfits ADEPT's cumulative wall energy only over the requested window. Transient wall flux therefore produces misleading parity ratios even when the selected intervals agree. Restrict the reference samples to the same interval and handle an empty window explicitly. Existing finding. - [P2] Fail verification on non-finite recomputed diagnostics. In
parity/__main__.py, a NaN relative error does not increaseworst = max(worst, err), andokchecks only that maximum and missing keys. A previously finiteiaw_max_adeptrecomputed as NaN can consequently print a mismatch yet returnPASSand exit zero. Reject non-finite comparisons explicitly and track failure independently of the maximum. Existing finding.
Postprocessing: Implemented and separately callable through BaseLPSE2D.post_process, outside the numerical call; ergoExo invokes it and exports artifacts. Inspected implementation patches include light-spectrum datasets/plots and QLE VDF/Landau outputs. The parity harness returns datasets and optionally persists series.nc, fields.nc, and metrics.npy. These findings affect scientific comparison and validation, not the absence of usable outputs.
Validation/scope: Static inspection only; no tests, simulations, benchmarks, or postprocessing were executed, and no external plots/artifacts were opened. Tool output limits prevented complete diff coverage, including full inspection of helpers.py, lpse_deck.py, and the configuration reference. Individual files and smaller relevant commit patches were inspected; this is not a complete-diff sign-off.
…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>
…lpse-parity-plan2 Brings PR ergodicio#388 (interpolated field save on a coarser grid, saved EPW ex/ey without the stray 4/(nx ny)) via lpse2d/lpse-parity. helpers.py conflicts resolved toward plan2's additions on ergodicio#388's code: - make_field_xarrays: ergodicio#388's unscaled ex / ey with plan2's three-component light fields (e0z / e1z). - interpolated save: ergodicio#388's _interpolated_field_save, with plan2's skipped keys (fd IAW super-sampled fields, iaw_density_old, epw_ledger) and the QLE vdf / gamma_L saved verbatim. - Needed by the coarser save grid: QLE's gamma_L enters the k-fields at the save grid's k modes (the simulation-grid modes of the same integer frequency, _grid_index_of_save_modes); with ergodicio#388's coarse k axes the full-grid gamma_L no longer matched them and make_field_xarrays raised (256 vs 160 modes). Test: test_qle_rate_is_saved_on_an_interpolated_save_grid (exact; fails without the index map). 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>
…d/lpse-parity-plan2 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rum probes on this branch - test_field_save.py (from PR ergodicio#388): its box ran TPD and SRS together on the separate EPW solver, which this branch refuses (as LPSE); SRS is now off (TPD + IAW; the combined solver would save E1 as its transverse part, which the analytic longitudinal E1 would not survive). The saved field set includes the out-of-plane e0_z / e1_z this branch's state carries (plan 2 F.1); tolerances unchanged. - test_light_spectrum.py combined probes: full-grid field save (tpd.yaml's 0.08 um save spacing is finer than their grid, refused since PR ergodicio#388). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
The field-save merge and test updates leave these previously reported correctness defects in the current head. Linking the existing inline findings rather than duplicating them:
- [P2] Compare HPE flux over the same time window.
parity/harness.py::compare_hpe_fluxdiscards the LPSE timestamps and averages the entire reference run, whilehpe_wall_powerfits ADEPT's cumulative wall energy only over the requested interval. Startup or later transients therefore produce misleading parity ratios even when the selected intervals agree. Apply the same window to the reference samples and handle an empty window explicitly. Existing finding. - [P2] Fail verification on non-finite recomputed diagnostics. In
parity/__main__.py::cmd_verify, a NaN error does not increaseworst = max(worst, err), andokchecks only that maximum and missing keys. A finite storediaw_max_adeptrecomputed as NaN can print a mismatch yet returnPASSand exit zero. Reject non-finite comparisons and track failure independently of the maximum. Existing finding.
Postprocessing: Implemented and separately callable through BaseLPSE2D.post_process, outside the numerical call; ergoExo invokes it and exports artifacts. The inspected field-save patch retains three-component light and QLE observations, including the coarse-grid Landau-rate mapping. The parity harness returns datasets and optionally persists series.nc, fields.nc, and metrics.npy. The findings affect scientific comparison and validation, not the absence of usable outputs.
Validation/scope: Static inspection only; no tests, simulations, benchmarks, or postprocessing were executed, and no external plots/artifacts were opened. Individual file diffs and smaller commit patches were inspected, but oversized responses left portions of large files inaccessible with these tools. Complete diff coverage could not be verified.
With an undepleted, prescribed pump (pump_depletion: false, drivers.E0) the step order of 914a0d7 (IAW, EPW, then light) left the only evaluation of the pump, E0_fn(t) in light_split_step, after its consumers: the IAW drive and the EPW's TPD / SRS sources read the previous step's pump -- zero on the first step, since the state starts at E0 = 0 -- while the Raman light evaluates the pump at its own sub-step times. The pump at the step's start t is now written to the state at the top of SplitStep.__call__, for the separate and the combined path, before any consumer (SplitStep.prescribed_pump; the evolved pump is untouched). This departs from LPSE, whose static pump is computed after the LW step at the step's start time (ZakharovSolver::evolve -> LightSolver:: computeFields), i.e. the same one-step lag, and is zero on the first step (no setup-time evaluation). PR ergodicio#387 review; recorded in Departures from LPSE. The shift is one EPW step of the pump envelope / KAP phase. Test: test_prescribed_pump_is_refreshed_before_the_iaw_and_epw[separate, combined]: one step inside the pump's rise from states whose E0 is zero, the pump at t - dt, or the pump at t gives identical outputs (exact). Before: the EPW (separate) and the IAW drive (combined) differed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
compare_hpe_flux averaged the whole LPSE lpse.flux run while hpe_wall_power fits adept's cumulative wall energy over lo < t <= hi, so startup or late transients made matching wall powers look like a parity discrepancy. The LPSE samples are now restricted to the same window (ps, as the file's time column), and an empty window raises (PR ergodicio#378 review). Test: test_hpe_flux_compares_the_same_time_window (a reference with flux 1 inside and 100 outside the window gives exactly 1; before 64). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… metric cmd_verify kept a running maximum of the relative differences; a NaN recomputed metric made its difference NaN, max() kept the finite maximum, and the run printed MISMATCH yet PASS with exit status 0 (reachable through harness.compare, e.g. a NaN in iaw_density_abs_max). The comparison is now _verify_report: non-finite values and out-of-tolerance metrics are failures counted per metric, alongside missing ones (PR ergodicio#378 review). Tests: test_hpe_flux_compares_the_same_time_window (previous commit's regression) and test_verify_fails_when_a_recomputed_metric_is_non_finite (cmd_verify with MLflow and the download stubbed: NaN -> exit 1, matching metrics -> 0; before: 0). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
…d/lpse-parity-plan2 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Request changes for the attached P2 defect: parity verify cannot read the full reference archives produced by run --full / log --full, preventing reproduction of their stored comparison metrics.
The HPE comparison-window and non-finite verification fixes are present, with corresponding regression tests inspected.
Postprocessing: Implemented and reachable through the separate BaseLPSE2D.post_process callable; the numerical call does not force plotting or artifact export. ergoExo invokes analysis and exports artifacts, and the parity harness returns datasets and optionally persists series.nc, fields.nc, and metrics.npy. This finding concerns a broken persisted-result verification path, not an absence of usable outputs.
Validation/scope: Static inspection only; no tests, simulations, benchmarks, or postprocessing were executed, and no external plots/artifacts were opened. Tool output limits prevented full inspection of large patches/files, including helpers.py, lpse_deck.py, the configuration reference, and test_combined.py. Individual file diffs and accessible smaller commit patches were inspected; this is not a complete-diff sign-off.
| stored = { | ||
| k: v for k, v in run.data.metrics.items() if "_energy_growth_" in k or k.startswith(("epw_half_max", "iaw_max")) | ||
| } | ||
| computed = harness.compare( |
There was a problem hiding this comment.
[P2] Resolve both reference layouts when verifying archived runs
For a normal LPSE run tree containing data/lpse.metrics, harness.log_reference(..., full=True) preserves that subdirectory under lpse_reference/. Thus parity run ... --full and parity log ... --full produce lpse_reference/data/lpse.metrics, not the flat path hard-coded in cmd_verify. The preceding download_run_artifacts(..., "lpse_reference/lpse.metrics") raises LookupError for such a fresh archive, and this comparison also assumes the flat layout. Users therefore cannot reproduce the stored metrics from the supported full-archive workflow. Resolve/download either metrics location and pass the resolved path to compare, using the existing reference.metrics_path / METRICS_LAYOUTS convention already used by run and log. Add a verify regression for a full archive as well as the flat archive.
Summary
Plan 2 of the
lpse2d(envelope-2d) parity work with the original LPSE C++ (deck v5.1.2): everything that remained after #372 for 2-D physics except 3-D. Stacked on #372 (lpse2d/lpse-parity): the first 21 commits here are that PR; review the 15 commits from1ebdb86's parent onward (git log lpse2d/lpse-parity..lpse2d/lpse-parity-plan2). Plan and results are in the research vault (Notes/adept/2026-09-16-103112-claude-lpse2d-parity-plan-2-*,2026-09-17-140000-claude-lpse2d-plan2-tiers-l-o-*).Prerequisites and numerics (P, N)
adept/_lpse2d/parity/: reference-run resolver, batch driver, run tags, job matrices),read_framesC-order fix, float64 enforced for parity runs.terms.light.snap_beam_kyreproduces either behaviour.noise_calibrate: lpse(LPSE's thermal-noise constant); N.5 reverse-mode gradient tests through every solver path.tpd-srs-iawexample moves to the combined formulation).Three-component fields (F):
E_zeverywhere, beam/seed polarization angle (s-pol SRS), in-plane B andp_zin the 2-D HPE push, HPE energy-conservation multiplier ported from LPSE.Light (L): FD stencil order 2/4/6 with the order-consistent TF/SF injector (
terms.light.fd_order); PML absorbers (absorber: pml); resonance absorption (resonance_absorption); FD injection of oblique, multiple, profiled and leftward (x-max) beams; light spectrum probes (save.light_spectrum);initial_perturbation; source windows / injector suppression;terms.light.one_waydiagnostic.Kinetic (K):
terms.qle— LPSE's quasilinear module (VDF diffusing in the EPW spectrum, Landau rates recomputed from∇_v f), with three measured departures from the C++ documented in the module: an energy-balance-verified diffusion constant with the Bohm-Gross group-velocity Jacobian, a conservative flux form, and∇f = f ∇ln f.IAW (I):
terms.iaw.solver: fd— LPSE's PPM finite-difference solver on a super-sampled grid with plasma-flow profiles (flow: linear / gaussian / log / file, temporal slope); CBET resonance localisation at the Mach ∓1 layers verified. Source windows and time gates (I.3).Options (O): LPSE's
absoluteThresholdsearch (threshold.find_threshold_lpse).Translator fixes found on the way:
laser.N.evolution.width(profiles were dropped;σ = W/√2), the fd-vs-spectral vote of an inactive Raman field, the spectral solver's incident-flux probe sitting inside its injector (spectral reflectivities logged before1ebdb86with the default probe offset are biased high; EPW growth ratios unaffected).Status
Unit-tested (full
tests/test_lpse2d: 252 passed, 56 min). Not yet compared with LPSE for the tier L / K / I decks (test_011, 012, 016, 019, 030, 029 > 1.5 ps, 001, 003): those reference runs need the blackbox binaries and are the next session. Draft until then.🤖 Generated with Claude Code