1. Framework Architecture

What follows is a discussion of the high-level elements of the ARMI framework. Throughout, links to the API docs will be provided for additional details.

1.1. The Reactor Data Model

The ARMI framework represents a nuclear reactor via a reactor data model, which is defined in the reactor package. Each physical piece of the nuclear reactor is defined by a Python object, called an ArmiObject. Each ArmiObject has associated data like: shape, material, or other physical values. The physical values can be nearly anything, and are attached to the data model via ARMI’s Parameter system. Example parameters might be: keff, flow rates, power, flux, etc.

The reactor data model is a hierarchical model, following the Composite Design Pattern. The top of the data model is the Reactor, which contains one Core object and a collection of zero or more ExcoreStructures. An example ExcoreStructure might be a SpentFuelPool.

For now, the Core object in ARMI assumes it contains Assembly objects, which are in turn made up as a collection of Block objects. The leaves of the the Composite Model in the ARMI framework are called Component.

../_images/armi_reactor_objects.png

The primary data containers in ARMI

Time-evolving the parameters on the reactor composite hierarchy is what most modelers and analysts will want from the ARMI framework.

Review the data model Tutorials section for examples exploring a populated instance of the Reactor data model.

1.1.1. Finding objects in a model

Under most circumstances a armi.reactor.reactors.Reactor instance will have a .core attribute, which is an instance of armi.reactor.reactors.Core. While the Composite pattern discussed above can be used very generally, the Core class enforces a couple of constraints that can be very useful:

In many scenarios, one wants to access specific assemblies or blocks from a core. There are a few ways to get the objects that you’re interested in:

  • The r.core.childrenByLocator dictionary maps armi.reactor.grids.IndexLocation objects to whichever assembly is at that location. For example

    >>> loc = r.core.spatialGrid[i, j, 0]
    >>> a = r.core.childrenByLocator[loc]
    

    To access the k -th block in an assembly, try:

    >>> b = a[k]
    
  • r.core.getAssemblies() loops through all assemblies in the core for when you need to do something to all assemblies.

1.1.2. Parameters

One of the main benefits to ARMI is that it enables simple interfaces to extract data from the reactor, do something with it, and add new results to the reactor. This enables specialized developers to write code that uses ARMI as input and output.

Most data is stored in ARMI as parameters. Most parameters will become persistent, meaning they will be saved to the database during database interactions, and therefore it will also be loaded when a database is loaded.

Details of the use and design can be found at parameters.

1.1.3. Converters

The converters subpackage contains a variety of utilities that can convert a reactor model in various ways. Some converters change designs at the block level, adjusting pin dimensions or fuel composition. Others adjust the reactor geometry at large, changing a 1/3-symmetric model to a full core, or changing a hexagonal geometry to a R-Z geometry. Converters are used for parameter sweeps as well as during various physics operations.

For example, some lattice physics routines convert the full core to a 2D R-Z model and compute flux with thousands of energy groups to properly capture the spectral-spatial coupling in a core/reflector interface. The converters are used heavily in these operations.

1.1.4. Blueprints

As seen in the User Guide, blueprints are how reactor models are defined. During a run, they can be used to create new instances of reactor model pieces, such as when a new assembly is fabricated during a fuel management operation in a later cycle.

1.2. Operators

Operators conduct the execution sequence of an ARMI run. They basically contain the main loop. When any operator is instantiated, several actions occur:

  1. Some environmental detail is printed out,

  2. A Reactor object is instantiated

  3. Loading and geometry input files are processed and the reactor object is populated with assemblies,

  4. The interfaces are instantiated and placed in the Interface Stack during the createInterfaces method call,

  5. The interactInit method is called on all interfaces, and

  6. Restart information is processed (if this is a restart run).

After that, depending on the type of Operator at hand, one of several operational loops will begin via operate(). Operator types are chosen by the runType setting, which is featured on the first tab of the ARMI GUI.

1.2.1. The Standard Operator

The two primary types of operators are the Standard Operator (along with its parallel version, the OperatorMPI), and the OperatorSnapshots. The former runs a typical operational loop, which calls all the interfaces through their interaction hooks in a sequential manner, marching from beginning-of-life through the number of cycles requested. This is how most quasistatic fuel cycle calculations are performed, which inform much of the analysis done during reactor design. The main code for this loop is found in the mainOperate method. This operator supports restart/continuation of past runs from an arbitrary time step.

1.2.2. The Snapshots Operator

Alternatively, OperatorSnapshots is designed to allow for additional analyses at specific time steps. It simply loops through all snapshots that have been requested via the Snapshot Request functionality (Lists -> Edit snapshot requests in the GUI). At each snapshot request, the state is loaded from a previous case, as determined by the reloadDBName setting and then the BOC, EveryNode, and EOC interaction hooks are executed from all the interfaces. Snapshots are intended to analyze an exact reactor configuration. Therefore, interfaces which would significantly change the reactor configuration (such as Fuel management, and depletion) are disabled.

1.2.3. The Interface Stack

Interfaces (armi.interfaces.Interface) operate upon the Reactor Model to do analysis. They’re designed to allow expansion of the code in a natural and well-organized manner. Interfaces are useful to link external codes to ARMI as well for adding new internal physics into the rest of the system. As a result, very many aspects of ARMI are contained within interfaces.

The flow of any ARMI calculation depends on the order of the interfaces, which is set at initialization according to the user settings and the corresponding ORDER attributes in interface modules. The collection of the interfaces is known as the Interface Stack and is prominently featured at the beginning of the standard output of each run, like this:

[R  0 ] -------------------------------------------------------------------------------
[R  0 ]                        ***  Interface Stack Report  ***
[R  0 ] NUM   TYPE                 NAME                 ENABLED    BOL FORCE  EOL ORDER
[R  0 ] -------------------------------------------------------------------------------
[R  0 ] 00    Main                 "main"               Yes        No         Reversed
[R  0 ] 01    Software Testing     "softwareTests"      Yes        No         Reversed
[R  0 ] 02    ReportInterface      "report"             Yes        No         Reversed
[R  0 ] 03    FuelHandler          "fuelHandler"        Yes        No         Normal
[R  0 ] 04    Depletion            "depletion"          Yes        Yes        Normal
[R  0 ] 05    MC2-2                "mc2"                Yes        No         Normal
[R  0 ] 06    DIF3D                "dif3d"              Yes        No         Normal
[R  0 ] 07    Thermo               "thermo"             Yes        No         Normal
[R  0 ] 08    OrificedOptimized    "orificer"           Yes        Yes        Normal
[R  0 ] 09    AlchemyLite          "alchemyLite"        Yes        No         Normal
[R  0 ] 10    Alchemy              "alchemy"            Yes        No         Normal
[R  0 ] 11    Economics            "economics"          Yes        No         Normal
[R  0 ] 12    History              "history"            Yes        No         Normal
[R  0 ] 13    Database             "database"           Yes        Yes        Normal
[R  0 ] -------------------------------------------------------------------------------

Any interface that exists on the interface stack is accessible from the operator or from any other interface object through the getInterface method.

1.2.4. Interface Interaction Hooks

Various interfaces need to interact with ARMI at various times. The point at which routines are called during a run set by developers in interface hooks, as seen below. At each point in the flow chart, interfaces are interacted with one-by-one as the interface stack is traversed in order.

../_images/armi_general_flowchart.png

The computational flow of the interface hooks

For example, input checking routines would run at beginning-of-life (BOL), calculation modules might run at every time node, etc. To accommodate these various needs, interface hooks include:

  • interactInit occurs right after all interfaces are initialized.

  • interactBOL – Beginning of life. Happens once as the run is starting up.

  • interactBOC – Beginning of cycle. Happens once per cycle.

  • interactEveryNode – Happens after every node step/flux calculation.

  • interactEOC – End of cycle.

  • interactEOL – End of life.

  • interactError – When an error occurs, this can run to clean up or print debugging info.

  • interactCoupled – Happens after every node step/flux calculation, if tight physics coupling is active.

These interaction points are optional in every interface, and you may override one or more of them to suit your needs. You should not change the arguments to the hooks, which are integers.

Each interface has a enabled flag. If this is set to False, then the interface’s hook code will not be called even though the interface exists in the problem. This is useful for interfaces that use code from other interfaces. For example, if subchan is activated, it still uses some code in the thermo module to compute the fuel temperatures, so the thermo interface must be available in a getInterface call.

1.2.5. Adding a new interface

When using the Operators that come with ARMI, Interfaces are discovered using the Plugin API and inserted into the interface stack during the createInterfaces method.

1.2.6. How interfaces get called

The hooks of interfaces are called during the main loop in armi.operators.Operator.mainOperate(). There are a few special operator calls in there to methods like armi.operators.Operator.interactAllBOL() that loop through the interface stack and call each enabled interface’s interactBOL() method. If you override mainOperate in a custom operator, you will need to add these calls as deemed necessary to have the interfaces work properly.

To use interfaces in parallel, please refer to armi.mpiActions.

1.3. Plugins

Plugins are higher-level objects that can add things to the simulations like Interfaces, settings definitions, parameters, validations, etc. They are documented in Making ARMI-based Apps and armi.plugins.

1.3.1. Entry Points

ARMI has a set of Entry Points that can run cases, launch the GUI, and perform various testing and utility operations. When you invoke ARMI with python -m armi run, the __main__.py file is loaded and all valid Entry Points are dynamically loaded. The proper entry point (in this case, armi.cli.run.RunEntryPoint) is invoked. As ARMI initializes itself, settings are loaded into a Settings object. From those settings, an Operator subclass is built by a factory and its operate method is called. This fires up the main ARMI analysis loop and its interface stack is looped over as indicated by user input.