Skip to content

Tabulated1D array evaluation snaps points within 1e-5 RELATIVE of an endpoint to it (np.isclose default rtol), zeroing cross sections just above thresholds #24

Description

@shimwell

Tabulated1D.__call__ snaps any x within 1e-5 relative of the first or last abscissa to that endpoint's y, because np.isclose is called with atol=1e-14 but numpy's default rtol=1e-5 still applies. The scalar path does not do this, so the two disagree, and the array path is the wrong one.

https://github.com/shimwell/endf-python/blob/main/src/endf/function.py#L135-L136

# In some cases, x values might be outside the tabulated region due only
# to precision, so we check if they're close and set them equal if so.
y[np.isclose(x, self.x[0], atol=1e-14)] = self.y[0]
y[np.isclose(x, self.x[-1], atol=1e-14)] = self.y[-1]

The comment says "due only to precision", and atol=1e-14 reads as that intent. But the effective tolerance is 1e-14 + 1e-5 * |x[0]|, which at 6 MeV is 67 eV and at a 5991 eV edge is 0.06 eV. Any genuine grid point inside that window is overwritten with the endpoint value, which at a threshold is zero.

Two confirmed cases

Ba L1 photoionization (photoat-056_Ba_000.endf, MF=23 MT=535), x[0] = 5991.0:

xs(5991.009)                      # 21012.35694510297   scalar, correct
xs(np.array([5991.009]))          # 0.0                 array, snapped
# gap 0.009 eV; window 1e-5 * 5991 = 0.0599 eV

Zero cross section 9 meV above an L1 edge whose value is 21,012 barns.

N15 (n,n3) on its ACE grid, x[0] = 6749151.0, next grid point 6749200.0:

# gap 49 eV; window 1e-5 * 6749151 = 67.5 eV  -> snapped to y[0] = 0

That one point is then missing from the MT 4 inelastic sum, so MT 4 no longer equals the sum of MT 51..91, by 4.61e-6 barns.

Note the MF=3 tabulation for the same reaction is not affected (x[0] = 6747200, gap 2000 eV, window 67 eV), which is why this only appears after NJOY processing. The window scales with the energy, so it bites wherever a grid happens to be finer than 1e-5 relative, and nowhere else. That is why it is rare and easy to miss.

Where it comes from: openmc has it too

These two lines are in openmc as well, at openmc/data/function.py:207-208, identical. endf-python inherited them in 503e4cc, "Initial ENDF functionality ported over from OpenMC", and they are unchanged in paulromano/endf-python main (src/endf/function.py:122-123) and in every fork of it.

Verified on openmc 0.15.3 with the real Ba tabulation, whose first abscissa IS the edge:

import numpy as np, openmc.data
t = openmc.data.Tabulated1D(
    np.array([5991.0, 5991.0, 6167.88862, 6350.0]),
    np.array([0.0, 21012.409, 19989.3069, 19016.02]),
    breakpoints=[4], interpolation=[2])

t(5991.009)                     # 21012.35694510297   scalar
t(np.array([5991.009]))[0]      # 0.0                 array

So this is not a regression against openmc; it is a long-standing behaviour that openmc, endf-python and every downstream consumer share. It matters more here than in openmc because a converter evaluates whole grids at once, which is the array path, whereas openmc's transport reads its own processed tables.

A note on testing it: the window is 1e-5 * |x[0]|, so a synthetic tabulation with a leading point far below the edge will not reproduce it. x[0] has to be at or near the value being probed. My first attempt at this comparison used a leading point at 5000 eV and wrongly concluded openmc was unaffected.

How it was found

Converting ENDF/B-VIII.1 to a different format and diffing against previously published output. Of 33 photoatomic elements compared, Ba was the only one that showed it: only a grid point that lands inside the window is affected, and most grids have none. On the neutron side it showed as N15's MT 4 disagreeing with its own components at one point in 979.

Suggested fix

Pass rtol=0 so the tolerance is the absolute one the comment intends:

y[np.isclose(x, self.x[0], rtol=0, atol=1e-14)] = self.y[0]
y[np.isclose(x, self.x[-1], rtol=0, atol=1e-14)] = self.y[-1]

A regression test worth having: evaluate the same x scalar and array and require agreement, using a tabulation whose second abscissa is within 1e-5 relative of the first.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions