Creating ENDF-6 Files from Scratch
You’ve managed to fit a spline, Pade approximant or a nuclear physics model nicely to experimental data. Now you are eager to put your result into an ENDF-6 file for further processing with NJOY or submit it as a candidate file to a nuclear data library project.
On this page, we go through the process of creating an ENDF-6 file that will contain a total cross section and an associated covariance matrix. Even though this file will be quite minimal, the steps explained here can be adjusted to include cross sections and covariance matrices of more reaction channels. Once you understand the general process, you will be able to create hand-tailored ENDF-6 files for your application needs.
Prerequisites
In order to create an ENDF-6 file from scratch with endf-parserpy, you need the following:
An understanding of the physical quantities defined in the ENDF-6 formats manual, e.g. such as mass number, charge number and cross section. You also need the manual to look up the variable names associated with those quantities.
An understanding of how ENDF-6 recipes determine the structure of Python dictionaries with ENDF-6 data, which was explained in this section.
The ENDF-6 recipe files corresponding to the MF/MT sections you want to create in order to determine the variables to be included in the Python dictionary. In general, the variable names in the recipes are the same as in the ENDF-6 formats manual.
Note
If you just get started with the ENDF-6 format, these prerequisites may seem daunting. However, with endf-parserpy you can construct the dictionary step-by-step. If you forget to include a required variable, you will receive a detailed error message informing you about the missing variable(s). You can work in an interactive Python session to construct a complete dictionary by trial and error, incrementally including the missing variables.
Creating the dictionary
The Python dictionary we are going to create will include the following MF/MT sections:
An MF1/MT451 section with meta data and a general description of the file content (manual page 57).
An MF3/MT1 section with tabulated total cross sections as a function of incident energy (manual page 122).
An MF33/MT1 section with a covariance matrix to express the uncertainty of the total cross section (manual page 293).
Creating the MF1/MT451 section
Section 1.1 in Chapter 1 of the ENDF-6 formats manual (page 57) lists all variables that need to be defined for the creation of an MF1/MT451 section. The ENDF-6 recipe for MF1/MT451 also contains the required variable names and furthermore we can infer the structure of the Python dictionary from it. Because this ENDF-6 recipe is not too long, we also provide it here for convenient reference:
[MAT, 1,451/ ZA, AWR, LRP, LFI, NLIB, NMOD]HEAD
[MAT, 1,451/ ELIS, STA, LIS, LISO, 0, NFOR]CONT
[MAT, 1,451/ AWI, EMAX, LREL, 0, NSUB, NVER]CONT
[MAT, 1,451/ TEMP, 0.0, LDRV, 0, NWD, NXC]CONT
[MAT, 1,451/ ZSYMAM{11}, ALAB{11}, EDATE{10}, {1}, AUTH{33} ]TEXT
[MAT, 1,451/ {1}, REF{21}, DDATE{10}, {1},
RDATE{10}, {12}, ENDATE{8}, {3} ]TEXT
for i=1 to 3:
[MAT, 1,451/ HSUB[i]] TEXT
endfor
for i=1 to NWD-5:
[MAT, 1,451/ DESCRIPTION[i]]TEXT
endfor
for i=1 to NXC:
[MAT, 1,451/ blank, blank, MFx[i], MTx[i], NCx[i], MOD[i]]DIR
endfor
SEND
This recipe does not contain any section blocks, hence all variables in this recipe must be available as keys in the root dictionary associated with MF=1/MT=451. Let’s first instantiate an EndfParserPy object:
from endf_parserpy import EndfParserPy
parser = EndfParserPy(explain_missing_variable=True)
Setting explain_missing_variable=True enables an experimental feature,
which will display the descripton of an omitted but required variable.
To faciliate the creation of the dictionary, we will make use of the
EndfDict class for a more convenient dictionary
building process:
from endf_parserpy import EndfDict
endf_dict = EndfDict()
Next, we create an empty MF=1/MT=451 section and associate it with a
short variable name for reduced typing and perform our first
little variable assignment for the MAT number (manual page 30).
Let’s assume we want to create a file for 26-Fe-54 which corresponds
to MAT=2625.
The code to accomplish the described actions is given by:
endf_dict['1/451'] = {}
d = endf_dict['1/451']
d['MAT'] = 2625
Let’s see what happens when we attempt to convert the
data of this very empty dictionary into the ENDF-6 format using
the write() method:
parser.write(endf_dict)
This instruction yields the following error message:
endf_parserpy.custom_exceptions.VariableNotFoundError:
Here is the parser record log until failure:
--------------------------------------------
Template: [ MAT , 1 , 451 / ZA , AWR , LRP , LFI , NLIB , NMOD ] HEAD
Error message: variable ZA not found
Explanation of missing variable `1/451/ZA`
------------------------------------------
ZA = (1000.0 * Z) + A
Z ... charge number of material
A ... mass number of material
This output displays the line in the ENDF-6 recipe that refers to
the missing variable, here ZA.
Furthermore, we obtain the exact location (in EndfPath form)
where the missing variable is expected to be.
Based on this information, we can now add the missing variable:
endf_dict['1/451/ZA'] = 1000.0 * 26 + 54 # 26054.0
The alternative assignment p['ZA'] = 26054.0 would have had the same effect.
One approach to completing the dictionary is calling parser.write(endf_dict)
iteratively and add each time the variable that was reported until
the function doesn’t complain anymore.
Probably a faster approach is to consult the explanations of the variables
in the ENDF-6 formats manual (page 57) and define all of them at once.
Here is a (somewhat messy) example code snippet for defining all quantities of an MF=1/MT=451 section:
parser = EndfParserPy(explain_missing_variable=True)
endf_dict = EndfDict()
endf_dict['1/451'] = {}
p = endf_dict['1/451']
p['MAT'] = 2654; p['ZA'] = 26*1000. + 54
p['AWR'] = 53.47625 ; p['LRP'] = 2; p['LFI'] = 0
p['NLIB'] = 45; p['NMOD'] = 1; p['ELIS'] = 0.0
p['STA'] = 0; p['LIS'] = 0; p['LISO'] = 0
p['NFOR'] = 6; p['AWI'] = 1.0; p['EMAX'] = 2e8
p['LREL'] = 0; p['NSUB'] = 10; p['NVER'] = 1
p['TEMP'] = 0.0; p['LDRV'] = 0; p['NWD'] = 6
p['NXC'] = 1; p['ZSYMAM'] = ' 26-FE- 54 '
p['ALAB'] = 'MyLab'.ljust(11)
p['EDATE'] = 'EVAL-JAN24'
p['AUTH'] = 'John Doe'.ljust(33)
p['REF'] = 'ABC-2023'.ljust(21)
p['DDATE'] = 'DIST-FEB24'
p['RDATE'] = 'REV2-MAR24'
p['ENDATE'] = '20240315'
p['HSUB/1'] = (f'----LIBPROJ-{p["NVER"]}'.ljust(22) + f'MATERIAL {p["MAT"]}').ljust(66)
p['HSUB/2'] = '-----INCIDENT NEUTRON DATA'.ljust(66)
p['HSUB/3'] = '------ENDF-6 FORMAT'.ljust(66)
p['DESCRIPTION/1'] = 'A new file is born with beautiful data!'.ljust(66)
p['MFx/1'] = 1; p['MTx/1'] = 451; p['NCx/1'] = 5; p['MOD/1'] = p['NMOD']
For understanding the meaning of a variable and its value, we can use the
explain() method of the
EndfParserPy class, e.g.
parser.explain('1/451/STA'), or look up its description in the ENDF-6 formats manual (page 57).
These assignments are also a good opportunity to remind ourselves of how
data types
are assigned to variables: The variables in the first two slots of an ENDF record
are of type float and the next four of type int.
Importantly, we need to keep in mind that the NWD variable counts
the number of all TEXT records in MF1/MT451. The first five of these
records have a specific structure and only the subsequent TEXT records
(associated with the DESCRIPTION[i] array) can be filled with free-form text.
This means that the number of elements in the DESCRIPTION array
is NWD-5. The preparation of the text fields in this section is still
a bit cumbersome.
Convenience functions may be added in the future to make the
handling of variables associated with strings easier so that the user
doesn’t need to be concerned anymore about the alignment of strings in
text fields or the number of dashes preceding certain words.
Also note that the MF1/MT451
dictionary contains a directory (manual page 57) represented by
the arrays MFx[i], MTx[i], NCx[i] and MOD[i], which keeps
track of MF/MT sections included in the file and the number of ENDF records stored
in each of them.
We will synchronize this directory with the full file using
the update_directory() function once
we’ve added all required information to the dictionary.
Regarding the process, there are a lot of variables and it takes time to understand their meaning and associate them with the right values. On the positive side, issues regarding formatting, reading and writing are completely decoupled from the specification of the data. In effect, this approach emphasizes an information-oriented perspective over a processing-oriented one. The user can focus on the correct definition of variables and doesn’t need to be concerned anymore with the technical details of how the data is organized in an ENDF-6 file.
Creating the MF3/MT1 section
The creation of the MF3/MT1 section (described on manual page 122) with cross sections as a function of incident energy is straightforward. The associated ENDF-6 recipe is given by:
[MAT, 3, MT/ ZA, AWR, 0, 0, 0, 0] HEAD
[MAT, 3, MT/ QM, QI, 0, LR, NR, NP / E / xs]TAB1 (xstable)
SEND
We have encountered the variables ZA and AWR already in the creation
of the MF1/MT451 section above. The mass-difference Q-value for the
total cross section is zero, i.e. QM = 0.0, and the same holds true
for the reaction Q-value, i.e. QI = 0.0. Also the breakup flag is
given by LR = 0.
For the total cross section, these values will be stored in an MT=1
section. For the association between reaction channels and MT numbers
consult the ENDF-6 formats manual (page 348).
As discussed in the section about the particularities of the TAB1 record, the NR and NP variable are
ignored because their values can be inferred from the length of the
list stored under the NBT and E variables.
Because of the presence of a table body section named xstable, the
variables NBT, INT, E and xs are supposed to be
in that section. Here we will use some dummy data for the excitation
function in the construction of the dictionary. We augment the dictionary
endf_dict (being an EndfDict object)
introduced above (which already includes the MF1/MT451 section):
endf_dict['3/1/MAT'] = endf_dict['1/451/MAT']
endf_dict['3/1/AWR'] = endf_dict['1/451/AWR']
endf_dict['3/1/ZA'] = endf_dict['1/451/ZA']
endf_dict['3/1/QM'] = 0.0
endf_dict['3/1/QI'] = 0.0
endf_dict['3/1/LR'] = 0
endf_dict['3/1/xstable/NBT'] = [5]
endf_dict['3/1/xstable/INT'] = [2]
endf_dict['3/1/xstable/E'] = [1.0, 2.0, 3.0, 4.0, 5.0]
endf_dict['3/1/xstable/xs'] = [10.0, 11.0, 12.0, 13.0, 14.0]
We can rely on the description in the ENDF-6 formats manual (page 122),
the ENDF-6 recipe shown above and also the iterative approach based
on calls to parser.write(endf_dict) to learn about the missing
variables.
In the example code snippet here,
we worked with the original endf_dict instance.
Equally possible, we could have used an abbreviation,
such as p = endf_dict['3/1'] for more concise specifications, e.g.
p['QM'] = 0.
Choosing NBT to be a list with a single entry given by the number of
elements in E (or xs) means that we are using a single interpolation region.
The choice of INT = [2] specifies linear interpolation for that region.
For details on the available interpolation schemes, see
the ENDF-6 formats manual (page 43).
Note
The parser doesn’t check whether the values provided
are physically meaningful. For instance, negative cross section
values in xs or negative incident energies in E will be written
by the writefile() method to an ENDF-6 file as
they are, without any warnings.
Creating the MF33/MT1 section
Any measurement without knowledge of its uncertainty is meaningless. Also evaluated nuclear data derived from a statistical analysis of experimental data need to be given with uncertainty information. The purpose of the MF33 section is the storage of covariance matrices for the cross section data included in the MF3 section (see manual page 293). There is an overwhelming number of options and ways for storing these matrices. The aim of this section is to walk you through the process for one specific and commonly used approach.
We assume that you have a covariance matrix prepared as a
numpy array that contains
the relative uncertainties in the cross section values.
We’ve introduced five cross section points in the definition of
the MF3/MT1 dictionary discussed earlier. For simplicity, let’s introduce a
covariance matrix associated with the same energy mesh as for
the cross sections, i.e. endf_dict['3/1/xstable/E'].
Note
Cross sections stored under MF3 are defined at specific energies and linear interpolation (or other interpolation schemes) needs to be used to determine their values at intermediate energies. To refer to this approach, people often say that cross sections are stored point-wise. In contrast, a covariance matrix in an MF33 section is defined group-wise. This means that the energy mesh provides the boundaries between distinct groups and uncertainty information is provided for each energy group. Consult the ENDF-6 formats manual (page 274) for more details. To keep this section manageable, we don’t discuss this aspect further.
An example covariance matrix can be created with the following code snippet:
import numpy as np
covmat = np.diag([0.04, 0.09, 0.16, 0.25])
With this specification including four elements together with the
adopted energies (endf_dict['3/1/xstable/E']), the uncertainty
is specified as 20% (square root of 0.04) between one and two eV,
30% between two and three electronvolt, etc. The numbers in
endf_dict['3/1/xstable/E'] were specified in the code snippet
introduced above in the section about MF3/MT1.
The ENDF-6 recipe for MF33 sections
is more complex and we don’t include it in full here.
Instead, we consider the relevant parts in a step-by-step approach
for instructional purposes.
As before, we will use the endf_dict dictionary that already includes
the MF1/MT451 and MF3/MT1 sections. The goal of this last part of the
tutorial is to properly set up an MF33/MT1 section with the covariance
matrix for the total cross section.
The first line of the MF33 recipe is given by:
[MAT, 33, MT/ ZA, AWR, 0, MTL, 0, NL] HEAD
We’ve already encountered MAT, ZA and AWR before.
The variable MTL indicates whether the covariance matrix
defined in this section is given by a sum of covariance matrices
from other MT sections in MF33. Here we directly provide our
covariance matrix, so MTL=0.
Let’s set up these variables:
endf_dict['33/1/MAT'] = endf_dict['1/451/MAT']
endf_dict['33/1/ZA'] = endf_dict['1/451/ZA']
endf_dict['33/1/AWR'] = endf_dict['1/451/AWR']
endf_dict['33/1/MTL'] = 0
The NL variable defines the number of subsections.
The relevant part of the recipe looks like this:
if MTL == 0:
for n=1 to NL:
(subsection[n])
...
(/subsection[n])
endfor
Because of MTL = 0 in our case, the part inside the if-block
is active. The integer stored in NL determines how
many subsection should be present in the dictionary.
Each subsection contains the covariance matrix
between the cross section data associated with this
MF33 section (here MF3/MT1) and another cross section
channel (determined by another MAT, MF, MT number
combination). In this guide, we only want to
include a single section that contains the covariance
matrix for MF33/MT1, hence NL = 1.
Let’s establish this assignment and create an empty subsection:
endf_dict['33/1/NL'] = 1
endf_dict['33/1/subsection[1]'] = {}
We could have also used the equivalent
EndfPath notation 33/1/subsection/1.
We descend further into the nested recipe structure to determine which additional variables we still need to define:
for n=1 to NL:
(subsection[n])
[MAT,33,MT/ XMF1, XLFS1, MAT1, MT1, NC, NI]CONT
...
(/subsection[n])
endfor
The variables XMF1, XLFS1`, MAT1 and MT1
specify to which other material/cross section combination
we want to define the covariance matrix, see also
manual page 295.
Because we are in the MF=33/MT=1 section for a covariance
matrix to a total cross section and we only provide the
matrix for that channel, by the convention laid out in
the manual, we have XMF1=0.0, XLFS1=0.0, MAT1=2625
and MT1=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'] = endf_dict['1/451/MAT']
endf_dict['33/1/subsection[1]/MT1'] = 1
The complete covariance matrix can be composed of summing up several
covariance matrices, perhaps containg contributions associated
with different sources of uncertainty. Each of these contributions
is stored in its own sub-subsection. There are two types of sub-subsections
referred to as NI-type and NC-type. The variables NC and NI
indicate the number of sub-subsection of the respective type.
We only want to include a single NI-type sub-subsection, so we have
the specification:
endf_dict['33/1/subsection[1]/NC'] = 0
endf_dict['33/1/subsection[1]/NI'] = 1
There are different ways of storing the covariance matrix in
a sub-subsection. We will use the one indicated by
the variable assignment LB=5. Furthermore, due to our
covariance matrix being symmetric, we can use the flag LS=1
to indicate this situation and store only half of the elements.
These variables are described in the ENDF-6 formats manual (page 301).
We include here an abridged version of the relevant recipe part:
for m=1 to NI:
(ni_subsection[m])
if LB>=0 and LB<=4 [lookahead=1]:
...
if LB==5 and LS==1 [lookahead=1]:
...
endif
(/ni_subsection[m])
endfor
The lookahead=1 specification informs the parser that
the variable names used in the logical expression may
be only defined within a certain number of ENDF records
(here 1) inside the if-branch, which is the
the case for the LB and LS variable.
To use the representation associated with LB=5 and
LS=1 (manual page 303), we need to introduce the corresponding variables
in our dictionary:
endf_dict['33/1/subsection[1]/ni_subsection[1]/LB'] = 5
endf_dict['33/1/subsection[1]/ni_subsection[1]/LS'] = 1
The ENDF record specifications inside the LB=5, LS=1
branch of the if-block tell us how the covariance matrix
should be stored:
NT := NE*(NE+1)/2
[MAT,33,MT/ 0.0, 0.0, LS, LB, NT, NE/
{ E[k] }{ k=1 to NE }
{ {F[k,kp] }{ kp=k to NE-1} }{ k=1 to NE-1 } ]LIST
The variables LB and LS have already been defined above.
The variable NE contains the number of energy mesh points
for the covariance matrix. We use the same mesh as for
MF3/MT1 with five mesh points (endf_dict['3/1/xstable/E']),
so NE=5.
NT is a placeholder variable whose value is defined in terms
of other variables, in our example NE. We can ignore this variable
as any definition of this variable in the dictionary would be ignored
by the parser.
The energy mesh of the covariance matrix is stored in the array
E[k]. Importantly, E is a Python dictionary with contiguous
integer keys covering the range from 1 to NE. We can
set up this dictionary with this code snippet:
energies = endf_dict['3/1/xstable/E']
endf_dict['33/1/subsection[1]/ni_subsection[1]/NE'] = len(energies)
endf_dict['33/1/subsection[1]/ni_subsection[1]/E'] = \
{k: v for k, v in enumerate(energies, start=1)}
The first instruction retrieves the energies
from MF3/MT1 (given as a list object).
The second instruction assigns the correct value to NE.
In the assignment accomplished by the third instruction,
the energies list is converted to a dict data type.
As the integer keys start at 1 (as can be inferred from
{E[k]}{k=1 to NE} in the recipe snippet above, we need
to specify the argument start=1 in the call to the
enumerate() function.
You may also consider the section that elaborated on
the modification of arrays
Finally (yes, we are almost done!), we need to put the covariance
matrix into the two-dimensional array F (again a repurposed
nested Python dictionary). The following code snippet achieves
this task:
NE = endf_dict['33/1/subsection[1]/ni_subsection[1]/NE']
endf_dict['33/1/subsection[1]/ni_subsection[1]/F'] = {}
F = endf_dict['33/1/subsection[1]/ni_subsection[1]/F']
for k in range(1, NE):
for kp in range(k, NE):
F[k, kp] = float(covmat[k-1, kp-1])
The variable covmat was set up at the very beginning
of this section.
The structure of the two nested for-loops exactly reflects
the structure in the ENDF-6 recipe definition
({ {F[k,kp] }{ kp=k to NE-1} }{ k=1 to NE-1 }).
Please note the notation F[k, kp] to set elements in
the nested dictionary is only possible because
F is an EndfDict object.
After this quite lengthy explanation with all the pointers to the ENDF-6 formats manual and also linking the building process of the dictionary to the relevant parts in the ENDF-6 recipe, here is the Python code that assembles all the code snippets introduced in this section in a single script:
import numpy as np
covmat = np.diag([0.1, 0.2, 0.3, 0.4])
endf_dict['33/1'] = {}
endf_dict['33/1/MAT'] = endf_dict['1/451/MAT']
endf_dict['33/1/ZA'] = endf_dict['1/451/ZA']
endf_dict['33/1/AWR'] = endf_dict['1/451/AWR']
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'] = endf_dict['1/451/MAT']
endf_dict['33/1/subsection[1]/MT1'] = 1
endf_dict['33/1/subsection[1]/NC'] = 0
endf_dict['33/1/subsection[1]/NI'] = 1
endf_dict['33/1/subsection[1]/ni_subsection[1]/LB'] = 5
endf_dict['33/1/subsection[1]/ni_subsection[1]/LS'] = 1
energies = endf_dict['3/1/xstable/E']
endf_dict['33/1/subsection[1]/ni_subsection[1]/NE'] = len(energies)
endf_dict['33/1/subsection[1]/ni_subsection[1]/E'] = \
{k: v for k, v in enumerate(energies, start=1)}
NE = endf_dict['33/1/subsection[1]/ni_subsection[1]/NE']
endf_dict['33/1/subsection[1]/ni_subsection[1]/F'] = {}
F = endf_dict['33/1/subsection[1]/ni_subsection[1]/F']
for k in range(1, NE):
for kp in range(k, NE):
F[k, kp] = float(covmat[k-1, kp-1])
Adding the TPID record
Complete ENDF-6 files also contain a TPID record at the very beginning of the file (see manual page 52). It is a TEXT record whose text field is ignored and hence may be used for version control information. The MAT number is given by a tape number NTAPE, which we can set to one. The MF and MT numbers in the control record must be zero. Also the TPID record is defined in an ENDF-6 recipe file and we can add this record to our dictionary using the following assignments:
endf_dict['0/0/MAT'] = 1
endf_dict['0/0/TAPEDESCR'] = 'some ignored description in the TPID record'
Updating the ENDF directory
Being (hopefully) statisfied with our data, we need to wrap it up by updating the directory in MF1/MT451. The information we have at present in the directory is given by:
>>> endf_dict['1/451/MFx']
{1: 1}
>>> endf_dict['1/451/MTx']
{1: 451}
>>> endf_dict['1/451/NCx']
{1: 5}
>>> endf_dict['1/451/MOD']
{1: 1}
In words: During the construction of the MF1/MT451 we’ve included
a directory that accounted only for this section. As we have
added an MF3/MT1 and also MF33/MT1 section, this information
is outdated. We can update it using the
update_directory() function:
from endf_parserpy import update_directory
update_directory(endf_dict, parser)
Let’s check the udpated values:
>>> endf_dict['1/451/MFx']
{1: 1, 2: 3, 3: 33}
>>> endf_dict['1/451/MTx']
{1: 451, 2: 1, 3: 1}
>>> endf_dict['1/451/NCx']
{1: 13, 2: 5, 3: 6}
>>> endf_dict['1/451/MOD']
{1: 1, 2: 0, 3: 0}
The result of the first two instructions indicate the MF/MT sections present and the third instruction the number of ENDF records associated with each of these sections, so we have:
MF1/MT451: 13 ENDF records
MF3/MT1: 5 ENDF records
MF33/MT1: 6 ENDF records
To check whether the update_directory() function
has done a good job,
let’s convert the data in endf_dict into the ENDF-6 format and print it:
lines = parser.write(endf_dict)
print('\n'.join(lines))
This yields the following output:
2.605400+4 5.347625+1 2 0 45 12654 1451 1
0.000000+0 0.000000+0 0 0 0 62654 1451 2
1.000000+0 2.000000+8 0 0 10 12654 1451 3
0.000000+0 0.000000+0 0 0 6 32654 1451 4
26-FE- 54 MyLab EVAL-JAN24 John Doe 2654 1451 5
ABC-2023 DIST-FEB24 REV2-MAR24 20240315 2654 1451 6
----LIBPROJ-1 MATERIAL 2654 2654 1451 7
-----INCIDENT NEUTRON DATA 2654 1451 8
------ENDF-6 FORMAT 2654 1451 9
A new file is born! 2654 1451 10
1 451 13 12654 1451 11
3 1 5 02654 1451 12
33 1 6 02654 1451 13
0.000000+0 0.000000+0 0 0 0 02654 1 099999
0.000000+0 0.000000+0 0 0 0 02654 0 0 0
2.605400+4 5.347625+1 0 0 0 02654 3 1 1
0.000000+0 0.000000+0 0 0 1 52654 3 1 2
5 2 2654 3 1 3
1.000000+0 1.000000+1 2.000000+0 1.100000+1 3.000000+0 1.200000+12654 3 1 4
4.000000+0 1.300000+1 5.000000+0 1.400000+1 2654 3 1 5
0.000000+0 0.000000+0 0 0 0 02654 3 099999
0.000000+0 0.000000+0 0 0 0 02654 0 0 0
2.605400+4 5.347625+1 0 0 0 1265433 1 1
0.000000+0 0.000000+0 2654 1 0 1265433 1 2
0.000000+0 0.000000+0 1 5 15 5265433 1 3
1.000000+0 2.000000+0 3.000000+0 4.000000+0 5.000000+0 1.000000-1265433 1 4
0.000000+0 0.000000+0 0.000000+0 2.000000-1 0.000000+0 0.000000+0265433 1 5
3.000000-1 0.000000+0 4.000000-1 0.000000+0 0.000000+0 0.000000+0265433 1 6
0.000000+0 0.000000+0 0 0 0 0265433 099999
0.000000+0 0.000000+0 0 0 0 02654 0 0 0
0.000000+0 0.000000+0 0 0 0 0 0 0 0 0
0.000000+0 0.000000+0 0 0 0 0 -1 0 0 0
Taking into account that the SEND (=Section end) records are not considered
for the counting,
we see that update_directory()
has indeed determined the correct number of ENDF records of each MF/MT section.
Summary
If you have followed along until here, you have a Python dictionary with
a general description, (dummy) total cross section data, and a
covariance matrix associated with the total cross section.
Congratulations! You can produce your awesome ENDF-6 file
with the writefile() method:
parser.writefile('output.endf', endf_dict)
The Python code that has been introduced in this tutorial in a step-by-step manner is listed in its full form in the following Python script:
from endf_parserpy import EndfParserPy
from endf_parserpy import EndfDict
from endf_parserpy import update_directory
import numpy as np
parser = EndfParserPy()
endf_dict = EndfDict()
# definition of MF1/MT451
endf_dict['1/451'] = {}
p = endf_dict['1/451']
p['MAT'] = 2654; p['ZA'] = 26*1000. + 54
p['AWR'] = 53.47625 ; p['LRP'] = 2; p['LFI'] = 0
p['NLIB'] = 45; p['NMOD'] = 1; p['ELIS'] = 0.0
p['STA'] = 0; p['LIS'] = 0; p['LISO'] = 0
p['NFOR'] = 6; p['AWI'] = 1.0; p['EMAX'] = 2e8
p['LREL'] = 0; p['NSUB'] = 10; p['NVER'] = 1
p['TEMP'] = 0.0; p['LDRV'] = 0; p['NWD'] = 6
p['NXC'] = 1; p['ZSYMAM'] = ' 26-FE- 54 '
p['ALAB'] = 'MyLab'.ljust(11)
p['EDATE'] = 'EVAL-JAN24'
p['AUTH'] = 'John Doe'.ljust(33)
p['REF'] = 'ABC-2023'.ljust(21)
p['DDATE'] = 'DIST-FEB24'
p['RDATE'] = 'REV2-MAR24'
p['ENDATE'] = '20240315'
p['HSUB/1'] = (f'----LIBPROJ-{p["NVER"]}'.ljust(22) + f'MATERIAL {p["MAT"]}').ljust(66)
p['HSUB/2'] = '-----INCIDENT NEUTRON DATA'.ljust(66)
p['HSUB/3'] = '------ENDF-6 FORMAT'.ljust(66)
p['DESCRIPTION/1'] = 'A new file is born!'.ljust(66)
p['MFx/1'] = 1; p['MTx/1'] = 451; p['NCx/1'] = 5; p['MOD/1'] = p['NMOD']
# definition of MF3/MT1
endf_dict['3/1/MAT'] = endf_dict['1/451/MAT']
endf_dict['3/1/AWR'] = endf_dict['1/451/AWR']
endf_dict['3/1/ZA'] = endf_dict['1/451/ZA']
endf_dict['3/1/QM'] = 0.0
endf_dict['3/1/QI'] = 0.0
endf_dict['3/1/LR'] = 0
endf_dict['3/1/xstable/NBT'] = [5]
endf_dict['3/1/xstable/INT'] = [2]
endf_dict['3/1/xstable/E'] = [1.0, 2.0, 3.0, 4.0, 5.0]
endf_dict['3/1/xstable/xs'] = [10.0, 11.0, 12.0, 13.0, 14.0]
# definition of MF33/MT1
covmat = np.diag([0.1, 0.2, 0.3, 0.4])
endf_dict['33/1'] = {}
endf_dict['33/1/MAT'] = endf_dict['1/451/MAT']
endf_dict['33/1/ZA'] = endf_dict['1/451/ZA']
endf_dict['33/1/AWR'] = endf_dict['1/451/AWR']
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'] = endf_dict['1/451/MAT']
endf_dict['33/1/subsection[1]/MT1'] = 1
endf_dict['33/1/subsection[1]/NC'] = 0
endf_dict['33/1/subsection[1]/NI'] = 1
endf_dict['33/1/subsection[1]/ni_subsection[1]/LB'] = 5
endf_dict['33/1/subsection[1]/ni_subsection[1]/LS'] = 1
energies = endf_dict['3/1/xstable/E']
endf_dict['33/1/subsection[1]/ni_subsection[1]/NE'] = len(energies)
endf_dict['33/1/subsection[1]/ni_subsection[1]/E'] = \
{k: v for k, v in enumerate(energies, start=1)}
NE = endf_dict['33/1/subsection[1]/ni_subsection[1]/NE']
endf_dict['33/1/subsection[1]/ni_subsection[1]/F'] = {}
F = endf_dict['33/1/subsection[1]/ni_subsection[1]/F']
for k in range(1, NE):
for kp in range(k, NE):
F[k, kp] = float(covmat[k-1, kp-1])
# add the TPID record
endf_dict['0/0/MAT'] = 1
endf_dict['0/0/TAPEDESCR'] = 'some ignored description in the TPID record'
# update the directory in MF1/MT451
update_directory(endf_dict, parser)
# output as a file
parser.writefile('output.endf', endf_dict)