A macro is a mechanism to abstract complex input file sequences into (parameterized) tokens. In its simplest form, a macro provides a way to substitute a code snippet from an input file:
<macro snippet>
line1
line2
line3
</macro>
In this example, every occurrence of the code named snippet in the input
file will now be replaced by the three lines defined between the
<macro>
and </macro>
tags.
For example, you could define a macro to set up a laser pulse like this:
<macro myFluid>
equations = [euler]
<Equation euler>
kind = eulerEqn
gasGamma = GAMMA
</Equation>
</macro>
You could then call your myLaser macro within the input file like this:
<Updater hyper>
kind = classicMuscl1d
onGrid = domain
...
myFluid
</Updater>
The USim engine (USim) will expand the input file use of your macro into:
<Updater hyper>
kind = classicMuscl1d
onGrid = domain
...
equations = [euler]
<Equation euler>
kind = eulerEqn
gasGamma = GAMMA
</Equation>
</Updater>
It is also possible to define a macro file, and provided that it is in the same directory as your input file, import it. This is useful when writing one custom macro that will be used over multiple simulations. The macro must have a .mac extension on it to be imported as a local macro. To extend the example above, say the macro myLaser is in the file Lasers.mac, the input file would look like this:
$ import fluidModels.mac
<Updater hyper>
kind = classicMuscl1d
onGrid = domain
...
myFluid
</Updater>
USim will expand the input file use of your macro into:
<Updater hyper>
kind = classicMuscl1d
onGrid = domain
...
equations = [euler]
<Equation euler>
kind = eulerEqn
gasGamma = GAMMA
</Equation>
</Updater>
The macro definition would remain the exact same. As long as the macro file is imported properly, it is just like having it defined explicitly in the input file.
Macros can take parameters, allowing variables to be passed into and used by the macro. Parameters are listed in parentheses after the macro name in the macro declaration, as in this example:
<macro finiteVolumeData(name, grid, components, write)>
<DataStruct name>
kind = nodalArray
onGrid = grid
numComponents = components
numNodes = 1
writeOut = write
</DataStruct>
</macro>
Once a macro is defined, it can be used by calling it and providing values or symbols for the parameters. The macro will substitute the parameter values into the body provided. Calling the example above with parameters defined like this:
finiteVolumeData(density, domain, 1, true)
will create the following code fragment in the processed input file:
<DataStruct name>
kind = nodalArray
onGrid = grid
numComponents = components
numNodes = 1
writeOut = write
</DataStruct>
Note
The parameter substitution happened in the scope of the caller. Parameters do not have scope outside of the macro in which they are defined.
As with symbols, macros can be overloaded within a scope. The particular instance of a macro that is used is determined by the number of parameters provided at the time of instantiation. This enables the user to write macros with different levels of parameterization:
<macro circle(x0, y0, r)>
r^2 - ((x-x0)^2 + (y-y0)^2)
</macro>
<macro circle(r)>
circle(0, 0, r)
</macro>
Looking in the example above, whenever the macro circle is used with a single parameter, it creates a circle around the origin; if you use the macro with 3 parameters, you can specify the center of the circle.
The macro substitution does not occur until the macro instantiation is actually made. This means that you do not have to define the 3-parameter circle prior to defining the 1-parameter circle, even though the 1-parameter circle refers to the 3-parameter circle. It is only necessary that the first time the 1-parameter circle is instantiated, that 3-parameter circle has already been defined, otherwise you will receive an error.
Macros can be particularly useful for defining complex mathematical expressions, such as defining functions in expr lists.
Consider a macro that should simplify the setup of a Gaussian. One could define the following macro:
<macro badGauss(A, x, sigma)>
A * exp(-x^2/sigma)
</macro>
While this is a legitimate macro, an instantiation of the macro via:
badGauss(A0+5, x-3, 2*sigma)
will result in:
A0+5*exp(-x+3^3/2*sigma)
which is probably not the expected result. One alternative is to put parentheses around the parameters whenever they are used in the macro.
<macro betterGauss(A, x, sigma)>
((A) * exp(-(x)^2/(sigma)))
</macro>
This will ensure that the expressions in parameters will not cause any unexpected side effects. The downside of this approach, however, is that the macro text is hard to read due to all the parentheses. To overcome this issue, txpp provides a mechanism to automatically introduce the parentheses around arguments by using a function block
<function goodGauss(A, x, sigma)>
A * exp(-x^2/sigma)
</function>
The previous example will produce the same output as the badGauss macro, but without requiring the additional parentheses in the macro text.
USim allows input files to be split into individual files, thus enabling macros to be encapsulated into separate libraries. For example, physical constant definitions or commonly-used geometry setups can be stored in files that can then be used by many USim simulations. Input files can be nested to arbitrary depth.
Files are imported via the import keyword:
$ import FILENAME
where FILENAME represents the name of the file to be included. txpp applies the standard rules for token substitution to any tokens after the import token. Quotes around the filename are optional and computed filenames are possible.
The USim preprocessor includes both flow control and conditional statements, similar to other scripting languages. These features allow the user a great deal of flexibility when creating input files.
A conditional takes either the form:
$ if COND
...
$ endif
or
$ if COND
...
$ else
...
$ endif
Conditionals can be arbitrarily nested. All the tokens following the
if
token are interpreted following the expression evaluation
procedure (see above) and if they evaluate to true, the text following
the if statement is inserted into the output. If the conditional
statement evaluates to false, the text after the else
is
inserted (if present). Note that true
and false
in
preprocessor macros are evaluated by Python – in addition to evaluating
conditional statements such as x == 1
, other tokens can be
evaluated. The most common use of this is using 0
for false and
1
for true. Empty strings are also evaluated to false. For more
detailed information, consult the Python documentation.
$ if TYPE == "MHD"
$ numComponents = 9
$ else
$ numComponents = 5
$ endif
A conditional statement can also use Boolean operators:
$ A = 0
$ B = 0
$ C = 1
#
# Below, D1 is 1 if A, B, or C are non-zero. Otherwise D1=0:
$ D1 = (A) or (B) or (C)
#
# Below, D2 is 1 if A is non-zero or if both B and C are non-zero. Otherwise D2=0:
$ D2 = (A) or ( (B) and (C) )
#
# This can be also be written as an if statement:
$ if (A) or ( (B) and (C) )
$ D3 = 1
$ else $
$ D3 = 0
$ endif
For repeated execution, USim provides while loops; these take the form:
$ while COND
.
.
.
$ endwhile
which repeatedly inserts the loop body into the output. For example, to create 10 stacked circles using the circle macro from above, you could use:
$ n = 10
$ while n > 0 circle(n)
$ n = n - 1
$ endwhile
Macros can be called recursively. E.g. the following computes the Fibonacci numbers:
<macro fib(a)>
$ if a < 2
a
$ else
fib(a-1)+fib(a-2)
$ endif
</macro>
fib(7)
Note
There is nothing preventing you from creating infinitely recursive macros; if terminal conditions are not given for the recursion, infinite loops can occur.
txpp allows symbols to be defined on the command line. These definitions override any symbol definitions in the outer-most (global) scope. This allows you to set a default value inside an input file that can then be overridden on the command line if needed.
For example, if the following is in the outermost scope of the input file (outside of any blocks or macros):
$ X = 3
X
Then this will result in a line containing 3
in the output. However, if
you were to invoke txpp via:
txpp.py -DX=4
then this will result in a line 4
.
However, if you were to define X
inside a block (not in the global
scope), such as:
<block foo>
$ X = 3
X
</block>
then X
will always be 3
, no matter what value for X
is specified on the
command line.
When writing reusable macros, best practices compel macro authors to
help ensure that the user can be prevented from making obvious mistakes.
One such mechanism is the requires directive, which terminates
translation if one or more symbols are not defined at the time. This
allows users to write macros that depend on symbols that are not passed
as parameters. For example, the following code snippet will not be
processed if the symbol NDIM
has not been previously defined:
<macro circle(r)>
$requires NDIM
$if NDIM == 2 r^2 - x^2 - y^2
$endif
$if NDIM == 3 r^2 - x^2 - y^2 - z^2
$endif
</macro>
One task that is encountered often during the simulation process is naming groups of similar blocks, e.g. similar species. Macros can allow us to concatenate strings to make this process more clean and simple. However, based on the white-spacing rules, strings will always be concatenated with a space between them. For example,
$a = hello
$b = world
a b
will result in
hello world
However, we can get around this rule to get the desired output with the following:
<macro concat(a, b)>
$ tmp = 'a tmp b'
</macro>
Now when calling
concat(hello, world)
the result will be:
helloworld
The first line appends a single quote to a
and stores the result
in tmp
. The next line then puts the token a
together
with the token b
. As they are now no longer two strings, they
will be concatenated without a space. The final evaluation of the
resulting string then removes the quotes around it, resulting in the
desired output.
Now that we have examined macros in an overview, we are ready for Introduction.