Extending the ENDF-6 Format

endf-parserpy leverages ENDF-6 recipe files for reading and writing ENDF-6 files. These recipe files are written in a formal language described in detail in this ArXiv preprint. This approach enables the addition of support for format extensions by updating these recipe files. The Python source code of endf-parserpy doesn’t need to be touched.

In this tutorial, we want to demonstrate this approach by including a format extension to enable the storage of covariance matrices relying on the decomposition C = S A S^T. Therefore, we want to extend the format to support the storage of two matrices S and A in an MF33 section.

As a first step, we retrieve all ENDF-6 recipes used by endf-parserpy:

from endf_parserpy.endf_recipes.endf6_ext import endf_recipe_dictionary

The variable endf_recipe_dictionary contains a dictionary with all the available recipes. If a recipe applies to all MT sections of an MF section, it is directly stored under an integer key given by the MF number. This is the case for the MF33 section, so we can print the corresponding MF33 recipe:

print(endf_recipe_dictionary[33])

We reproduce the output for the part of the recipe that is concerned with the different representations of covariance matrices within an NI-type subsection, see ENDF-6 formats manual (page 301):

if LB>=0 and LB<=4 [lookahead=1]:
        [MAT,33,MT/ 0.0, 0.0, LT, LB, 2*NP, NP/
                {Ek[k] , Fk[k]}{k=1 to (NP-LT)}
                {El[k] , Fl[k]}{k=1 to LT} ]LIST

elif LB==5 and LS==0 [lookahead=1]:

         ...

elif LB==5 and LS==1 [lookahead=1]:

         ...

elif LB==6 [lookahead=1]:

         ...

elif (LB==8 or LB==9) and LT==0 [lookahead=1]:

         ...

endif

We can assign our format extension to a new LB number and include the associated ENDF record specifications in an additional if-branch. Choosing LB=10, here is a possible form of the additional branch:

format_extension = """

elif LB==10 [lookahead=1]:
    numel := NE + (NE-1)*M + M*M
    [MAT, 33, MT / 0.0, 0.0, NE, LB, numel, M /
        { E[i] }{ i=1 to NE},
        {{ S[i,j] }{ i=1 to NE-1 }}{ j=1 to M },
        {{ A[i,j] }{ i=1 to M  }}{ j=1 to M }
    ] LIST

"""

The notation numel := ...` introduces a name for an expression that can be used as an abbreviation in record specifications.

We can now include this additional representation option into the ENDF-6 recipe dictionary:

from copy import deepcopy
import re
import numpy as np
mf33_recipe = endf_recipe_dictionary[33]
m = re.search(r'elif.*LB==8(.|\n)*?endif', mf33_recipe, re.MULTILINE)
inspos = m.span()[1] - len('endif')
new_mf33_recipe = mf33_recipe[:inspos] + format_extension + mf33_recipe[inspos:]
new_recipe_dictionary = deepcopy(endf_recipe_dictionary)
new_recipe_dictionary[33] = new_mf33_recipe

This code snippet finds the last elif ... endfif part by regular expression matching and then introduces the string with the format extension just before the word endif. We could have achieved the same by copy-pasting the complete recipe specification into a text editor and insert the format extension manually at the correct location.

To make EndfParserPy aware of the format extension, we need to pass the recipes argument with the updated ENDF recipe dictionary to its constructor:

parser = EndfParserPy(recipes=new_recipe_dictionary)

For the sake of illustration, let’s create an MF33/MT1 section making use of the format extension. We can adopt a large of part of the set up from the tutorial that covered the creation of an ENDF-6 file, more precisely the section that covered the creation of MF33/MT1.

The following code snippet sets up the generic part specifying that we want to have one NI-type subsection:

endf_dict = EndfDict()
endf_dict['33/1'] = {}
endf_dict['33/1/MAT'] = 2625
endf_dict['33/1/ZA'] = 26054
endf_dict['33/1/AWR'] = 53.47625
endf_dict['33/1/MTL'] = 0
endf_dict['33/1/NL'] = 1
endf_dict['33/1/subsection[1]'] = {}
endf_dict['33/1/subsection[1]/XMF1'] = 0.0
endf_dict['33/1/subsection[1]/XLFS1'] = 0.0
endf_dict['33/1/subsection[1]/MAT1'] = 2625
endf_dict['33/1/subsection[1]/MT1'] = 1
endf_dict['33/1/subsection[1]/NC'] = 0
endf_dict['33/1/subsection[1]/NI'] = 1

The next part is concerned with the representation of the covariance matrix and we use here our newly introduced LB=10 option with some dummy data for the energies E and the two matrices S and A:

energies = np.linspace(1, 10, 5)
NE = len(energies)
M = 2
myS = np.full((NE-1, M), 1.)
myA = np.full((M, M), 2.)

endf_dict['33/1/subsection[1]/ni_subsection[1]/LB'] = 10
endf_dict['33/1/subsection[1]/ni_subsection[1]/NE'] = NE
endf_dict['33/1/subsection[1]/ni_subsection[1]/M'] = M

endf_dict['33/1/subsection[1]/ni_subsection[1]/E'] = \
   {k: v for k, v in enumerate(energies, start=1)}

endf_dict['33/1/subsection[1]/ni_subsection[1]/S'] = {}
S = endf_dict['33/1/subsection[1]/ni_subsection[1]/S']
for k in range(1, NE):
    for kp in range(1, M+1):
        S[k, kp] = float(myS[k-1, kp-1])

endf_dict['33/1/subsection[1]/ni_subsection[1]/A'] = {}
A = endf_dict['33/1/subsection[1]/ni_subsection[1]/A']
for k in range(1, M+1):
    for kp in range(1, M+1):
        A[k, kp] = float(myA[k-1, kp-1])

Finally, we can output the data in the ENDF-6 format:

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

Once you understand the formal ENDF format description language. you can quickly bend the format in any way you want to test your ENDF-6 format extension proposals.