ENDF-6 File Plumbing

The creation of a comprehensive ENDF-6 file often is a multi-year, multi-person effort. Several persons work together to leverage their combined expertise in the design of nuclear experiments, nuclear physics, integral benchmarks, the usage of transport codes, statistical procedures, the ENDF-6 format, and the processing of ENDF-6 files to application formats for creating a well-performing ENDF-6 file.

Considering the huge effort that has gone into the production of the existing ENDF-6 files in various nuclear data library projects, files are usually not created from scratch but rather existing files tweaked to improve their performance. This tweaking may also involve the merging of specific information from several files into a single one.

In this guide, we explain the following basic operations on ENDF-6 files:

  • Tweaking cross sections

  • Removing MF/MT sections

  • Including an MF/MT section from another file

  • Modifying arrays

The explanations for these actions will also give some intuition on how similar operations not listed here can be accomplished.

Tweaking a cross section

Let’s assume the elastic cross section in an ENDF-6 file stored in MT=2 (see manual page 348) is globally underestimated by 5%. Therefore, we want to rescale this cross section accordingly.

Note

In principe, other cross sections must be updated as well to preserve the sum rules (see manual page 40). There are several ways how this can be done, e.g. by updating the total cross section. However, consistent updating will not be covered in this short guide.

The following code snippet achieves this:

from endf_parserpy import EndfParserPy
from endf_parserpy import EndfDict
import numpy as np

parser = EndfParserPy()
endf_dict = EndfDict(parser.parsefile('input.endf'))

xs = np.array(endf_dict['3/2/xstable/xs'])
xs *= 1.05
endf_dict['3/2/xstable/xs'] = list(xs)

parser.writefile('output.endf', endf_dict)

The EndfParserPy class is imported for reading and writing ENDF-6 files. The EndfDict class is an enhanced dict that enables convenient access to data via EndfPaths.

After instantiating an EndfParserPy object, the ENDF-6 file input.endf is read and the resulting dict immediately converted to an EndfDict object.

Next, we retrieve the list with the elastic cross sections stored in the MF3/MT2 section, or more precisely at 3/2/xstable/xs. The list is converted to a numpy ndarray to leverage the associated functionality. The array in xs can then be rescaled by 1.05 using a simple instruction. The result cast to a list is assigned to the respective location in endf_dict, replacing the previous list. Finally, the updated data is written to the file output.endf.

In order to see whether the procedure had the intended effect, we can compare the original file with the adjusted one:

from endf_parserpy import compare_objects
endf_dict1 = parser.parsefile('input.endf')
endf_dict2 = parser.parsefile('output.endf')
compare_objects(endf_dict1, endf_dict2, atol=1e-6, rtol=1e-6 fail_on_diff=False)

The reported differences should only involve the location 3/2/xstable/xs. Please also take note of the information in the section about writing ENDF-6 files regarding the control of output precision.

With the instructions provided above, potentially small numerical differences are introduced in other MF/MT sections if the original file uses an unconventional notation style for real values, e.g. switching from floating point notation to decimal notation to increase precision. To avoid this issue from the start, we can use the include argument in the call of the parsefile() method to only parse MF3/MT2. The other sections will then be read verbatim as string and consequently also written verbatim to the output file. The adjusted instruction for reading the ENDF-6 file in the current example would be:

endf_dict = EndfDict(parser.parsefile('input.endf', include=[(3,2)])

Removing an MF/MT section

For removing MF/MT sections from a file we can use basic Python functionality for deleting keys from dictionaries. For example, the following code snippet removes the MF3/MT2 section from an ENDF-6 file:

from endf_parserpy import EndfParserPy, EndfDict
from endf_parserpy import update_directory
parser = EndfParserPy()
endf_dict = EndfDict(parser.parsefile('input.endf', include=[])
del endf_dict['3/2']
update_directory(endf_dict, parser)
parser.writefile('output.endf')

The include=[] argument causes the parser to not parse any MF/MT section in the ENDF-6 files and to store the raw strings in the dictionary instead. In this way, we ensure that all preserved sections are copied verbatim to the new file. The update_directory() invocation ensures that line counts are properly updated in the directory listing in MF1/MT451 (see manual page 57).

To check if everything worked as expected, we can again compare the input and output file:

>>> endf_dict1 = parser.parsefile('input.endf', include=[])
>>> endf_dict2 = parser.parsefile('output.endf', include=[])
>>> compare_objects(endf_dict1, endf_dict2, fail_on_diff=False)
at path /3: only obj1 contains {2}
False

Including an MF/MT section from another file

To include an MF/MT section from another file, we read both files verbatim into two dictionaries and use basic Python functionality to manipulate the dictionaries for the desired effect. The resulting dictionary is then written to an ENDF-6 file. Assume that we want to merge the elastic cross sections (stored in MF3/MT2) from a file input1.endf into another file input2.endf. Here’s the code snippet that implements the described actions for this case:

from copy import deepcopy
from endf_parserpy import EndfParserPy, EndfDict
from endf_parserpy import update_directory
endf_dict1 = parser.parsefile('input1.endf', include=[])
endf_dict2 = parser.parsefile('input2.endf', include=[])
endf_dict1 = EndfDict(endf_dict1)
endf_dict2 = EndfDict(endf_dict2)
endf_dict2['3/2'] = deepcopy(endf_dict1['3/2'])
update_directory(endf_dict2, parser)
parser.writefile('output.endf', endf_dict2)

The argument include=[] prevents parsing so that all sections are read verbatim into lists of strings. Thereby, all string representations of numbers in the input files are copied as they are to the output file. The invocation of the deepcopy() function is not really necessary. However, without this operation, endf_dict1 and endf_dict2 would share the same dictionary for the MF3/MT2 data. In this case, assignments such as endf_dict2['3/2/AWR'] = 10 would cause the same change in endf_dict1. Using the deepcopy() function prevents this coupling. The update_directory() invocation ensures that line counts are properly updated in the directory listing in MF1/MT451 (see manual page 57).

Modifying arrays

Arrays are implemented as dictionaries with contiguous integer keys. Consider the following part extracted from the ENDF-6 recipe for MF6 sections:

for j=1 to NE:
    [MAT, 6, MT/ 0.0, E[j] , ND[j], NA[j], NW[j], NEP[j]/
             {Ep[j,k], {b[m,j,k]}{m=0 to NA[j]}}{k=1 to NEP[j]} ]LIST
endfor

Suppose the dictionary containing all these variables is called d and that the counter variable NE contains the value 6. The array E[j] would appear as key E in d and d['E'] would be a dictionary with integer keys from 1 to 6.

Suppose we want to insert a new element after the second element. One approach to achieve this is to convert the dictionary first to a list, use the Python functionality for inserting an element into a list, and finally convert the list back to a dictionary. The following code snippet demonstrates this approach:

vals = list(d['E'].values())
vals.insert(2, 5)  # inserted value is 5
d['E'] = {k: v for k, v in enumerate(vals, start=1)}

Of course, we would then also need to increase the associated counter variable NE by one. All other arrays whose size is determined by the loop variable j need to be extended by one element as well.

In contrast, changing a single value can be achieved with a single instruction, e.g.

d['E'][5] = 10