Repository navigation
fix(inelastic): stop loss-density bisection at floating-point resolution (#345) - #358
Conversation
…ion (#345) The Mermin DIIMFP is an adaptive integral at 1e-5; its quadrature noise has jumps of that size that the width-invariant split test of `adaptive_density` cannot resolve, so the panel across each was bisected to one ulp of ln W and then padded with zero-width panels up to MAX_NODES (Si Mermin row at T = 1000 eV: 20 099 nodes, 18 jumps of 0.4 to 7 times 1e-5). A panel whose midpoint rounds onto an end is now not split (5 792 nodes). The padding had no integral, so stored tables are bit-identical; the Al, Cu, Si and Au Mermin tables of the delta(E) runs to 3 keV build in about half the time. A row that still has a failing panel at MAX_NODES is now refused with InelasticTableError::DensityNodeLimit instead of being stored silently. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…ing evaluation) Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
|
Judge pass: still carries a fresh Stand-down passes against this claim: 4 of 3 (streak cap met); claim age 22m of 30m (age floor NOT met). The bounded fallback force-reclaims only once BOTH are met (#9927). This comment is edited in place on each pass rather than reposted (#5123, #6514). |
Judge verdict: approvedReviewed at head Fixed during evaluation: the CHANGELOG listed the per-material counts of rows that reached ulp in the wrong order: "(12, 33, 8 and 3 ...)" for Al, Cu, Si, Au. The PR's own table says Al 33, Cu 8, Si 12, Au 3. I corrected this in 1. Diagnosis and floorI accept the diagnosis. A kink's chord error falls as h², so it cannot drive bisection to one ulp. Only a jump of fixed size can do that, because the split test does not depend on width. The measured 3.6e-6 to 6.7e-5 relative jumps, in up/down pairs, match 2. Byte-identityI agree with this claim, and the reasoning holds beyond the measured cases. The new check skips exactly the splits that would have placed a duplicate node. On main, such a split added a node whose x equalled one end of the panel, with
One caveat: on main the padding counted toward 3.
|
Closes #345
Diagnosis
Reproduced with
row_density/loss_density_node_counton the committed Si ELF (Mermin, model Fermi energy 13.46 eV, density tolerance 1e-5):g = p·Wjumps across a one-ulp panel by 3.6e-6 to 6.7e-5 of the local value (most are 1-2e-5), often in up/down pairs a few 1e-4 apart in ln W.MerminPenn::diimfp_au, not a physical kink. The adaptive integral atDEFAULT_MERMIN_TOLERANCE= 1e-5 changes how it subdivides between neighbouring W. The exact DIIMFP is continuous in W. A kink cannot drive bisection to ulp because its chord error falls as h²; only a jump does.MAX_NODES. This is the Full Penn: make one inelastic table build finish in minutes (#256) #299 mechanism without a grid cell to stop it.Fix (one rule, no tuned constant)
adaptive_densityno longer splits a panel whose midpoint is not a float strictly inside it (!(xm > x[i] && xm < x[i+1])). This generalises the #299 guard: that guard stops at the grid cell width near unresolved full-Penn cells, and this one stops at the floating-point resolution ofx = ln W, where bisection can no longer place a node. Each jump now costs about 50 bisection levels and then stops. The module docs record this as this project's choice ("Floating-point resolution").I rejected the evaluation-error option ("don't split below the DIIMFP's own error"). It changes the split decision of panels that never reach ulp width, so it would change rows that never hit the cap.
MAX_NODES is now visible
adaptive_densityreturns(density, capped). A table row whose refinement still has a failing panel atMAX_NODESis refused with the newInelasticTableError::DensityNodeLimit { energy_ev, max_nodes }. This follows the rate refinement's convention, where its row bound is an error.MomentumTransferSamplerignores the flag and keeps its previous behaviour; the issue only covers table rows.Which rows change: none of the stored output
The old padding panels have
h = 0and carry no integral or moment, so the quantiles are unchanged. I checked this end to end. I built these_yield.pymerminconfiguration at 3000 eV (band-bottom axis, 5 eV, 10 points/decade, rate refinement) for Al, Cu, Si and Au with a main binary and a fixed binary, and the inelastic table files are byte-identical (cmp).No row in these builds reaches the bound with the fix, so the new error breaks none of them. The single-pole, full-Penn, golden, examples and other suites below pass unchanged. Rows that never reach ulp width make exactly the same split decisions as before.
Tests
a_jump_is_bisected_to_floating_point_resolution_and_no_further(synthetic, four 2e-5 jumps): 1 634 nodes vs 1 466 smooth, not capped, narrowest panel > 0 and ≤ ~3 ulp, integral within 1e-5. With the guard disabled: 20 002 nodes, fails.the_si_mermin_row_at_1000_ev_stops_at_floating_point_resolution: 5 792 nodes (< 3 × the 3 304 of T = 100 eV and < MAX_NODES/2), not capped, no zero-width panel, matchesloss_density_node_count. The row's mean loss matches the model's S/λ⁻¹ to 3.7e-7 (the test asserts < 1e-5). With the guard disabled: 20 099 nodes, fails.a_row_that_reaches_the_node_bound_is_refused: a synthetic DIIMFP with 1e-2 noise at every loss givesDensityNodeLimitfromloss_rows.a_step_at_an_unresolved_cell_does_not_exhaust_the_nodesandwithout_unresolved_cells_the_refinement_is_unchangedpass unchanged (1 476 vs 1 466 nodes), so the full-Penn guard still works.Verification
Run with
CARGO_BUILD_JOBS=4 RAYON_NUM_THREADS=2 RUST_TEST_THREADS=2, one at a time:cargo fmt --all --check: okcargo clippy --workspace --all-targets -- -D warnings: okcargo test -p lindhard --lib: 329 passedcargo test -p lindhard --test mermin_inelastic --test penn_inelastic_table --test penn_full --test golden --test electron_secondaries: 12 / 18 / 14 / 3 / 34 passedcargo test -p lindhard-cli --test examples: 38 passedpython3 validation/update_docs.py --check: okCHANGELOG entry under Fixed. No new data and no new constants.
🤖 Generated with Claude Code
loom dashboard