Skip to content

Unresolved resonance ranges with LRF=1 are silently dropped (parse_mf2 dispatches on LRF where it means LRU) #15

Description

@shimwell

Summary

parse_mf2 (src/endf/mf2.py) selects the unresolved-resonance branch on LRF == 2, but the resolved/unresolved distinction is carried by LRU, not LRF.

https://github.com/shimwell/endf-python/blob/main/src/endf/mf2.py#L42-L53

            if LRF == 0:
                ...
            elif LRU in (0, 1):
                # resolved resonance region
                rrange.update(_FORMALISMS[LRF].dict_from_endf(file_obj, NRO))
            elif LRF == 2:
                # unresolved resonance region
                rrange.update(Unresolved.dict_from_endf(file_obj, LFW, LRF, NRO))

Per ENDF-102, LRU=1 is the resolved region and LRU=2 the unresolved one. Within LRU=2, LRF selects the representation: LRF=1 for the energy-independent cases (Case A, LFW=0; Case B, LFW=1) and LRF=2 for the fully energy-dependent one (Case C).

So a range with LRU=2, LRF=1 matches neither elif: not LRU in (0, 1), and not LRF == 2. It falls through with nothing read.

That Unresolved.dict_from_endf contains explicit LFW == 0 and LRF == 1 and LFW == 1 and LRF == 1 branches — code the current dispatch can never reach — is fairly strong evidence the condition was meant to be LRU == 2.

Reproduction

A minimal MF=2 section with LRU=2, LRF=1, LFW=0 (Case A):

import io
from endf.mf2 import parse_mf2

def f(v): return f"{v: .6E}".replace('E+0','+').replace('E-0','-').replace('E+','+').replace('E-','-').rjust(11)
def i(v): return str(int(v)).rjust(11)
def line(*x): return "".join(x).ljust(66) + "9999 2151\n"

text = (
    line(f(95244), f(241.968), i(0), i(0), i(1), i(0))       # HEAD, NIS=1
  + line(f(95244), f(1.0),     i(0), i(0), i(1), i(0))       # isotope, LFW=0, NER=1
  + line(f(100.0), f(1000.0),  i(2), i(1), i(0), i(1))       # range: LRU=2, LRF=1
  + line(f(2.5),   f(0.9),     i(0), i(0), i(1), i(0))       # SPI, AP, LSSF, NLS=1
  + line(f(241.968), f(0.0),   i(0), i(0), i(6), i(1))       # LIST header, NJS=1
  + line(f(10.0), f(3.0), f(1.0), f(0.5), f(0.04), f(0.0))   # D, AJ, AMUN, GNO, GG
)

r = parse_mf2(io.StringIO(text))['isotopes'][0]['ranges'][0]
print(sorted(r))
['EH', 'EL', 'LRF', 'LRU', 'NAPS', 'NRO']

Only the range header survives. SPI, AP, LSSF, NLS and the whole ranges list are gone.

Effect

  • Every unresolved resonance range written with LRF=1 — Case A and Case B, which is how most actinide and many structural-material evaluations write their URR — returns no parameters at all. There is no error, so a consumer sees an evaluation that simply appears to have no URR data.
  • Because the branch also never reads the records, the file position is left before them. Any subsequent range in the same section is then parsed starting from the wrong record, so the corruption is not confined to the range that was skipped.
  • LRU=2, LRF=2 (Case C) happens to work, since LRF == 2 is true for it. That is why this is not immediately obvious.

Suggested fix

            elif LRU == 2:
                # unresolved resonance region
                rrange.update(Unresolved.dict_from_endf(file_obj, LFW, LRF, NRO))

Worth a test with an evaluation that has a Case A or Case B URR — U238 or Am241 from ENDF/B-VIII would do.

Notes

Found while porting the reader to Rust. The port reproduces the current dispatch deliberately, with a comment pointing here, so the two agree until this is settled; the Case A/B readers are implemented on the Rust side and switch on as soon as the condition is corrected.

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