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")
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]:
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]:
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]:
In [9]:
infT = collector.xsTables['infTot']
In [10]:
infT.shape
Out[10]:
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]:
The ordering of each of these dimensions is found by examining the univIndex
, states
, and burnups
attributes.
In [12]:
collector.univIndex
Out[12]:
In [13]:
collector.states
Out[13]:
In [14]:
collector.burnups
Out[14]:
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]:
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]:
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]:
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]:
In [19]:
collector.universes
Out[19]:
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]:
In [22]:
u0.axis
Out[22]:
In [23]:
u0.states
Out[23]:
The contents of the xsTables
dictionary are numpy.array
s, views into the data stored on the BranchCollector
.
In [24]:
list(u0.xsTables.keys())
Out[24]:
In [25]:
u0Tot = u0.xsTables['infTot']
In [26]:
u0Tot.shape
Out[26]:
In [27]:
u0Tot
Out[27]:
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]:
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))
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]:
In [31]:
try:
collector.perturbations = ['boron', 'fuel', 'ctrl'] # wrong
except ValueError as ve:
print(str(ve))
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())
In [34]:
writer = Writer(collector)
In [35]:
print(writer.write.__doc__.strip())
In [36]:
# write to a file "in memory"
out = writer.write("0")
In [37]:
print(out[:1000])
In [ ]: