Copyright (c) 2017-2020 Serpent-Tools developer team, GTRC

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Data files are not included with the python package, but can be downloaded from the GitHub repository. For this tutorial, the files are placed in the directory identified with the SERPENT_TOOLS_DATA environment variable.


In [1]:
import os
coeFile = os.path.join(
    os.environ["SERPENT_TOOLS_DATA"],
    "demo.coe")

Coefficient file to nodal diffusion cross sections

A recent feature of SERPENT is the ability to performing branching calculations using the automated burnup sequence. serpentTools can read these coefficient files using the BranchingReader. This automated burnup sequence is ideal for generating group constant data for nodal diffusion codes, that often include some multi-physics features, criticality searches, or other control mechanisms. A criticality search could be performed by tweaking the boron concentration in the coolant or adjusting control rod insertions. Similarly, some codes may include coupled TH analysis to convert power profiles to temperature profiles and adjust cross sections accordingly. Each code has a unique flavor for utilizing a set of group constants across these perturbations, and this notebook will demonstrate using the BranchCollector to gather and write a simple set of cross sections.


In [2]:
import numpy
import serpentTools
from serpentTools.xs import BranchCollector

In [3]:
coe = serpentTools.read(coeFile)

This specific input file contained two perturbations: boron concentration and fuel temperature. Boron concentration had three branches: nom with no boron, then B1000 and B750, with 1000 and 750 ppm boron in coolant. Fuel temperature had a nominal branch at 900 K, with 1200 and 600 K perturbations as well. These can be confirmed by observing the branches dictionary on the BranchingReader


In [4]:
list(coe.branches.keys())


Out[4]:
[('nom', 'nom'),
 ('B750', 'nom'),
 ('B1000', 'nom'),
 ('nom', 'FT1200'),
 ('B750', 'FT1200'),
 ('B1000', 'FT1200'),
 ('nom', 'FT600'),
 ('B750', 'FT600'),
 ('B1000', 'FT600')]

Cross sections are spread out through this BranchingReader across branches, burnup, and universes. The job of the BranchCollector is to place that data into mutli-dimensional matrices that represent the perturbations chosen by the user. A single group constant, say total cross section, has unique values for each universe, at each burnup point, for each perturbed state, and each energy group. Such a matrix would then contain five dimensions for this case.

First, we create the BranchCollector from the BranchingReader and instruct the reader what perturbations are present in the file. The ordering is not important at this point, as it can be changed later.


In [5]:
collector = BranchCollector(coe)

In [6]:
collector.collect(('BOR', 'TFU'))

Now we can inspect the perturbation states, states, found by the collector.


In [7]:
collector.states


Out[7]:
(('B1000', 'B750', 'nom'), ('FT1200', 'FT600', 'nom'))

The group constants are stored in the xsTables dictionary. Here we select the total cross section, infTot for further exploration.


In [8]:
list(collector.xsTables.keys())


Out[8]:
['infTot',
 'infFiss',
 'infS0',
 'infS1',
 'infDiffcoef',
 'b1Tot',
 'b1Fiss',
 'b1S0',
 'b1S1',
 'b1Diffcoef']

In [9]:
infT = collector.xsTables['infTot']

In [10]:
infT.shape


Out[10]:
(5, 3, 3, 3, 2)

Five dimensions as mentioned above. But how are they ordered? Inspecting the axis attribute tells us that the dimensions are universe, boron concentration, fuel temperature, burnup, and energy group.


In [11]:
collector.axis


Out[11]:
('Universe', 'BOR', 'TFU', 'Burnup', 'Group')

The ordering of each of these dimensions is found by examining the univIndex, states, and burnups attributes.


In [12]:
collector.univIndex


Out[12]:
('0', '10', '20', '30', '40')

In [13]:
collector.states


Out[13]:
(('B1000', 'B750', 'nom'), ('FT1200', 'FT600', 'nom'))

In [14]:
collector.burnups


Out[14]:
array([ 0.,  1., 10.])

For example, if we wanted the total cross section for universe 10, at 1000 ppm boron, nominal fuel temperature, and 10 MWd/kgU burnup, we would request


In [15]:
infT[1, 0, 2, 2]


Out[15]:
array([0.324746, 0.864346])

For this example, the scattering matrices were not reshaped from vectors to matrices and we would observe slightly different behavior in the 'Group' dimension.


In [16]:
collector.xsTables['infS1'].shape


Out[16]:
(5, 3, 3, 3, 4)

Four items in the last axis as the vectorized matrix represents fast to fast, fast to thermal, thermal to fast, and thermal to thermal scattering.


In [17]:
collector.xsTables['infS1'][1, 0, 2, 2]


Out[17]:
array([0.087809  , 0.00023068, 0.00073939, 0.123981  ])

Many nodal diffusion codes request group constants on a per universe basis, or per assembly type. As we saw above, the first dimension of the xsTables matrices corresponds to universe. One can view group constants for specific universes with the universes dictionary.


In [18]:
collector.universes


Out[18]:
{'0': <serpentTools.xs.BranchedUniv at 0x7fefd982acb0>,
 '10': <serpentTools.xs.BranchedUniv at 0x7fefd982ad10>,
 '20': <serpentTools.xs.BranchedUniv at 0x7fefd982ad70>,
 '30': <serpentTools.xs.BranchedUniv at 0x7fefd982add0>,
 '40': <serpentTools.xs.BranchedUniv at 0x7fefd982ae30>}

In [19]:
collector.universes


Out[19]:
{'0': <serpentTools.xs.BranchedUniv at 0x7fefd982acb0>,
 '10': <serpentTools.xs.BranchedUniv at 0x7fefd982ad10>,
 '20': <serpentTools.xs.BranchedUniv at 0x7fefd982ad70>,
 '30': <serpentTools.xs.BranchedUniv at 0x7fefd982add0>,
 '40': <serpentTools.xs.BranchedUniv at 0x7fefd982ae30>}

In [20]:
u0 = collector.universes['0']

These BranchedUniv objects store views into the underlying collectors xsTables data corresponding to a single universe. The structuring is identical to that of the collector, with the first axis removed.


In [21]:
u0.perturbations


Out[21]:
('BOR', 'TFU')

In [22]:
u0.axis


Out[22]:
('BOR', 'TFU', 'Burnup', 'Group')

In [23]:
u0.states


Out[23]:
(('B1000', 'B750', 'nom'), ('FT1200', 'FT600', 'nom'))

The contents of the xsTables dictionary are numpy.arrays, views into the data stored on the BranchCollector.


In [24]:
list(u0.xsTables.keys())


Out[24]:
['infTot',
 'infFiss',
 'infS0',
 'infS1',
 'infDiffcoef',
 'b1Tot',
 'b1Fiss',
 'b1S0',
 'b1S1',
 'b1Diffcoef']

In [25]:
u0Tot = u0.xsTables['infTot']

In [26]:
u0Tot.shape


Out[26]:
(3, 3, 3, 2)

In [27]:
u0Tot


Out[27]:
array([[[[0.313696, 0.544846],
         [0.311024, 0.617734],
         [0.313348, 0.614651]],

        [[0.313338, 0.54515 ],
         [0.310842, 0.618286],
         [0.31299 , 0.614391]],

        [[0.31673 , 0.548305],
         [0.313987, 0.621804],
         [0.316273, 0.61812 ]]],


       [[[0.313772, 0.541505],
         [0.311335, 0.609197],
         [0.313311, 0.608837]],

        [[0.313437, 0.542373],
         [0.310967, 0.609192],
         [0.31316 , 0.608756]],

        [[0.316688, 0.545294],
         [0.314245, 0.612767],
         [0.316392, 0.612985]]],


       [[[0.20802 , 0.228908],
         [0.205774, 0.10707 ],
         [0.203646, 0.      ]],

        [[0.207432, 0.315208],
         [0.205326, 0.      ],
         [0.203533, 0.      ]],

        [[0.210873, 0.223528],
         [0.208646, 0.      ],
         [0.206532, 0.      ]]]])

Changing perturbation values

The values of states and perturbations can be easily modified, so long as the structures are preserved. For example, as the current states are string values, and of equal perturbations (three boron concentrations, three fuel temperatures), we can set the states to be a single 2x3 array


In [28]:
collector.states = numpy.array([
    [1000, 750, 0], 
    [1200, 600, 900]], 
    dtype=float)
collector.states


Out[28]:
array([[1000.,  750.,    0.],
       [1200.,  600.,  900.]])

Some error checking is performed to make sure the passed perturbations match the structure of the underlying data. Here, we attempt to pass the wrong number of fuel temperature perturbations.


In [29]:
try:
    collector.states = numpy.array([
        [1000, 750, 0],
        [1200, 600],  # wrong
    ])
except ValueError as ve:
    print(str(ve))


Current number of perturbations for state TFU is 3, not 2

If the specific perturbations were not known when creating the collector, the value of perturbations can also be changed, with similar error checking.


In [30]:
collector.perturbations = ['boron conc', 'fuel temperature']
collector.perturbations


Out[30]:
['boron conc', 'fuel temperature']

In [31]:
try:
    collector.perturbations = ['boron', 'fuel', 'ctrl']  # wrong
except ValueError as ve:
    print(str(ve))


Current number of perturbations is 2, not 3

Example nodal diffusion writer

As each nodal diffusion code has it's own required data structure, creating a general writer is a difficult task. The intent with the BranchCollector is to provide a framework where the data is readily available, and such a writer can be created with ease. Here, an example writer is demonstrated, one that writes each cross section. The writer first writes a table of the perturbations at the top of the input file, showing the ordering and values of the perturbations. Options are also provided for controlling formatting.

The full file is available for download: nodal_writer.py


In [32]:
from nodal_writer import Writer

In [33]:
print(Writer.__doc__.strip())


Class for writing an example cross section file.

    Parameters
    ----------
    collector: xs.Collector
        Object that read the branching file and stored the cross sections
        along the perturbation vector
    xsPerLine: int
        Number of cross sections / group constants to write per line
    floatFmt: str
        Formattable string used when writing floating point values
    strFmt: str
        Formattable string used when writing the names of the perturbations
    xsRemap: None or dict
        Dictionary used to find a replacement name for cross sections when
        writing.  Between each cross section block, the name of cross
        section and group will be written as ``# {name} group {g}``.
        When ``xsRemap`` is ``None``, the names are ``mixedCase`` as
        they appear in ``HomogUniv`` objects, e.g.  ``'infTot'``,
        ``'diffCoeff'``, etc. If ``xsRemap`` is a dictionary, it can
        be used to write a different name. Passing ``{'infTot': 'Total
        cross section'}`` would write ``'Total cross seciton'``
        instead of ``'infTot'``, but all other names would be unchanged.

In [34]:
writer = Writer(collector)

In [35]:
print(writer.write.__doc__.strip())


Write the contents of a single universe

        Parameters
        ----------
        universe: int or key
            Key of universe that exists in ``self.collector``. Typically
            integer values of homogenized universes from coefficient file
        stream: None or str or writeable
            If ``None``, return a string containing what would have been
            written to file. If a string, then write to this file. Otherwise,
            ensure that the object has a ``write`` method and write to this
            object
        mode: {'a', 'w'}
            Write or append to file. Only needed if stream is a string

In [36]:
# write to a file "in memory"
out = writer.write("0")

In [37]:
print(out[:1000])


# Cross sections for universe 0
boron conc           1.00000000E+03 7.50000000E+02 0.00000000E+00
fuel temperature     1.20000000E+03 6.00000000E+02 9.00000000E+02
Burnup [MWd/kgU]     0.00000000E+00 1.00000000E+00 1.00000000E+01
# infTot group 1
 3.13696000E-01 3.11024000E-01 3.13348000E-01 3.13338000E-01
 3.10842000E-01 3.12990000E-01 3.16730000E-01 3.13987000E-01
 3.16273000E-01 3.13772000E-01 3.11335000E-01 3.13311000E-01
 3.13437000E-01 3.10967000E-01 3.13160000E-01 3.16688000E-01
 3.14245000E-01 3.16392000E-01 2.08020000E-01 2.05774000E-01
 2.03646000E-01 2.07432000E-01 2.05326000E-01 2.03533000E-01
 2.10873000E-01 2.08646000E-01 2.06532000E-01
# infTot group 2
 5.44846000E-01 6.17734000E-01 6.14651000E-01 5.45150000E-01
 6.18286000E-01 6.14391000E-01 5.48305000E-01 6.21804000E-01
 6.18120000E-01 5.41505000E-01 6.09197000E-01 6.08837000E-01
 5.42373000E-01 6.09192000E-01 6.08756000E-01 5.45294000E-01
 6.12767000E-01 6.12985000E-01 2.28908000E-01 1.07070000E-01
 0.00000000E+00 3.1

In [ ]: