Macros are an essential feature of the FELIX data processing philosophy. A macro is simply a text file containing a series of FELIX commands, which together perform a specific task. You can edit a macro file using most text editors. In addition to direct commands, a macro can contain symbol substitution, arithmetic expressions, and flow control statements (that is, for loops, if/then/else statements, goto's, and subroutine calls). In effect, macros combine the capabilities of a programming language with the standard capabilities of FELIX, allowing you to design customized processing procedures.
FELIX comes with a library of macros capable of many common processing tasks. You can modify these macros to fit your specific needs. For example, the apodization function in our example macros will probably need to be different for each user. In addition to using macros for custom data-processing and -analysis tasks, macros are also used to implement the FELIX graphical interface. In fact, the advanced user will find it easy to create control panels and menus to design a custom environment.
It is very important to remember to place custom macros in directories where FELIX can find them. The macros that drive the graphical interface are found in the $BIOSYM/macro/felix/menus/mac subdirectory (FELIX macros). This directory is defined by the reserved symbol macpfx. The user macros can be stored in a directory defined by the reserved symbol macpf3. To keep things simple, you should initially keep all user-written macros in the directory defined by the symbol macpf3. The maximum length for a directory pathway is currently 128 characters, the same as the maximum length for a single symbol value.
When FELIX searches for macro files, it first looks in the current working directory, then in the directory defined by the symbol macpfx. This symbol is given an initial value in the .felixrc file (found in the home directory) that defines the initial directory pathway so the graphical interface runs by default. If a named macro is not found in the directory defined by the macpfx symbol, FELIX then searches in the directory defined by the symbol macpf1. This logic continues up to the directory defined by the symbol macpf9. Execution speed may be severely degraded by long search paths, especially when NFS-mounted directories are involved.
To write or edit a macro, it is easiest to use your system text editor. For users of UNIX workstations, this is a simple matter because you can open a separate window for macro editing while still running FELIX. Most people use the vi editor to change the macros, but other editors may be used as long as the resultant files don't contain formatting characters and do contain standard carriage control.
It is standard for the first line of any macro to contain the macro's name, though this item is optional. FELIX ignores any macro line that starts with c**. The beginning of a typical macro is shown below. Note that the line numbers at left are used for reference only, and should not be included in the macro:
2 set 0
Macros may also contain tab characters and blank lines, so that they may be organized so as to enhance readability. In addition, any characters that come after a semicolon (;) are ignored by the macro interpreter. This makes it easy to add comments to macros as shown below:
5 fra o 10 10 10 10 ;Open first graphics frame
6 fra t 1
10 fra o 30 30 300 300 ;Open second graphics frame
14 set 1
15 sb 600 90
16 dr ;draw sinebell window
20 ex &return ;return to GUI
Since macros are simply a series of FELIX commands, you must tell the program when to return control from the macro interpreter to the command interpreter. There are two ways to do this. If you are executing macros from text mode, the last line in the macro should be an end statement. If you are executing macros from the graphical interface, the end statement must be preceded by an ex &return statement or a ret statement. This ensures that the interface returns to the appropriate graphical state upon completion of the macro.
Once you've created a macro, the standard way to execute it is to issue the execute macro command (ex) followed by the name of the macro. For example, the command sequence shown below executes a macro named test1.mac. FELIX searches for the macro in the current working directory and then in the directory defined by the symbol macpfx, followed by the directories defined by the symbols macpf1, macpf2, macpf3, ..., macpf9.
> ex test1.mac
> ex test1
This method of running a macro may be used when in text mode, or, alternatively, may be part of an existing macro. The following macro illustrates how a logic operation can determine subsequent macro tasks.
2 if &color eqs red then
3 ex red_draw.mac
5 ex blue_draw.mac
Another way to execute a macro from text mode is to input the name of the file preceded by a period. This is a quick shorthand notation that can replace the above example. For example:
executes the macro named test1.mac.
You can also execute user macros from the graphical interface by selecting the User/User macro menu item. To execute a macro, enter its name in the entry box in the control panel. Macros that are executed from within menus must have the statement ex &return as the last line in the macro to ensure that the macro returns to the appropriate state in the graphical interface.
The mechanics of this feature are based on local symbols. Everything following the macro name on the command line is made available to the called macro, in the form of local symbols. When the called macro begins executing, one or more new local symbols are defined. This applies when the macro is executed by ex, cal, or exr. They always have the names: _args, _arg1, _arg2, etc. The first of these, _args, is the number of arguments passed to this macro. When no arguments are given, _args is zero. Otherwise, there are additional symbols _arg1, _arg2, etc., each of which is the value of the nth argument passed to this macro. It is an error to try to use an argument number greater than the number of arguments actually passed to the macro.
For example, the command line:
> ex test 13 4
causes the macro named test.mac to begin executing, with the following local symbols:
The command line:
> exr find 'Look for' h* n*
causes the find.mac macro to begin executing, with the following local symbols:
FELIX macros allow loop structures that are very similar to the loops in the BASIC and FORTRAN programming languages. Loops allow a set of operations to be carried out iteratively. Loops are necessary for processing multidimensional data for which each vector along a dimension is processed in exactly the same manner. Rather than typing in the same sequence of commands several thousand times, you can type in the processing sequence only once and use a loop to apply the specified sequence any number of times.
The basic layout of the loop structure is shown below:
1 for symbol start_value end_value increment
The argument symbol is the name of the loop counter whose value is incremented to reflect the current integer status of the for loop. The argument start_value defines the initial value of the loop counter, and the argument end_value defines the last value for the loop counter. Both these arguments are inclusive with respect to their values, may be positive or negative, and may be a real number or an integer. If either of these values is a real number, the actual value used is the closest integer value. The argument increment is the loop counter increment. This defaults to 1 if omitted, but may be set to any non-zero integer. All arguments for the loop sequence may be in the form of symbols.
Macro loops are related to symbol substitution, in that each loop maintains a counter symbol that is incremented each time through the loop. A simple example of a macro containing a loop is:
2 for count 1 5
3 ty count = &count
5 ex &return
The loop structure is defined by the for and next commands. Commands enclosed between the for and next are executed while incrementing the value of the specified counter symbol for each loop (the symbol count is incremented from 1 to 5 in steps of one). In this macro, the only command inside this loop is the type command (ty), which prints text defined by its command line. The output of the above macro loop is:
The output created by this macro illustrates the operation of for loops. Each time through the loop, the for counter (count in this example) is set to a value corresponding to the number of times the loop has repeated. Symbol substitution replaces the &count parameter with the value of the for loop counter, and the ty command explicitly types out the text following the ty command.
For loops may be nested up to eight levels deep; however, each loop must have a unique counter symbol. An example of a nested for loop is:
2 for row 1 3
3 for col 1 4
4 ty row=&row col=&col
7 ex &return
The inner loop (which affects the symbol col) increments through all its values, the outer loop (row) increments one, and then the inner loop increments through all its values again. Thus the ty command is executed 12 times, for all the combinations of the row symbol from 1 to 3 and the col symbol from 1 to 4. Nested loops are useful for processing multidimensional spectra.
Text scrolling in the parent window can be one of the most time consuming aspects of running a FELIX macro. You may notice that, on some computers, macros containing type statements in a for loop run much slower than those without these statements. On the other hand, type statements are very useful for monitoring the progress of a macro. This problem can be prevented by placing a dollar sign ($) at the end of the line of text. This suppresses the line feed and causes new text to be printed on top of the previous text. For example, if we change the third line in the macro test1.mac to read:
ty count=&count $
it runs more quickly, without text scrolling.
You may occasionally want to interrupt macro execution prior to completion. Getting stuck in a lengthy loop is not uncommon when writing new macros for novel data processing. If you only need to stop a running macro, you can type <Ctrl>-\ (press the <Ctrl> and backslash keys simultaneously) or <Ctrl>-c.
For more elaborate handling of interrupt signals, you may use the esc command to escape from a loop. To do this, you must include a specific command structure in your macro that uses the escape command (esc), with the format:
Each time the esc command is encountered, the program updates the argument symbol name. If the <Esc> key is not in the keystroke queue (i.e., you don't press <Esc>), the value for symbol name is set to 0. If the <Esc> key appears in the keystroke queue (meaning you did press <Esc> on your keyboard), the value for <symbol name> is set to 1. As shown in the following example, you can then include logical branching in your loop to specify how the macro is halted:
2 for loop 1 9999
3 ty &loop
4 esc out
5 if &out ne 0 quit
While allowing for elaborate handling of interrupt requests, the esc command does slow down your macro. Hence you may prefer to simply use the <Ctrl-\> key combination.
The following is a simple macro used for reversing each 1D vector in a matrix. This example illustrates many of the features described above. It uses the reserved symbol d2size to ensure that each vector along the second dimension is acted upon accordingly:
2 for row 1 &d2size ;for each row in the matrix
3 loa 0 &row ;load the row
4 rev ;reverse the data points
5 sto 0 &row ;store the row back
6 ty Row= &row $ ;type the row number, no scrolling
7 esc out ;check for escape key
8 if &out ne 0 quit ;quit if escape hit
9 next ;loop to next row
The following is a macro for deleting cross-peak footprints along the first dimension for a defined region. A macro such as this might be used to remove cross-peak footprints that are part of a T1 streak, which may be defined by the user symbols d1low and d1high:
2 dba list zero 1 xpk:peaks ;zero list 1
3 get 'D1 low point: ' d1low ;get low limit
4 get 'D1 high point: ' d1high ;get high limi
5 dba list range 1 xpk:peaks cen1 &d1low &d1high pknum
6 for loop 1 &pknum ;for each peak number
7 dba list load 1 &loop peak ;load the first peak number
8 drx xpk:peaks.&peak 0 ;undraw the footprint
9 dba item delete xpk:peaks.&peak ;delete that footprint
10 ty Deleting peak number: &peak ;talk about it
11 esc out ;check for escape
12 if &out ne 0 quit ;quit if escape
15 ex &return
Macros allow unconditional branching using variations of the FORTRAN goto or the switch statement in C. The structure of this command is:
where the argument label is a specific label identified in the macro. Labels are identified by adding a colon (:) to the end of the label name. For example, the statement:
looks for the label shown below:
The go command is very useful when working within interactive macros that prompt you for replies or that need to make decisions based on the values of symbols that are set with toggles or switches. Here is an example of a simple menu that uses a go statement in this way. The only argument of the go command is a label name, which may be explicit or symbolic; for example:
4 go quit
8 ty All done!
When the go command is executed, the macro jumps to the specified label (in this case, quit:) and continues executing. Labels may be up to sixteen characters long and represent a specific place within a macro file. Within any macro, each label name must be unique. When a label is branched to, the commands that follow it are executed. In the example shown above, after the go branch to the quit: label, the message All done! is printed to the screen. If the specified label is not found within the macro, the macro continues on to the next line as if no go statement existed.
Macros also allow branching using a case-type go to command (gto). This command determines branching based on a fixed integer, which is usually a number defined by a symbol. The format of this gto command is:
In practice, if the argument symbol_value equals 0, the macro branches to the label defined by value0. Likewise, if the argument symbol_values equals 2, the macro branches to the label defined by value2.
Throughout the graphical interface, the gto branching statement is used to make decisions based on symbol values defined by control panel toggles or switches. For example, a symbol defined by a toggle can be used to choose between Fourier transform options. For this example, if the particular symbol called fttype (defined by a specific toggle) has a value of 0, a FFT is specified. If the symbol fttype has a value of 1, a BFT is needed. We could then create the following macro to make use of this information:
2 mnu p transform 20 4
3 if &button eq 0 quit
4 gto &fttype fft bft
8 go quit
12 go quit
14 ex &return
Macros also allow branching using the arithmetic if command (gif). This command determines branching based on the sign of a fixed integer, which is usually defined by a symbol. The format of the gif command is:
In practice, if the argument symbol_value is less than 0, the macro branches to the label defined by value_lt_0. If the argument symbol_value equals zero 0, the macro branches to the label defined by value_eq_0. And if the argument symbol_value is greater than 0, the macro branches to the label defined by value_gt_0. The gif statement is commonly used in graphical interface macros.
Macros can contain if statements for conditional branching. if statements are similar to go statements in that they can result in a branch to a specified macro label, but differ in that the branching may or may not happen, based on the specified condition. The first type of if statement consists of a conditional and a target label:
Another type of if statement, the if/then/else, is used to control the execution of blocks of code depending on the state of the conditional. The form of the if/then/else is:
if conditional then
The most basic conditional consists of two strings and a relational operator and has a value of either true or false. The conditional is evaluated by comparing string1 with string2 using the specified relational operator. A typical if statement is:
if &number eq 0 quit
This command compares the value of the symbol number to 0 using the eq (equal to) relational operator. If the symbol number equals 0, the value of the conditional is true, and a branch is made to the label quit:. If the symbol number does not equal 0, the conditional is false, no branch is taken, and the next command in the macro is executed.
Conditionals use two different types of relational operators. Arithmetic relationals interpret the strings as numbers, and string relationals interpret the strings as ASCII characters. Because numbers and ASCII strings do not sort in the same order, you must be aware of the ASCII code when using string relationals other than eqs and nes. A list of the relationals is shown below for both arithmetic and string characters:
Conditionals can be combined using and and or operators to test more than one relation at a time. The and operator yields a true result only when it operates on two true conditions. The or operator yields a true result if either conditional is true. Examples of conditionals containing the and and or operators are:
string1 operator string2 and string3 operator string4
string1 operator string2 or string3 operator string4
An example macro that uses if commands is given below. This macro plays a guessing game using a binary search and uses a variety of flow-control commands that are discussed above.
2 ty Guess a number between 1 and 10=
4 get "Are you ready? (y/n):" yesno
5 if &yesno nes y ready ;ask again if not ready
6 def lo 1 ;define the necessary symbols
7 def hi 10
8 def old 0
10 eva middle ((&hi+&lo-1)/2) 1 ;guess middle of range
11 if &hi eq &lo done ;we are done
12 if &middle ne &old guess
13 if &middle eq &hi mlo
14 def middle &hi
15 go guess
17 def middle &lo
19 get "Is it bigger than &middle?" yesno
20 def old &middle
21 if &yesno nes y no
22 eva lo (&middle+1)
23 go again
25 def hi &middle
26 go again
28 ty The number is &middle.
29 ex &return
This macro uses if commands containing both numeric and string relational operators. The string relational nes is used to distinguish between the user responses, while the numeric relations eq and ne are used to compare numbers. The eva (evaluate) command is used twice to force evaluation of expressions.
Another macro example using if statements follows. In this example, the gv command gets the value of a datapoint from the workspace and places it in a specified symbol, to find the largest, smallest, and average value in the workspace:
2 gv 1 bigpt
3 def smlpt &bigpt
4 def tot 0
5 for point 1 &datsiz
6 gv &point thispt
7 if &thispt gt &bigpt newbig
8 if &thispt lt &smlpt newsml
9 go loop
11 def bigpt &thispt
12 go loop
14 def smlpt &thispt
16 eva tot (&tot+&thispt)
18 eva avg (&tot/&datsiz)
19 ty Biggest=&bigpt
20 ty Smallest=&smlpt
21 ty Average=&avg
22 ex &return
This example first sets the symbols bigpt and smlpt to the value of the first datapoint and the symbol tot to zero. The macro then loops through all datapoints (from 1 to &datsiz), comparing values with bigpt and smlpt. If any point is larger than bigpt or smaller than smlpt, the new large or small point is saved. The sum of all point values is accumulated in tot, which is used to calculate the average.
Macros can also contain if/then/else structures for more orderly flow control. The if/then/else statements may be nested up to eight deep. The basic if/then/else/eif structure is:
if conditional then
The condition is evaluated by comparing string1 and string2 using the specified relational operators described above. A typical if/then/else/eif statement is:
if &number eq 0 then
type Symbol not defined
eva count (&value/&number)
ty Symbol count is: &count
In the example shown above, the if/then/else/eif structure is used to prevent a divide by zero error if the value of the symbol number is 0.
Macros can contain ifx statements for conditional macro execution. ifx statements are similar to if statements in the arguments required; however, they differ in that they result in the execution of another macro instead of branching to a label in the same macro. The basic ifx command consists of a conditional and a macro filename:
ifx conditional macro_name
The conditional is evaluated in the same way as in the if statement. A typical ifx statement is:
ifx &number eq 0 root
This command line compares the value of the symbol number to 0 using the eq relational operator. If the symbol number equals 0, the macro named root.mac is executed.
The exr command (execute and return) is a powerful way to execute macros. This command brings the concept of a subroutine call, as used in high-level programming languages, into the FELIX command arsenal.
The exr command preserves where you are in the current macro, loads and runs the new macro, and resumes exactly where it left off when the new macro finishes. Macros executed via exr may be nested up to eight levels deep.
Several commands are used only within macros. For example, the esc command described above is a command that is useful only in a macro. There is no reason to check for an <Esc> key unless a macro is running, since otherwise there is no process to interrupt. Some other examples are shown below.
Communicate statements (com) pause an executing macro, prompt the user for a single command line, execute that one command, and then restart the macro after the pause point. For example, the com command can be used to test a range of window functions as they affect a dataset:
2 rn test.dat
In the above example, at the com command (line 3), a FELIX prompt appears and waits for the user to enter any FELIX command they desire.
Error statements (err) define an error trap destination for a macro. If any command sets the value of the reserved symbol status to non-zero, it means that command didn't function properly. If you use an err statement, the macro resets the status to 0 and continues executing where the error trap indicates. There are currently two types of err commands. If the command:
err label label
is encountered, the macro branches to the specified label. If the command string:
err macro macro
is encountered, macro interpretation is transferred to the specified macro. Error traps are most commonly used in macros that read user data.
The ty statement types the following text to the parent window; the tym statement types the following text to the status line in the main FELIX window as well. This text can be used for detailing the status of any macro.
tym All done!!
ty Data size is: &datsizNote: Symbols may be substituted in any macro command. Remember that the tym command incurs a significant overhead and noticeably slows down macro execution.
Macros may also be created from within FELIX using simple program commands. This technique is often used for saving a sequence of commands for execution later. For example, spectrum annotations are saved to a macro file. When you want to redisplay the annotations, simply execute that annotation macro.
Before building a macro using FELIX commands, it is safest if you inquire as to the presence of a macro file with the name you want to use. This is accomplished in your macro using the inquire (inq) command, which accepts the input string:
inq prefix file_name symbol
The prefix argument reflects the filetype based on the prefix, whether it is a macro (mac), annotation file (ann), datafile (dat), or matrix (mat) file. For files without prefixes, set the prefix parameter to nul. Based on the prefix type, FELIX uses the directory defined by the *pfx symbol to locate the file. For example, if prefix is mac, FELIX looks in the directory defined by the symbol macpfx to determine whether a file named filename exists. If the file is found, the symbol defined by the argument symbol is given a value of 1. If the file is not found, the symbol defined by the argument symbol is given a value of 0. An example that uses the inq command to determine whether a file named test exists in the macro directory is:
2 def filnam test
3 inq mac &filnam exist
4 if &exist eq 1 fileok
5 ty ----------
6 ty File &filnam not found.
7 go quit
9 ty ----------
10 ty File &filnam exists.
12 ex &return
Before building a macro using FELIX commands, you must first open that file. The open file command (opn) performs this function and accepts three arguments:
opn prefix file_name overwrite/append
The prefix parameter defines the prefix for the new file and places that file in the directory defined by the corresponding pfx symbol. As discussed above, if prefix is mac, the resulting macro file is created in the directory defined by the symbol macpfx. The file_name argument is the name of the file to be created. The overwrite/append argument defines whether you want to overwrite an existing file (0) or append to an existing macro file (1). Setting overwrite/append to 1 is useful for modifying macros that already exist, without recreating them from scratch. Two files can be opened simultaneously, providing one file is opened for output only.
Once a macro file has been opened for writing, the put command (put) is used to add text to the file. The put command adds all text following it to the file, including spaces. Quotes are not needed. Sample put statements are:
putdef annsize &annsiz
putdef thick &thick
putdef level 1.0
Open macro files must be explicitly closed before execution, using the close file command (cls), which requires no arguments. Since you are creating a macro, it is very important that before closing it, appropriate end and return statements are added to the file. The end command must be the last line in a macro in order for the program to switch from the macro interpreter to the command interpreter. Similarly, the ex &return statement must be at the end of a macro if you want it to return to the graphical interface. An example of building a macro is:
2 def return root
3 opn mac cplevel 0
5 putdef level &mylevl
6 putdef nlevel &mynlev
7 putdef pencol &mycolr
8 putdef cycle &mycycl
9 putex &return
The resulting macro, named cplevel.mac, is created in the directory defined by the symbol macpfx:
2 def level 1.2
3 def nlevel 10
4 def pencol 2
5 def cycle 3
6 ex root
This macro can now be executed.
When building macros in this manner, you may want to place a symbol-definition statement point to another symbol instead of a value. In the example above, if you changed line 8 in the build.mac file to read putdef cycle !&newcyc, line 5 in the cplevel macro would read def cycle &newcyc. Delayed symbol substitution such as this greatly increases the power of the program.
FELIX macros are the most powerful and innovative feature of this NMR processing software. By writing your own macros, you can automate anything that you can do with FELIX manually. Your ability to transform and manipulate data automatically using macros is limited only by your imagination. For example, if you want to read a datafile (rn), apply an exponential window function (em), Fourier transform (ft), phase correct (ph), and draw a spectrum (dr), you could write the following macro and name it process.mac:
2 get 'Enter file name:' filnam
3 rn &filnam
4 em 3
9 ex &return
The get command prompts you with "Enter file name:" and waits for a datafile name to be input. The read command (rn) reads the file (notice the use of the symbol filnam). Then the data are multiplied by a 3 Hz exponential window (em 3). The data in the workspace is then Fourier transformed (ft), phase corrected (ph), and displayed on the current graphics terminal with the draw command (dr). The ex &return statement returns the macro to the graphical interface. The end command (end) is mandatory at the end of every macro; this tells FELIX to stop executing commands from the macro and accept input from you again. Instead of manually entering the six commands listed above, you can simply enter:
> ex process
from text mode.
Macros that are only slightly more complex can be used to perform repetitive tasks by constructing loops that process several datafiles in a similar manner. For example, if you want to generate hardcopy plots for a series of related 1D spectra, you might construct a macro similar to the macro mplot.mac:
3 get 'Enter file name (quit):' filnam
4 if &filnam eqs quit then
5 ty Returning to &return
6 ex &return
8 rn &filnam
9 em 3
15 go again
As in the previous macro, you are first prompted for a filename, then the data are multiplied by an exponential, Fourier transformed, phased, and drawn. The spectrum on the screen is then plotted on the current hardcopy device with the hardcopy plot command (hcp). After the first spectrum is plotted, the mplot.mac macro branches back to the label again: and prompts for a second datafile, which is then processed and plotted. In this macro, to quit you simply enter quit instead of an actual filename.