Extending REDTEN



next up previous contents index
Next: The Sato Metric Up: No Title Previous: Case Mapping

Extending REDTEN

  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 gif. 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'' rdrgif 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
*

  1. We first define the default name of the object that this function will create. It is conventional in REDTEN to assign this name to the lisp varibale with the same name as the user-name of the function, in this example riemann.
  2. When the user executes the riemann() function, the REDUCE parser examines the simpfn property of riemann for the name of the actual RLISP function to execute. It is conventional in REDTEN to name this function by appending a !* to the user-name (there are plenty of violations of this convention, however). Thus, the actual work of computing the Riemann tensor is done by the RLISP function riemann!*() (but see §F.1 for a caveat).
  3. A function called in this way receives a single argument which is a list of all actual arguments given in the function call (note the arguments should not be quoted). This statement begins the actual definition of the function (or ``procedure'' in REDUCE terminology).
  4. Various local variables will be required, which should be declared here, otherwise they will become ``fluid'' when this function is compiled.
  5. For the current metric the Riemann function stores the name of the object it creates on the metric under the key riemann (see line 14). Any future calls to riemann() will find and return this name without making new calculations if the user did not supply a name (see line 7). The current tensor metric (returned by getmetric() with an argument of 1) is checked for a riemann property, any value found is assigned to lex; nil is assigned if no property exists.

  6. If the user does not supply a name to use for the output object, and the value found at line 6 is that of an indexed object, that object is returned immediately (see line 17 for an explanation of the format of the return value). If the user did supply a name for the output object, then a new object will be created and the calculations performed, even if the same calculation has already been done.
  7. The actual name of the output object is determined from the following rules: If a name was supplied by the user, then that is the name of the new object (the generic Riemann name will also point to it, see generics()). If no name was supplied, then the default name (stored on riemann) is made into the object name by prepending the name of the current tensor metric and an _ character. Thus, if the metric has the name g1, the default name of the Riemann tensor created for this metric is g1_R.

    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.

  8. With the name of the output object stored in tnsr the internal function to create indexed object is called (still named mktnsr!*() although it can create any kind of indexed object). Its arguments are nearly as described in §2.2, but the symmetry lists are entered in internal format where the block size is placed in a sublist in each independent symmetry. The object has four covariant tensor indices, a Riemann symmetry, is not implicit, the itype is riemann, and the description string is set to something informative.

    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 %.

  9. The printname of the object is changed so that no matter what its real name is it is displayed in the same fashion (by default using the name R in this example).

  10. Equation A.1 is now used to generate an indexed expression that will compute the Riemann tensor. This is written in an internal REDUCE format called ``prefix-form''. The result is assigned to lex for use in line 12 (this assignment could have been by-passed, writing the expression directly into line 12, but the code would be harder to read).

    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.

  11. The indexed expression in prefix-form is now passwd to the evaluator evaltnsr1(). This function is called almost directly when the user makes an indexed assignment with ==, and receives the name and index of the object on the left-hand side, and the indexed expression on the right-hand side. A fourth argument is no longer used. It is in this function that the two levels of REDTEN alluded to above meet: the riemann!*() function as examined so far exists simply to automate the construction of an expression the user could have typed in by hand.

    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.

  12. It is usual to apply a write-protection to the output object to prevent the user from accidentally changing components. If modification is desired, then the user can clear the protection with protect().
  13. The name of the output object is then placed on the property list of the current tensor metric under the key riemann. This is checked in line 6 to see if the object has previously been computed for this metric. This property is also used by the generic object system.

  14. Lines 15 do a special check; other functions may have similar checks or none at all.
    1. The components of the object are stored on the tvalue property. If there is no value here, then the object has no explicit components, i.e. it is 0. The body of the if statement is enclosed by the << and >> symbols.
    2. In this case an informative message is printed. The tabthenprint() function keeps track of the indenting of messages delivered during calculations.
    3. terpri() simply terminates a line, moving the cursor to the beginning of the next.

  15. A call to cleaner() prints the ... finished messages for functions that have been called upon to perform work (but not directly by the user), and it cleans up temporary objects if this function was called directly by the user. Its single argument is the user-name of the function in which it is used.

  16. Finally, the name of the object is returned. To return a single name to the algebraic simplifier it should be in the form of a dotted-pair whose first element is the name, and whose second element is 1. Any function which calls this one must use car() to isolate the actual object name in the return value.

  17. The function definition ends here.

  18. To interface with the generic name system, a psuedo-object is created by makegeneric() which has many of the properties of the actual object. The first argument to makegeneric() is the name of the generic object, in this case the value of riemann (R). Recall that the name of the actual object is of the form g1_R. The second argument specifies on which metric the relation between the generic and actual object will be stored (type 1 in this example, i.e. the tensor metric), and under what property to look for the relation. In this case, when the generic object R is used, the system will examine the tensor metric for the riemann property, and use the object so specified (this is the same property used by the riemann!* function itself). The third and fourth arguments gives the indextype and symmetries of the generic object; the last argument gives the name of the lisp function that can be used to compute the actual object.



next up previous contents index
Next: The Sato Metric Up: No Title Previous: Case Mapping



John Harper
Wed Nov 16 13:34:23 EST 1994