Uh oh!
There was an error while loading. Please reload this page.
KineticForces - BUG FIX - Collisionless (nutype=zero) energy integral NaN + nuzero sentinel (#281) - #282
Conversation
The collisionless (nutype="zero", or any nu==0 config) energy integral produced NaN/DomainError for resonance poles in the Maxwellian tail (x_res >~ 35): under the u = 1-exp(-x) substitution u_res rounds to 1.0, so the residue weight R proportional to (1-u_res) collapses to 0 while log1p(-u_res) -> -Inf, tripping 0*(-Inf) = NaN; in the [35,36] band a Gauss-Kronrod node coincident with the breakpoint gives 0/0 = NaN (QuadGK DomainError). Root cause is the u-substitution compressing the real-axis pole toward u=1. Fix: dispatch the structurally-collisionless case to a dedicated real-x-space integral over [0, X_ENERGY_MAX=72] (matching Fortran PENTRC energy.f90), where the pole x_res in (0,72) stays a well-conditioned O(1) number. Each pole is removed by analytic principal-value + residue (causal nu->0+ branch -/+ i*pi). The collisional (nu>0) path keeps the u-space formulation unchanged (off-axis complex pole, no truncation) - byte-identical output. Verified: x_res sweep 0.5..800 all finite; matches the nu->0+ harmonic limit to ~1e-6; 120 kinetic unit tests pass. Addresses #281. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nce poles Follow-up to the x-space collisionless energy integral: the real-axis resonance denominator Ω(x) = leff·wb·√x + n·(we+wd·x) is a difference of O(1e4)-magnitude terms that cancel to ~0 at the pole, so it rounds to exactly 0.0 over a ~ULP-wide window (granularity ulp(1e4) ≈ 1.8e-12). Across that window the physical term N·exp(-x)/(iΩ) is 0/0 → NaN (QuadGK DomainError), which neither a Newton pole refinement (Ω already rounds to 0) nor a one-ULP nudge (whole window is flat-zero) can escape. Fix: in the flat-Ω window QuadGK is handed the analytic Laurent regular-part limit of the pole-subtracted integrand, [h′ - h·Ω″/(2Ω′)]/(iΩ′) with h = N·exp(-x) (new _energy_numerator_deriv + _collisionless_regular_part). Other poles' finite subtractions are retained. Collisional (ν>0) path is untouched. Also adds the nutype="zero" regression sentinel: - test fixture regression_solovev_kinetic_nuzero (calculated path, [KineticForces] nutype="zero"), - regression-harness case solovev_kinetic_nuzero, - fullruns ex6 and a tail-pole case in the runtests_kinetic ν→0⁺ limit test. Validation: Solovev calculated et[1] = 15.885 - 0.482i (finite). Re sits on the harmonic 15.888 to 0.02% (collisions are a small perturbation to the real energy); Im is the resonant ∓iπ damping, smaller than the harmonic -0.711 as expected. Runtime 23s. 124 kinetic unit tests pass; x_res sweep 0.5..800 NaN-free. Closes#281. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
logan-nc
commented
Jun 15, 2026
@claude review this pr |
Code ReviewOverviewThis PR fixes a real NaN bug ( Issues1. Dispatch on function call instead of field value ( if_energy_collision_frequency(1.0, p) ==0.0return_integrate_energy_collisionless(p, leff, wb, n, we, wd, atol, rtol)
endSince 2. Reactive NaN guard in the integrand closure ( if!isfinite(val)
# find nearest pole k, replace val with _collisionless_regular_part...endThis works in practice (the only non-finite path is the flat-Ω window around the pole), but it is reactive: it intercepts NaN after the fact and silently replaces A proactive threshold check is more explicit: k =argmin(abs(x - xp) for xp in x_poles) # or the existing loopifabs(x - x_poles[k]) < threshold
val =_collisionless_regular_part(x_poles[k], p, leff, wb, n, wd)
...else
val =_energy_integrand_real(x, p)
...endThe threshold would need to cover the flat-Ω ULP window — roughly 3. Nested @testbegin...@testisfinite(real(et[1]))
@testreal(et[1]) ≈15.885 rtol =0.01...trueendIf any inner Minor
SummaryThe math and the fix are correct. Items 1 and 2 are the issues worth addressing before merge: item 1 is a one-line change, item 2 requires a bit more refactoring but eliminates a potential silent-wrong-answer path. |
…residue path Fold the two energy-integral paths (u-space for ν>0, real x-space for ν=0) into a single real-x-space principal-value + residue integrator, `_integrate_energy_resonant`. The pole is `x_pole = x_res − i·ν/Ω′` — off the real axis for ν>0, on it for ν=0 — so the collisionless case is the exact ν→0 limit of one formula rather than a separate branch. The causal ∓iπ·sign(Ω′) retarded branch now falls out of the signed zero of `pole_offset = ν/Ω′` (verified to 1e-14 for both signs of Ω′), and the flat-Ω regular-part limit (`_real_pole_regular_part`, renamed) only ever fires for ν=0. Deletes the ~40-line u-space collisional block (`u_poles`/`u_breaks`, the `u=1−exp(−x)` substitution, the `xr>700` guard). Net −47 lines. The collisional harmonic path now routes through x-space instead of u-space, moving `solovev_kinetic_calculated` et[1] by ~1e-6 relative (per-integral reassociation ~5e-12 amplified through the ill-conditioned eigenproblem; harness 0.00%, inside the fullruns rtol brackets). The 3 ideal harness cases are 0.0-diff. Collisionless ν=0 output is unchanged from #282. Verified: fortran-physics-reviewer PASS (residue, add-back, causal branch, regular-part guard, truncation); 126 runtests_kinetic + 24 runtests_fullruns green; regression harness 3 ideal cases 0.0-diff. Adds an Ω′<0 collisionless causal-branch unit test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
logan-nc
commented
Jun 19, 2026
Follow-on: unified onto a single x-space integrator (0d7c02c)Per reviewer feedback that the two parallel integration paths were a maintainability cost, the collisional (ν>0) and collisionless (ν=0) cases are now one real-x-space PV+residue integrator, This supersedes two claims in the PR description above:
Validation: Post-merge note: re-cache the |
…uns to smoke (closes#273) Split the two conflated jobs the nerfed Solovev-kinetic cases were doing: - REGRESSION (harness): repoint the two kinetic cases at new full-band example decks examples/Solovev_kinetic_{calculated,nuzero}_example (mpsi=16, delta_mlow=delta_mhigh=8 → mpert=32), so the pinned eigenvalue is the mode-converged value (calc et[1]≈1.84−1.41i, nuzero≈1.72−1.08i) rather than the mpert=8-truncated 15.9. Mirrors how the ideal cases already point the harness at examples/ decks while fullruns use the nerfed test/test_data fixtures. Runtime ~105-120s/case (within the ~2-min budget). NB: the mpert=32 value is a Julia self-consistent anchor — the Fortran cross-check in benchmark_solovev_kinetic_stability.jl is locked to mpert=8 (15.888 vs 15.619). - TEST (fullruns ex3-ex6): relax to run-through + physical-sanity smoke tests on the nerfed fixtures (isfinite, total energy > 0, damping imag < 0). Drop the numeric value pins — including the platform-fragile imag(et[1])≈-0.711 rtol=0.08 pin that failed on macOS aarch64 (-0.856). Closes#273. The mode band (not mpsi) is the dominant cost+physics lever here; the auto-mpsi (log_asymptotic) path is a no-op for analytic Solovev (no separatrix → flat slope estimate → near-uniform ~16-surface grid). Verified: 126 runtests_kinetic + 17 runtests_fullruns green; harness runs both new decks within budget at the expected mpert=32 values. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
logan-nc
commented
Jun 19, 2026
Follow-on: kinetic regression on a mode-converged deck; fullruns → smoke tests (closes #273, 8bf5d01)Separated the two jobs the nerfed Solovev-kinetic cases were conflating: Regression harness — the two kinetic cases now point at new full-band example decks
Tests (fullruns ex3–ex6) — relaxed to run-through + physical-sanity smoke tests on the nerfed fixtures ( Validation: 126 So this PR now: fixes the collisionless NaN (#281), unifies the energy integral onto one x-space path, and re-architects the kinetic coverage into fast smoke tests + mode-converged harness pins (closing #273). |
…ix/kinetic-collisionless-nan
…ation' into bugfix/kinetic-collisionless-nan
…files; collapse nu=0 deck via harness override Make the Solovev kinetic example physically rigorous and remove the duplicate collisionless deck. Profiles: replace the invented parabolic kinetic.dat with profiles tied to the Solovev equilibrium pressure P(psi)=P0*(1-psi), P0~4.27e4 Pa (beta_axis~11%). T(psi)=100+2600*(1-psi) eV (core 2700, edge 100); n=P0*(1-psi)/(2*e*T) (core ~4.9e19) with a small edge density floor so T stays finite. 2*n*T matches P_eq to ~1e-4. Written to the example deck and both kept test fixtures with a provenance header. nu=0 collapse: delete examples/Solovev_kinetic_nuzero_example. Add a generic [overrides] mechanism to the regression harness (CaseSpec.overrides -> _materialize_rundir copies the deck to a temp dir, patches dotted gpec.toml keys, cleans up). solovev_kinetic_nuzero now reuses the calculated deck with KineticForces.nutype="zero". Re-baselined: calculated et[1]~1.894-1.525i, nuzero et[1]~2.333-2.174i. Reader robustness: KineticProfiles readdlm(...; comments=true) so a '#' provenance header cannot widen the parsed matrix and pad the data rows. Deck migration: port the kinetic decks off the removed bal_flag/mer_flag onto local_stability_flag and drop the deprecated delta_mband (the develop merge removed those keys, which was breaking every kinetic run on this branch). Verified: fullruns 17/17 green (ex5/ex6 with new profiles), harness override runs end-to-end with the same equilibrium and distinct collisionless damping. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…100 and harden boundary-pole handling Manual PR review asked whether the real-x-space energy integral's upper limit X_ENERGY_MAX could be chosen more robustly, and whether the approach is robust to a resonance pole sitting at or within ~1e-15 of that limit (the direct analog of the original u→1 NaN this branch fixed). Findings: the integrand AND every resonance pole decay as x^p·exp(-x) (p ≤ 3.5 with the qt heat-flux factor) — at a simple pole Ω′ is O(the drift frequencies), so a dropped pole's residue scales as xr^2.5·exp(-xr), the same law as the smooth tail. exp(-x) does not underflow until x≈746, so the Fortran default 72 was a tolerance edge, not a precision edge. Benchmarking confirmed a tighter, tolerance-derived limit buys no speed (QuadGK accepts the near-zero tail in a single panel; 72→100 is byte-identical eval count). So extend the fixed limit 72 → 100 (100^3.5·exp(-100) ≈ 4e-37, ~30 orders below any tolerance) and document the real justification. A kept pole (xr < X_ENERGY_MAX) is subtracted analytically/exactly and is robust at any proximity to the limit; the xr >= X_ENERGY_MAX guard prevents the only NaN risk, log(X_ENERGY_MAX - x_pole)=log(0), at the exact endpoint. Near-degenerate poles (Ω′→0) are handled by the SINGULAR_EPS guard, not by the limit. Added a regression testset forcing a resonance to exactly X_ENERGY_MAX and ±1e-13 around it, asserting finiteness, continuity across the boundary, and agreement with the ν→0⁺ limit. All 200 kinetic unit tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8f6f68d
into
test/kinetic-pv-regression-validationUh oh!
There was an error while loading. Please reload this page.
Summary
Fixes the collisionless (
nutype="zero") energy-integral NaN tracked in #281 (deferred out of #280), and adds the end-to-endnuzeroregression sentinel that was blocked on it.Stacked on #280 (
test/kinetic-pv-regression-validation) — it depends on that PR'snutypethreading. Base will be retargeted todeveloponce #280 merges.The bug
The collisionless energy integral (
src/KineticForces/EnergyIntegration.jl) producedNaN/ a QuadGKDomainErrorwhenever a resonance pole fell in the Maxwellian tail (x_res ≳ 35). Two mechanisms, both rooted in theu = 1 − exp(−x)substitution compressing the real-axis pole towardu = 1:u_res = 1 − exp(−x_res)rounds to1.0, soR ∝ (1−u_res) = 0whilelog1p(−u_res) = −Inf→0·(−Inf) = NaN;Ω(x) = leff·wb·√x + n·(we+wd·x)— a difference ofO(10⁴)-magnitude terms — rounds to exactly0.0over a ~ULP-wide window (granularityulp(1e4) ≈ 1.8e-12), where the physical termN·exp(−x)/(iΩ)is0/0. Neither a pole-location Newton refinement (Ωalready rounds to 0) nor a one-ULP nudge (the whole window is flat-zero) escapes it.The fix
Split the energy integral by collisionality (
integrate_energydispatches on_energy_collision_frequency(1.0,p) == 0.0):[0,1)integral with the off-axis complex-pole add-back. The harness confirms byte-identical output (see below)._integrate_energy_collisionless) over[0, xmax = 72], matching Fortran PENTRCenergy.f90. Each real pole is removed by analytic principal-value + residue (causalν→0⁺branch∓iπ·sign(Ω′)), an improvement over the deprecatedximagcontour offset. Inside the flat-Ωwindow the integrand is replaced by the analytic Laurent regular-part limit[h′ − h·Ω″/(2Ω′)]/(iΩ′)(h = N·e^{−x}), via new_collisionless_regular_part+_energy_numerator_deriv.Validation
fortran-physics-reviewerindependently re-derived the residue, the causal∓iπ·sign(Ω′)branch, the regular-part Laurent limit (the crux), the numerator derivative, and thexmax=72truncation — all match the code (PASS), againstpentrc/energy.f90and Logan & Park 2013 Eq. (8).regression-guardian):diiid_n1,solovev_n1,solovev_multi_n,solovev_kinetic_calculatedare 0.0-diff vsdevelop(81 quantities, zero regressions — the collisional/ideal paths are byte-identical).nuzerosentinel: Solovev calculated collisionlesset[1] = 15.885 − 0.482i.Resits on the harmonic15.888to 0.02% (collisions are a small perturbation to the real energy);Imis the resonant∓iπdamping, smaller than the harmonic−0.711as expected. Runs in ~68 s (≤ 2 min budget).runtests_kinetic.jlunit tests pass (incl. a new tail-pole case in the ν→0⁺-limit test);x_ressweep0.5…800is NaN-free;runtests_fullruns.jlex6added.Changes
src/KineticForces/EnergyIntegration.jl— collisionless x-space branch + regular-part limit + dispatch.test/test_data/regression_solovev_kinetic_nuzero/— new fixture ([KineticForces] nutype="zero").regression-harness/cases/solovev_kinetic_nuzero.toml— new harness case.test/runtests_fullruns.jl(ex6),test/runtests_kinetic.jl(tail-pole case).Closes#281.
🤖 Generated with Claude Code