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.