A Quick Introduction to the Python Interface

Introduction

From a user’s perspective, switching to a Python based interface has many potential benefits. However, the added flexibility can be quite daunting, as the background machinery is extensive. Fortunately, you do not need to have any prior experience with Python (nor any other scripting or programming language), to get started. This chapter will introduce the basics of the interface, and explain many of the constructs that will be encountered later in the manual.

A living input file

Before continuing, it is worth comparing traditional text file based code input, to input provided via a python interface. Firstly, since both methods need to gather data from the user which defines how the target program will do its work, both will have a certain number of variables (or data cards) whose values should be set. In that sense, apart from a small amount of necessary python boilerplate, there is no real difference in the two approaches, and it is only a matter of reading the manual to find out which data goes where. There are, however, a number of important differences:

  1. Unlike a traditional system, not all cards and options are always available, and you must specify which ones are required using the import mechanism. In most cases, this is accomplished by adding a few (as little as 2) lines, and is usually a clear reflection of what the input is trying to accomplish, and what in the system is used to accomplish it. As we will illustrate later in the manual, the import mechanism can also be used to share data between your input scripts.

  2. In the python interface, your input is not restricted to what the target system understands. Anything interpretable by python is allowed. For instance, calculations (of arbitrary complexity) to obtain a certain input parameter can be performed directly in the input script. If used correctly, this greatly increases the traceability and repeatability of your input.

  3. Your python input naturally scales with the underlying complexity of the problem, that is, the more complex the problem, the more complex your input script tends to be.

  4. A potential drawback of the python interface is that, due to broad scope of the interpreter and the shear amount of tools available, input accomplishing the same thing can look very, very different. This is not so uncommon even in traditional systems, but here different styles and mastery of python can have a much larger impact.

So, instead of viewing your input as a static recipe, rather use it as a worksheet, containing all your scratch calculations, comments and useful bits of data from other input scripts. These are the features that are generally not available in traditional text based input.

Python basics

The python interpreter

An interpreter is the machinery that translates your input into instructions that the underlying system understands. Python provides an interactive interpreter session, where you can directly see the effect of your input. To launch an interpreter session, simply type the following in a terminal

$ python

Attention

If you are working in a terminal or Anaconda Prompt, remember to activate the correct environment first:

$ conda activate rapyds
(rapyds) $ python

When running a particular input script, the python interpreter will be called on each line of the script. Thus, you can emulate the behavior by entering each command interactively in the interpreter session. For example, suppose you have an input script that contains the following:

from core import *

a = 10
v = 5 * units.cm
c = a * v

Its behavior can be emulated in the interpreter as follows:

>>> from core import *
>>> a = 10
>>> v = 5 * units.cm
>>> c = a * v

The python interpreter is a great way to get familiar with the interface, check the effect of your inputs, and get online help.

Creating variables and assigning data

Creating a variable is as simple as picking a name and assigning a value to it using the equal sign:

>>> a = 5

This variable a now has the value 5, which can be checked as follows:

>>> a
5

Variable names can be as short or descriptive as you want. The rules are quite simple:

  1. Variables names must start with a letter e.g vb or mv1. Starting with a number is not valid.

  2. The remainder of your variable name may consist of letters, numbers and underscores. For example, my_first_variable, surface001.

  3. Variable names are case sensitive. That is, apple and Apple will be two different variables.

You can at any point override an existing variables value:

>>> a = 5
>>> a = 7
>>> a
7

Basic python data types

Python has the following 5 basic data types:

  1. Signed integers, e.g,

    >>> a = 1
    >>> b = -10
    
  2. Real (or floating point) numbers:

    >>> f = 1.456
    >>> x = 1.034E-12
    
  3. Booleans:

    >>> p = True
    >>> q = False
    
  4. Strings:

    >>> name = 'Tired'
    >>> fruit = "Apple"
    
  5. A special type used to denote an empty variable:

    >>> y = None
    

Operations on integers and floats are performed using the usual suspects: +, -, *, /, for addition, subtraction, multiplication, and division respectively. For example:

>>> x = 0.1 * 8
>>> x
0.8
>>> y = x / 8
>>> y
0.1
>>> y = y + 0.9
>>> y
1.0

Note that you can append the = sign to perform an operation directly to a variable, that is,

>>> x = x + 1

is equivalent to

>>> x += 1

Warning

When dividing integers, python will default to (rounded down) integer division, e.g

>>> 8 / 3
2

as opposed to

>>> 8.0 / 3.0
2.6666666666666665

Basic containers

For certain parameters, input must be specified as a collection of objects. The following describes three basic python container types.

Lists

A list is a basic array, containing any collection of data types. It is created using matching square brackets:

>>> l = [1, 2, 3, 10]

You can query the length of a list using the len command:

>>> len(l)
4

Elements in a list are accessed using the get [i] method, where the index i runs from 0 to len(l)-1:

>>> l[0]
1
>>> l[2]
3

You can also change the value at a particular index by assigning new data to it:

>>> l[1] = 5
>>> l[1]
5

Finally, two lists can be combined using the + operator:

>>> a = l + [9, 10]
>>> print(a)
[1, 5, 3, 10, 9, 10]

and constant lists can be easily generated using the * operator:

>>> a = [9] * 5
>>> print(a)
[9, 9, 9, 9, 9]

Note

Lists need not only contain data of a single type, that is,

>>> m = [1, 2.8, 'apple']

is a perfectly valid list. However, in the large majority of input specified to the system, lists are assumed have entries of the same type. The tuple container is used when mixed data types are expected.

Dictionaries

Dictionaries are general containers of key, value pairs. It is created using matching curly brackets, with key and values separated by a colon. For example,

>>> d = {'a' : 1, 'b': 2}

will have keys 'a', 'b' with corresponding values 1 and 2. You can access and modify values using their keys:

>>> d['a']
1
>>> d['b'] = 5
>>> d['b']
5

A dictionary is automatically extended if you assign a value to a key not already in the container:

>>> d['c'] = 10
>>> print(d)
{'a': 1, 'c': 10, 'b': 5}

Strings and integers are typically used as keys, while values can be of any type.

Tuples

A tuple is another basic collection of data types. It is constructed using matching round brackets:

>>> t = (1, 'bean', 4.9)

Like a list, data members of a tuple can be accessed using their index:

>>> t[1]
'bean'

However, unlike a list, values can not be modified. Nor can data be added or removed. In the interface, tuples are used to pass static sized data of multiple types.

System interface basics

Like any python based system, the rapyds interface is organized into a number of packages. The most important packages are:

Package

Description

core

Fundamental system functionality. Most input will use elements of this package.

material_library

Pre-defined material types.

applications

Package containing different applications or run modes. An input script will usually target one module in this package.

csg

The Constructive Solid Geometry package, which is used in the construction of an assembly library

Each package contains a number of modules, which in turn contain the elements you will use in your input scripts. To access these modules, you need to import them, which is described next.

The import mechanism

To gain access to the internals of a particular package, it needs to be imported. This is achieved through the import statement. In python the import mechanism is very flexible, but for now we will only focus on exposing the entire functionality of a package:

from package_name import *

For example,

>>> from core import *

will make the following modules available:

Module

Description

units

System of physical dimensions and units.

material

Used to define and manipulate isotopes and material mixtures of isotopes.

assembly

Used when creating and manipulating components and assemblies.

control

Define and manipulate reactor control elements (control rods and banks).

energy_condensation

Defines built-in group structures, as well as methods used to manipulate energy group structures.

geometry

Basic geometry manipulation (transformations etc).

state

Set and manipulate reactor or assembly states (temperatures, densities etc.)

boundary_conditions

Defines different boundary condition options.

reactions

Defines various reaction channel tags (capture, fission, n2n, etc.).

utilities

Basic utility module, primarily used for operating system interface (e.g. obtain file paths).

fuel_bundles

Custom depletion mesh objects.

In addition, this bulk import also makes the following symbols and tags available:

Tag/Symbol

Description

_, _p, _p1

Place holder tokens.

material_tags

Material type tags.

bundle_tags

Burnable structure tags.

travel

Control structure position tags.

state_parameters

State parameter tags.

In a similar fashion, the various sub modules can also be imported from the other packages. These will be encountered later in the manual, and for now we focus on the content of the core package. The actual elements you will use to set and manipulate input are contained within these modules.

The various ways in which elements from these modules are used are described in Interface elements, while Additional data types describes the new data types and containers added by the system.

The member of operator

Before continuing, we must introduce one crucial bit of python notation. As mentioned earlier, the system is structured in layers, starting with packages, then modules which contain the interface elements. These elements themselves can also be containers of elements. In short, the system uses a nested structure, with elements grouped by functionality and purpose. To access elements in this nested structure, the python member of . operator is used. For example, to access the fuel temperature state, which is contained in the parameter element of the state module, use

>>> sp = state.parameter.fuel_temperature

This operator will be encountered over and over in the remainder of this section, so it is important to understand its purpose.

Additional data types

This section describes the most important new data types, extending the built-in python data types and containers.

Note

This introduction is by no means exhaustive. For a complete list see Data Types.

Units

The python interface for OSCAR-5 incorporates a complete set of dimensions and units. Any dimensioned variable must in fact be assigned a variable with the appropriate unit. All unit names and abbreviations are available from the units container, and a dimensioned quantity is produced by multiplying an integer or real value with a unit. For example, to create a variable with length dimensions:

>>> x = 5.0 * units.cm

Variables can be transformed to any other unit with the same dimension, for example:

>>> x = 5.0 * units.cm
>>> y = x.to(units.inches)
>>> print(y)
1.96850393701 inch

The unit system is built on the excellent pint package. It includes all SI and Imperial units, using standard abbreviations. We have also introduced a number of new units, specific to reactor analysis applications. See Additional Units for a complete list.

Operations +, -, *, / can also be applied to dimensioned variables, with the following caveats:

  • Sum and difference can only be applied to variables with the same dimensions. In particular, adding or subtracting a scalar from a dimensioned quantity will produce an error. The units however, need not be the same. For example,

    >>> x = 1.0 * units.g + 3 * units.kg
    >>> print(x)
    3001.0 gram
    
  • Multiplication or division of a dimensioned quantity with a scalar will produce a quantity with the same dimensions, for example

    >>> x = 3.5 * units.MeV
    >>> x *= 10
    >>> print(x)
    35.0 megaelectron_volt
    

    When multiplying or dividing dimensioned quantities, the resulting dimension will change. For example.

    >>> x = 1.0 * units.g
    >>> v = 2.0 * units.cc
    >>> d = x / v
    >>> print(d)
    0.5 gram / cc
    >>> print(d.to(units.kg / (units.m * units.m * units.m)))
    500.0 kilogram / meter ** 3
    

Labeled grids

Labeled grids are used throughout to specify core maps and load positions. This data type is constructed from a list of lists, with each row a new list of the same. The first row is interpreted as column headers, and the first entry in subsequent rows are used as row labels. Thus, to create a \(m\times n\) grid, you would specify a list containing \(m+1\) lists, each of length \(n+1\). For example:

from core import *

g = \
[['-',   'A',   'B',   'C',   'D'],
 ['1', 'F01', 'F04', 'F07', 'F10'],
 ['2', 'F02', 'F05', 'F08', 'F11'],
 ['3', 'F03', 'F06', 'F09', 'F12']]

lb = utilities.create_labeled_grid(g)

Row and column labels can be any string or integer.

Values in the grid can be accessed using row and column labels:

>>> lb[0][0]
'F01'

or the row and column label:

>>> lb['1A']
'F01'

Note

For most parameters expecting a labeled grid, the utilities.create_labeled_grid is not necessary, as the system will automatically construct one from the raw data g.

Placeholders

Placeholder tokens are used to either specify an empty position in a grid, or a position that will be filled by some other mechanism. The empty place holder _ is used to denote a position in a map with no data,

from core import *

g = \
[['-',   'A',   'B',   'C',   'D'],
 ['1',     _, 'F04', 'F07',     _],
 ['2', 'F02', 'F05', 'F08', 'F11'],
 ['3',     _, 'F06', 'F09',     _]]

Tags

In the new interface, strings are only used for naming objects, paths and files. Thus, string variables are only defined by the user, and there is no pre-defined set of strings reserved for system functionality. Instead, the system relies on variables or tag objects. This is, in part, to better adhere to python conventions (be more pythonic), but has the added benefit of making these tags part of the interface (with auto complete and online help), and not only be hidden somewhere in a manual.

Tag objects are used in almost all the modules, and will be introduced in their relevant contexts throughout the manual. For now, to get familiar with the concept, we will look at material type tags, located in the material module. The tags are collected in the material_tags container, and can be viewed using the help method (more on this later):

>>> material_tags.help()
control:  Tag used to flag a material as control type.
reflector:  Tag used to flag a material as reflector type.
void:  Material used to specify voided regions (e.g. beam tubes filled with gas).
moderator_and_structural_mixture:  Tag used to flag a material as a mixture between moderator and structural material.
gap:  Tag used to flag a material as the filler between fuel and cladding.
moderator:  Tag used to flag a material as moderator.
soluble_absorber:  Tag used to specify a soluble absorber material.
structural:  Tag used to flag a material as structural.
absorber:  Tag used to flag a material as absorber (alias for control).
generic:  Tag used to denote a generic material.
clad:  Tag used to flag a material as cladding.
self_cooled_channel:  Specify a material as self cooled.
burnable_absorber:  Tag used to flag a material as a burnable absorber.
fuel:  Tag used to flag a material as fuel.

There are always two ways to access tag object.

  1. Within special tag containers, e,g.

    >>> material_tags.fuel
    
  2. Constructed directly from the containing module,

    >>> material.fuel()
    

Interface elements

There are three major ways in which the user interfaces with the system:

  1. Set data to pre-defined sets of parameter containers. This is the primary way in which data is passed to applications.

  2. Use function calls or methods to set more dynamic data.

  3. Construct and use new data containers.

These three groups of interface elements are described in more detail in the following sections.

Assigning data to parameter sets and containers

All application input is passed by assigning values to attributes of a predefined parameter pack. For example,

>>> from applications.core_follow import *
>>> from core import *
>>> parameters.cycle_name = 'C0001'
>>> parameters.start_time = '1977/10/30 08:00:00'

This is similar to the more familiar keyword value assignment mechanism, except that in this case the keyword cycle_name belongs to the parameters set. In short, in the python interface, all keywords will belong to a container (or more correctly, an object), and must be accessed using the member of operator.

Note

In this case, the parameters container was made available by the from applications.core_follow import * statement, and there was no need to create a new instance.

Some variables are containers in their own right, and the sub variables are then assigned to in a nested fashion, using the member of operator. For example, the parameter pack contains the model attribute, which collects all data defining the calculational model (covered in detail here):

>>> parameters.model.core_pitches = 7.71 * units.cm, 8.1 * units.cm

The model attribute in turn, can also have a number of sub containers, so that the nested assignment can become quite deep. In this case, there frequently exists top level get/set methods to simply the input. For example,

>>> parameters.model.inventory_manager.inventory = 'Path_to_my_inventory'

is equivalent to

>>> parameters.set_inventory('Path_to_my_inventory')

Function calls

Function calls are most frequently used to add dynamic sets of data, whose length or contents is determined by the user. Typical examples are:

  • Cells added to an assembly,

  • Isotopes added to a material composition,

  • Depletion steps in an irradiation history,

  • Axial segments in a homogenization application

Data is passed to the function using positional and keyword arguments, delimited by round brackets. For example,

>>> parameters.irradiation_history.add_step(2.3 * units.days, power=3 * units.MW)

will add an irradiation step of duration 2.3 days, with an average power of 3 MW.

Attention

Positional arguments must be passed in the correct order, but keyword arguments can be passed in any order. Thus, for readability, it is recommended to always use the keyword value pair signature of functions. Thus, the above should be replaced with the equivalent:

>>> parameters.irradiation_history.add_step(duration=2.3 * units.days, power=3 * units.MW)

Note

Just like data attributes, functions belongs to an object or module, and is accessed using the member of operator.

Some functions also return objects, which can then be manipulated further, or used in the input. For instance, the above call will return an object containing the data defining the irradiation step, and

>>> step = arameters.irradiation_history.add_step(duration=2.3 * units.days, power=3 * units.MW)
>>> step.average_banks = travel.percent_extracted(65)

will modify its duration. Similarly, the add_cell function of an assembly will return a reference to the cell. The exact calling signature and what is returned, is described in the relevant documentation.

In addition to dynamic data, functions are also used to conveniently set or get static data, or as an alternative way to create new data containers. The system also provides an number of utility functions (available from the utilities module), that can be used to set paths, access files, etc. For example,

>>> utilities.path_relative_to(__file__, 'my_path')

will calculate the absolute path of ‘my_path’ relative to the current module.

Since all input are valid python modules, the advanced user will quickly realize how defining his own functions can simplify the creation of complicated input.

Creating new containers

The creation of containers (or objects), are required when a default container is not provided by the system, or the user whats to activate a particular option, requiring its own set of data. Typical examples include:

  • Creating a new material,

  • Creating a new assembly,

  • Customizing the way assembly inventories are imported

For example, when creating a new material, you must first choose the appropriate container depending on how the material composition will be specified (e.g. through mass fractions, atomic fractions or as densities). When using mass mass fractions, the material.MaterialMassFractionMixer container must be used:

>>> mixer = material.MaterialMassFractionMixer(density=2.7 * units.g/units.cc)

The above statement instantiates (creates) the data object, with the arguments passed to its initiation function. This works just just like a normal function call <Function calls>.

After the container is created, data can be assigned to it, and functions can be used just like in automatically generated parameter packs. For example,

>>> mixer.total = 1.0
>>> mixer.add('Al-27', 0.16)

Note

Usually only the parameters for which no reasonable default exists, and is required for the container to function, must be passed during construction. In the above example, the mixer needs a density value, since without it, the container won’t be able to initiate it’s internal data, nor perform any calculations.

Most data containers do not have these restrictions, and data can be passed during construction, or assigned to the instance after construction. For example,

>>> asm = assembly.Assembly(name='some_name')

is equivalent to

>>> asm = assembly.Assembly()
>>> asm.name = 'some_name'

Attention

Remember to add the rounded brackets even if no parameters are passed during construction! That is,

>>> asm = assembly.Assembly

is not equivalent to

>>> asm = assembly.Assembly()

Alternatively, instead of constructing the container, many modules provide factory functions which creates these objects. For example, the construction of material.MaterialMassFractionMixer can be replaced with

>>> mixer = material.create_mass_fraction_mixer(density=1.0 * units.g / units.cc)

Note

In the rapyds package, all data containers (or classes) are named using the CamelCase convention, with the first letter also capitalized. Functions and variables are named using the snake case convention.

Containers (or classes) can be seen as small functional components, which can perform a number of tasks, using its own set of input data. Thus, they are also used to define and configure certain options in input modules. For example, the collection of history importers all have the same purpose (to initiate an assembly inventory), but they do it differently, and accept different input. Thus choosing one of these importers is equivalent to specifying how inventories should be initiated. This, and other examples will be encountered throughout the manual.