Skip to content

Discrete lines in lin-lin tabular energy distributions are sampled off-line (affects secondary-photon spectra) #4068

Description

@dallonby

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.cppContinuousTabular::sample, lin-lin branch (~line 222): no k >= n_discrete guard (the histogram branch at ~line 217 has it)
  • src/secondary_kalbach.cppKalbachMann::sample (~line 186): same
  • src/secondary_correlated.cppCorrelatedAngleEnergy::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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions