replace_missing looks for a decay-data stand-in by stepping the atomic number towards stability until it finds a nuclide the library has:
while product not in decay_data:
if Z > 98:
Z -= 2
A -= 4
else:
if beta_minus:
Z += 1
else:
Z -= 1
product = f'{ATOMIC_SYMBOL[Z]}{A}'
There is nothing stopping the walk. If the direction it picks never reaches a nuclide in decay_data, Z runs down past 1 and the loop dies on ATOMIC_SYMBOL[-1]:
>>> import endf
>>> from endf.chain import Chain
>>> names = ['dec-049_In_115', 'dec-049_In_116', 'dec-050_Sn_116']
>>> decay = [endf.Material(f'tests/{n}.endf') for n in names]
>>> neutron = [endf.Material('tests/n-049_In-115_trimmed.endf')]
>>> Chain.from_endf(decay, [], neutron, reactions=('(n,gamma)',), progress=False)
missing In116 ec/beta+ Cd116
...
File "endf/chain.py", line 209, in replace_missing
product = f'{ATOMIC_SYMBOL[Z]}{A}'
~~~~~~~~~~~~~^^^
KeyError: -1
In116 has a small ec/beta+ branch to Cd116. With no cadmium in decay_data the direction is judged from mass_longest_lived, which defaults to the product's own A, so beta_minus is False and the walk goes down: Ag, Pd, Rh … and off the end. The stand-in it wanted was one step up, at In116.
A full decay sub-library has every nuclide, so the walk always terminates within a step or two and this never shows. It bites as soon as the library is a subset — a reduced chain, a test fixture, or a library that simply lacks an element.
The direction heuristic is reasonable and I would not change it. What is missing is a bound:
while product not in decay_data:
...
if not 1 <= Z <= 118 or A < 1:
return None # or raise a message naming the product
product = f'{ATOMIC_SYMBOL[Z]}{A}'
and then a caller that copes with None — from_endf already handles None from the Z == 0 neutron case a few lines above, so most of the plumbing is there. Reporting the product by name would also be more useful than KeyError: -1, which says nothing about what was being replaced.
Found while porting the reader to Rust. There is no sensible behaviour to reproduce, so the Rust replace_missing returns None when the walk leaves the table, and its caller drops the target rather than inventing one.
replace_missinglooks for a decay-data stand-in by stepping the atomic number towards stability until it finds a nuclide the library has:There is nothing stopping the walk. If the direction it picks never reaches a nuclide in
decay_data,Zruns down past 1 and the loop dies onATOMIC_SYMBOL[-1]:In116 has a small ec/beta+ branch to Cd116. With no cadmium in
decay_datathe direction is judged frommass_longest_lived, which defaults to the product's own A, sobeta_minusisFalseand the walk goes down: Ag, Pd, Rh … and off the end. The stand-in it wanted was one step up, at In116.A full decay sub-library has every nuclide, so the walk always terminates within a step or two and this never shows. It bites as soon as the library is a subset — a reduced chain, a test fixture, or a library that simply lacks an element.
The direction heuristic is reasonable and I would not change it. What is missing is a bound:
and then a caller that copes with
None—from_endfalready handlesNonefrom theZ == 0neutron case a few lines above, so most of the plumbing is there. Reporting the product by name would also be more useful thanKeyError: -1, which says nothing about what was being replaced.Found while porting the reader to Rust. There is no sensible behaviour to reproduce, so the Rust
replace_missingreturnsNonewhen the walk leaves the table, and its caller drops the target rather than inventing one.