Bug Description
ContinuousTabular::sample, KalbachMann::sample, and CorrelatedAngleEnergy::sample_dist all guard their histogram interpolation branch with k >= n_discrete, so a sampled discrete line is returned at exactly its tabulated energy. None of them guard the lin-lin branch. After a discrete hit the CDF walk exits with r1 < c_k, so the lin-lin inversion runs with a negative (r1 - c_k) and returns an energy shifted below the line — bounded by the std::max(0.0, ...) clamp at E_l_k - p_l_k/frac, i.e. potentially a substantial fraction of the local bin scale, and always in the same direction.
Relevant code (develop @ 86ceaad):
src/distribution_energy.cpp — ContinuousTabular::sample, lin-lin branch (~line 222): no k >= n_discrete guard (the histogram branch at ~line 217 has it)
src/secondary_kalbach.cpp — KalbachMann::sample (~line 186): same
src/secondary_correlated.cpp — CorrelatedAngleEnergy::sample_dist (~line 223): same
Reachability (measured against ENDF/B-VIII.0)
A scan of the official ENDF/B-VIII.0 HDF5 neutron library (556 files, 155,600 incident-energy tables, checking the n_discrete_lines and interpolation attributes on every tabulated energy distribution):
| Product particle |
Tables with discrete lines and lin-lin interpolation |
| neutron |
0 |
| photon |
19,913 |
So neutron-only transport never reaches the bug with this library — which is why k-eigenvalue validation suites don't see it. But every coupled neutron–photon run samples secondary gammas through this code: the affected tables are the discrete-gamma-plus-continuum distributions of (n,2n), (n,3n), (n,n′) continuum and similar reactions across roughly half the nuclides in the library. Each time the discrete portion of such a table is sampled, the emitted photon energy lands below its gamma line instead of on it, which distorts photon spectra, line-based responses, and (to the extent line positions matter) heating/dose tallies.
Proposed fix
Mirror the existing histogram guard in all three samplers: for k < n_discrete, return E_out = e_out[k] exactly (for Kalbach-Mann also take r[k]/a[k] directly, which is what the parameter interpolation reduces to at E_out = E_l_k). PR with exactly that change incoming.
Related minor observation in the same walk code, reported for completeness (harmless in practice): CorrelatedAngleEnergy::sample_dist initializes c_k1 to infinity and only assigns it inside the continuous loop, so when r1 lands in the last bin the stale c_k1 == c_k makes the nearest-CDF angle-table pick always choose table k+1 for that bin.
Found while porting the transport engine to Apple Silicon GPUs (dallonby/openmc-metal) — the port's sampler returns discrete lines exactly, and reconciling that deviation against the CPU reference led to auditing where the difference is reachable.
🤖 Generated with Claude Code
Bug Description
ContinuousTabular::sample,KalbachMann::sample, andCorrelatedAngleEnergy::sample_distall guard their histogram interpolation branch withk >= n_discrete, so a sampled discrete line is returned at exactly its tabulated energy. None of them guard the lin-lin branch. After a discrete hit the CDF walk exits withr1 < c_k, so the lin-lin inversion runs with a negative(r1 - c_k)and returns an energy shifted below the line — bounded by thestd::max(0.0, ...)clamp atE_l_k - p_l_k/frac, i.e. potentially a substantial fraction of the local bin scale, and always in the same direction.Relevant code (develop @ 86ceaad):
src/distribution_energy.cpp—ContinuousTabular::sample, lin-lin branch (~line 222): nok >= n_discreteguard (the histogram branch at ~line 217 has it)src/secondary_kalbach.cpp—KalbachMann::sample(~line 186): samesrc/secondary_correlated.cpp—CorrelatedAngleEnergy::sample_dist(~line 223): sameReachability (measured against ENDF/B-VIII.0)
A scan of the official ENDF/B-VIII.0 HDF5 neutron library (556 files, 155,600 incident-energy tables, checking the
n_discrete_linesandinterpolationattributes on every tabulated energy distribution):So neutron-only transport never reaches the bug with this library — which is why k-eigenvalue validation suites don't see it. But every coupled neutron–photon run samples secondary gammas through this code: the affected tables are the discrete-gamma-plus-continuum distributions of (n,2n), (n,3n), (n,n′) continuum and similar reactions across roughly half the nuclides in the library. Each time the discrete portion of such a table is sampled, the emitted photon energy lands below its gamma line instead of on it, which distorts photon spectra, line-based responses, and (to the extent line positions matter) heating/dose tallies.
Proposed fix
Mirror the existing histogram guard in all three samplers: for
k < n_discrete, returnE_out = e_out[k]exactly (for Kalbach-Mann also taker[k]/a[k]directly, which is what the parameter interpolation reduces to atE_out = E_l_k). PR with exactly that change incoming.Related minor observation in the same walk code, reported for completeness (harmless in practice):
CorrelatedAngleEnergy::sample_distinitializesc_k1to infinity and only assigns it inside the continuous loop, so whenr1lands in the last bin the stalec_k1 == c_kmakes the nearest-CDF angle-table pick always choose tablek+1for that bin.Found while porting the transport engine to Apple Silicon GPUs (dallonby/openmc-metal) — the port's sampler returns discrete lines exactly, and reconciling that deviation against the CPU reference led to auditing where the difference is reachable.
🤖 Generated with Claude Code