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