From the user perspective REDTEN consists of two levels: a lower level of indexed algebra in which the user can enter indexed expressions and have them evaluated; and a higher level in which the user applies functions that compute objects of interest and which maintain the book-keeping in the system. The higher level functions are ``layered'' onto the lower level in the same way that REDTEN itself is layered onto REDUCE. The packages described in Chapters 4 and 6 are high level functions, but it is important to realize that the basic REDTEN indexed algebra engine could be used to compute interesting quantites even without these packages, as was done in Chapter 2.
Although the development of the GR package is essentially complete, the spinor, frame (tetrad), and Newman-Penrose packages require more work. As well, there are many other computations that would be useful to have automated in high level functions, such as Petrov classification of metrics, etc . In this sense REDTEN is incomplete: there are always other interesting objects to compute, in other formalisms etc., which would be convenient (but not absolutely necessary) to have high level functions to deal with.
This appendix describes how the user can extend REDTEN by writing functions to automate the computation of specific indexed objects. It is assumed that the user has a basic understanding of RLISP and lisp in general. NOTE: Although the user is free to modify or extend REDTEN as required, no such modified copy may be redistributed without the modifications being plainly marked, and the copy must not be misrepresented as the original. If you have a brilliant addition, send it to the authors to be included in the distribution.
From the programmers perspective, there are actually three levels in REDTEN: a ``basement'' level of basic functions lies under the low level (the division is somewhat blurry). Some of these functions are required when writing high level code, as they deal with the mundane details of maintaining indexed objects in the system.
As an example of writing high level function, we shall use the riemann() function of the GR package as a starting point. All of the high level functions work in the same basic fashion: they write an indexed expression the equivalent of what the user would type and cause it to be evaluated. The complications come with all of the other book-keeping functionality required. Before actually detailing the structure of a typical high level function, consideration must first be given to the internal representation of an indexed object.
The user interface to an indexed object consists of a name followed by an index enclosed in square brackets. The indexed object name has another property not mentioned in §2.2.6: the simpfn propert gives the name of an RLISP function to execute when the algebraic simplifier encounters the name. All indexed objects are handled by the same function (mkrdr()) which receives both the object name and the separately parsed index as arguments. This function then constructs the internal ``rdr-form'' of an indexed object; it also handles some error checking and display requests (see §2.4.3).
Internally, an indexed object (after parsing) is a lisp list whose first element (the car()) is the ``function'' rdr which itself has a simpfn property pointing to the RLISP function simprdr(). The simprdr() function supervises the read-out of object componenets if the index is fixed, and returns an unevaluated rdr-form otherwise. The second element (cadr()) of the rdr-form is the indexed object name, and the third (caddr()) is the index, containing the internal forms of any index operators. A fourth element is no longer used, but some REDTEN code may still make a harmless reference to it.
The various index operators are all translated into an internal form when they are parsed as shown in table A.1. The index itself becomes an ordinary lisp list, most operators exist as elements at the top level of this list, except for the shift operations which become sublists with the operator first, and the index element second. Knowing these internal forms the programmer can write any indexed expression.
Table A.1: Internal forms of index operators
To write a high level function first requires an equation defining the output indexed object; the definition of the Riemann tensor (equation 3.3) is repeated here for reference with different indices,
Below is the source code for the riemann() function. The function performs several basic tasks: determining the name of its output object, checking to see if the object already exists, creating it if not, setting up and evaluating the indexed expression which computes the output, and some book-keeping and cleanup. Each line of the function is described separately after.
riemann := '!R; 1
*
put ('riemann, 'simpfn, 'riemann!*); 2
*
*
symbolic procedure riemann!* (u); 3
*
begin scalar tnsr, lex; 4
*
tnsr := mycar getnme (mycar (u), '(nil . nil), 'riemann); 5
*
lex := get (getmetric (1), 'riemann); dotfill 6
*
if not tnsr and indexed (lex) then return (lex . 1); 7
*
tnsr := newnme (tnsr, riemann); 8
*
9
*
put (tnsr, 'printname, riemann); 10
*
11
*
evaltnsr1 (tnsr, '(a!# b!# c!# d!#), lex, 'nil); 12
*
protect!* (tnsr, 'w); 13
*
put (getmetric (1), 'riemann, tnsr); 14
*
if not get (tnsr, 'tvalue) then << 15a
*
~tabthenprint ("** this space is flat"); 15b
*
~terpri ()15c
*
~>>;
*
cleaner ('riemann); 16
*
return (tnsr . 1); 17
*
end; 18
*
*
19
*
newnme() requires two arguments: the user-input name (which may be nil), and the default name (stored in this case on riemann, note there is no quote before riemann: its value is passed to the function). newnme() also prints the Computing ... message to indicate to the user what the real name of the object will be. The generic name for the Riemann tensor will point to this object.
mkmsg() takes one list, the first is a string containing %
symbols. These are replaced in sequence by the remaining elements of
the list. To print an indexed object wrap its rdr-form with the
rdr!-string() function; to print a list as an index wrap it with
index!-string(); \
% produces a literal %.
The prefix form involves lisp lists whose first element is an algebraic function, which will be applied to the remaining arguments. The following functions may be useful in constructing an indexed expression:
There are two ways to construct a list in lisp: using the quote function (the input shorthand is ') which does not evaluate its arguments, and using the function list() which does evaluate its arguments. If a list is to be written whose contents may vary between one evaluation and the next, the list() function must be used. Generally, only numbers will be constant as such (for example, can be constructed as '(quotient 1 2)); even if the name of an indexed object is assumed to be known it is better to make use of a variable which holds the name, so that the name can easily be changed later without affecting the code. Hence, writing indexed expressions requires many calls to list(). Notice that the arguments to list() must themselves be quoted if they are to be used literally. If any sublist is constructed using the list() function, all enclosing lists must be constructed the same way (not using the quote function), or the sublists will not be evaluated.
Consider the code fragment
list ('rdr, mycar (christoffel1!* ('nil)), '(b!# d!# a!# !*br c!#))
which creates an rdr-form representing the first term of equation A.1. The name to be used for the indexed object is found by calling the christoffel1!*() function, which works in the same way as the riemann!*() function considered here. That is, christoffel1!*() either returns the name of the previously computed object, or does the computation now. In either case the return value is of the form described under line 17, so that mycar() is used to isolate the name. This method of having one function call others that it depends on allows the user to create all the objects of interest from the metric with one command (but see §5.1 for reasons why this is not usually a good idea).
The index can of course be written with specific indices and is thus constructed using the quote function. The actual indices are selected from those stored in the variable alphalist!* (which is simply a list of indices that the user will not use, since algebraic values assigned to these symbols could potentially have side-effects). The index is written with the internal forms of the index operators as shown in table A.1; in the example above an ordinary differentiation operator has been placed in the index.
Shift operations are indicated by making the index-element into a sublist with the operator as the first element. For example, an index might be written '(a!# (!*at!* b!#)) to indicate shifting the second index-element. If the exact indextype of an available object is unknown, the absolute shift operators can be used to ensure a canonical form is used in the expression. This technique is used in the frame package where it is not certain in advance whether the user has defined a covariant or contravariant connection.
Examination of the rest of line 11 will show the correspondence with the terms of equation A.1. The repeated index e!# represents a contraction in the two products; and note the use of the unary minus to negate the last term.
When evaltnsr1() finsishes, the output object (whose name is the value of tnsr) will have the appropriate values. If showindices is on, then the indices will be displayed as the calculation proceeds.