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:
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.
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.
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.
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:
Variables names must start with a letter e.g
vbormv1. Starting with a number is not valid.The remainder of your variable name may consist of letters, numbers and underscores. For example,
my_first_variable,surface001.Variable names are case sensitive. That is,
appleandApplewill 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:
Signed integers, e.g,
>>> a = 1 >>> b = -10Real (or floating point) numbers:
>>> f = 1.456 >>> x = 1.034E-12Booleans:
>>> p = True >>> q = FalseStrings:
>>> name = 'Tired' >>> fruit = "Apple"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 gramMultiplication 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_voltWhen 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', _]]
Interface elements
There are three major ways in which the user interfaces with the system:
Set data to pre-defined sets of parameter containers. This is the primary way in which data is passed to applications.
Use function calls or methods to set more dynamic data.
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.