Basics of Macro Definition and Usage
A macro file contains one or more macros. A macro is invoked as a parameterized token with the result being that the body of the macro is placed into the input file. In this section we dicuss the basics of macros, including their definition and use.
Macros in Brief
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 myLaser>
EmField
<BoundaryCondition LaserPulseBC>
... some regular boundary conditions ...
</BoundaryCondition>
</macro>
You could then call your myLaser macro within the input file like this:
<Field exampleField>
kind = myLaser
</Field>
The XSim engine (Vorpal) will expand the input file use of your macro into:
<Field exampleField>
kind = EmField
<BoundaryCondition LaserPulseBC>
... some regular boundary conditions ...
</BoundaryCondition>
</Field>
Macro Parameters
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 box(lx, ly, lz, ux, uy, uz)>
lowerBounds = [lx ly lz]
upperBounds = [ux uy uz]
</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:
$ NX = 10
$ NY = 20
$ NZ = 30
box(0, 0, 0, NX, NY/2, NZ)
will create the following code fragment in the processed input file:
lowerBounds = [0 0 0]
upperBounds = [10 10 30]
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.
Macro Overloading
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 at 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 the 3-parameter circle has already been defined, otherwise you will receive an error.
Defining Functions Using Macros
Macros can be particularly useful for defining complex mathematical
expressions, such as defining functions in STFunc blocks with
kind = expression
. However, one must be careful because
of the substitution rules. For example, a macro that defines
a Gaussian could be written,
<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, XSim 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 betterGauss macro, but without requiring the additional parentheses in the macro text.
More About Parameters
In the previous examples, parameters were always single tokens or simple expressions. However, the preprocessor allows you to pass parameters that span multiple lines. This can be particularly useful for writing larger macros. An example of multiple line parameter passing would be defining a general particle source. This example below shows a macro defining a general species:
<macro ions(name, charge, extra)>
<Species name>
kind = relBoris
emField = emSum
charge = charge extra
<ParticleSink leftAbsorber>
kind = absorber
lowerBounds = [-1 -1 -1]
upperBounds = [ 0 NY1 NZ1]
</ParticleSink>
</Species>
</macro>
The parameter extra can be an arbitrary string such as:
ions(species1, 1.6e-19, "mass = 1e-28")
or it can be an empty string, if no additional information is needed:
ions(species2, 1.6e-19, "")
In addition, you can add entire input file blocks to this parameter. Assume we have a macro called loader, defined as follows:
<macro loader(ionDens)>
<ParticleSource ptcl_loader>
kind = randDensSrc
lowerBounds = [ -0.05 -0.05 -0.2]
upperBounds = [ 0.05 0.05 0.2]
density = ionDens
vbar = [0. 0. 0.]
vsig = [V_ion_rms V_ion_rms V_ion_rms ]
<STFunc macroDensFunc>
kind = expression
expression = H(0.1 - sqrt(x*x + y*y))
</STFunc>
</ParticleSource>
</macro>
Using this macro with the ions macro defined previously, we can now create an ion species with a source via a single line:
ions(species3, 1.6e-19, loader(1e18))
Importing Macros from Files
It is also possible to import a macro file that contains your
own custom macros. This is useful when reusing one or more
custom macros over multiple simulations. For example, physical
constant definitions or commonly-used geometry setups can be
stored in files that can then be reused. The macro file must
have a .mac extension on it to be imported as a local macro,
and it must be in either the directory of your .pre file, or its
directory must be in the environment variable, TXPP_PATH
.
To extend the example above, say the macro myLaser is in the file Lasers.mac. The input file would look like this:
$ import Lasers.mac
<Field exampleField>
kind = myLaser
</Field>
Vorpal will expand the input file use of your macro into:
<Field exampleField>
kind = EmField
<BoundaryCondition LaserPulseBC>
... some regular boundary conditions ...
</BoundaryCondition>
</Field>
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.
Files are imported via the import keyword:
$ import FILENAME
where FILENAME represents the name of the file to be included. XSim 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.
Conditionals
The Vorpal 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.
The most general form for a conditional is
$ if (COND1)
...
$ elseif (COND2)
...
$ elseif (COND3)
...
$ else
...
$ endif
where there is a stanza starting with $ if
, zero or
more stanzas beginning with $ elseif
, and zero or one
stanza beginning with $ else
. As in general usage,
when a stanza is reached where the conditional evaluates to
True, those lines are pre-processed and the resulting lines
are inserted into the input file. If a conditional statement
does not evaluate to True, then that branch of the conditional
statement is skipped by the pre-processor. If no branches
evaluate to True, then the lines after the else
(if present) are processed. Conditionals can be arbitrarily
nested. Use of parentheses around testing condition expressions
is important when also using the boolean operators not
,
and
, or or
.
Most valid Python expressions can be inserted for the
conditional test, but this is an area continuously undergoing
improvement, so there may be some volatility. In particular, it
is desired to have a conditional test that evalutes in Python
to either True or False. As an example, for a variable that
is undefined, both $ if (undefvar)
and
$ if not (undefvar)
branches will be skipped by the
pre-processor because neither evaluates to True. For the
moment, other unexpected behavior can occur when checking for
empty strings. The only valid way to do this for now is to
use $ if (isEqualString(s1, ""))
. The isEqualString
should generally be used for strings to be careful, but most
string comparisons, except for those involving empty strings,
work in the manner expected for Python evaluation. Support for
empty string comparisons using standard Python syntax will be
supported in the future.
Example Conditional Statements
$ if (NDIM == 2)
$ dt = 1/(c * sqrt(1/dx**2+1/dy**2))
$ else
$ dt = 1/(c * sqrt(1/dx**2 + 1/dy**2 + 1/dz**2))
$ 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
Repetition
For repeated execution, Vorpal 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
Recursion
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.
Requires
When writing reusable macros, the best practice is for 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>
String Concatenation
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 may be concatenated with a space between them. For example,
$ a = hello
$ b = world
a b
will result in
hello world
The space insertion is not done, however, if the last character
of the first string is not a letter or a number, or if the first
character of the second string is not a letter. We can avoid
this rule altogether by using the concatenate
macro:
concatenate(hello, world)
in which case, the result will always be:
helloworld
The concatenate
macro is located in the file
listUtilities.mac, and is always available at the top level
for importation.
Built-in macros and further discussion thereof can be found in the Reference Manual.