Go to the previous, next section.

Fortran Programs

There are a small set of routines that get called from a model written in FORTRAN that enable Ingrid to work. There are three groups of routines: setup, which are used to setup and initialize the system, data access, which allow Ingrid and the model to exchange data, and routines that can be used to extend Ingrid, i.e. add new words/commands to the language.

There is a later section (Test Code) which provides a small working example of how a model might function with Ingrid.

Setup routines

There are a series of routines that need to be called to setup a model to use Ingrid. They fall into several groups.

createIngrid

The first step is to initialize/create Ingrid. This is a simple subroutine call with no arguments.
CALL createIngrid
When it finishes the Ingrid stack looks like
Ingrid: SOURCES MODEL --
Thus all the definitions that follow (before the call to FGFILE) will be in the MODEL object unless specified otherwise. Later FGFILE will set the stack so that it only contains the Ingrid: object.

Optionally one can also call

CALL createHDF
This enables writeHDF, which allows one to write HDF files. This requires adding the library -ldf when linking.

Optionally one can also call

CALL createEOS
This enables some oceanic equation of state functions (see section Stream filters). This requires adding the library -leos when linking.

Grids and Variables

To setup the system, Ingrid has to be told what variables exist and what grids correspond to each of those variables. In FORTRAN, this is done with a small set of setup routines. For setting up grids, there are SetIVAR1 and SetIVAR2.

SetIVAR1 or SetIVAR2 used to set the grid for the independent variables ('XYZT'); NewVAR is then used to add a variable to the list. This is done to make it easy to specify different grids for different variables. For example,

CALL SetIVAR1('X','longitude',NX,XS,XE)
CALL FGINTERP('X /fullname /Longitude def pop')
CALL SetIVAR1('Y','latitude',NY,YS,YE)
CALL SetIVAR1('Z','real',NZ,ZS,ZE)
CALL SetIVAR1('T','monthtime',NT,TS,TE)
Note that we have (optionally) set the fullname of X to be Longitude. That will be used as the label on plot axes, for example. Other variables, where we have not set the fullname, will be labelled by their name as given in the SetIVAR1 call.

Once the grids are defined, we can use NewVAR and NewRVAR to define variables that are functions of those grids. For example,

IDTAUX = NewVAR('TAUX','taux','XYT','XY')
IDU = NewVAR('U','zonal vel.','XYZT','XYZ')

CALL SetIVAR1('Y','latitude',NY,YS+DY/2,YE+DY/2)
IDTAUY = NewVAR('TAUY','tauy','XYT','XY')
would make TAUX and U have the same grid, while TAUY would be shifted in latitude by half a grid step. Note that the wind does not depend on z, while the zonal velocity u does.

To have a grid with uneven spacing, one used SetIVAR2.

CALL SetIVAR2('Z','real',NZ,ZSS)
where ZSS is an array with NZ points in it.

There is some point to choosing `name' carefully. Ingrid has a large number of operators that operator on streams: add, mul, sin, cos , ldots. These operators construct the name of the output stream from the name(s) of the input stream(s), a process that can get quickly out of hand if names are chosen unnecessarily long. Furthermore, there is a slightly more sophisticated way of specifying the name that gives the operators more latitude in combining names. For example, if we change the TAUX example to,

IDTAUX = NewVAR('TAUX','/MODEL (tau sub x)','XYT','XY')
IDTAUY = NewVAR('TAUY','/MODEL (tau sub y)','XYT','XY')
NewVAR sees that the string begins with a `/', so it interprets the string as Ingrid commands (PostScript). Here the name has two parts: MODEL, and \tau_x. This means that a contour plot of TAUX would be labeled MODEL \tau_x, while a vector plot of TAUX and TAUY would be labeled MODEL (\tau_x, \tau_y). This also means that if we later decide to compare the model TAUX to some dataset taux, we will get the difference labeled MODEL - DATA \tau_x.

The second way to pass data into Ingrid is compatible with the way that SUMRY does it. Here one calls

CALL NewRVAR('shortname','name','XYZT',MQ)
where `XYZT' is the only choice that SUMRY lets you have (here you could have more or fewer independent variables if you want), and MQ is the integer identifier that gets passed to RECEEP. See section Data access routines

Model Description String

There is also a setup routine ModelDESC which allows one to set the model description string. This string is used in documenting plots and files.
CALL ModelDESC('The model description')

Variable Description String

There is also a setup routine VarDESC which allows one to set a description string for each variable separately. This string is used in documenting plots and files.

If called after ModelDESC, the description string will follow the description string set with ModelDESC. If called before ModelDESC, the string will replace the string set with ModelDESC.

Multiple calls result in multiple lines of documentation.

CALL VarDESC('TAUX','zonal wind stress computed using Large and Pond')

MODEL missing_value

Frequently one wants to flag missing data with a particular value or NaN. This can be done in the FORTRAN setup code by saying
CALL FGINTERP('/missing_value NaN def')
This sets the missing data flag for all the model variables.

If you would like to set the missing data value for a particular variable VAR to something else (e.g. 999.99), sometime after the NewVAR call that defines VAR, you say

CALL FGINTERP('VAR /missing_value 999.99 put')

File Interpreter FGFILE

Once all the model variables have been defined, it is time to process the Ingrid command file. this is done by
CALL FGFILE(IUNT)
where IUNT is the fortran unit number that the file is open on.

The File Interpreter skip all lines until it finds a line that starts \begin{ingrid}, and continue interpreting until it finds a \end{ingrid}. It then repeats the search until the end of the file. The intention was to make it easy to have other programs put their parameters in the same file.

There is a similar subroutine FGSECTION. It processes only a section of the input file, namely

CALL FGSECTION(IUNT,'mark')
will skip all lines until it finds a line that begins \section{mark}, at which point it starts looking for a line that begins \begin{ingrid}. It continues interpreting until it finds a \end{ingrid}. It then repeats,executing Ingrid commands between \begin{ingrid} and \end{ingrid} until it finds another line that starts \section{newmark}, at which point it stops reading until the next FGSECTION call.

FGSECTION will also skip all lines until it finds a line that begins \begin{mark}, at which point it starts looking for a line that begins \begin{ingrid}. It continues interpreting until it finds a \end{ingrid}. It then repeats,executing Ingrid commands between \begin{ingrid} and \end{ingrid} until it finds a line \end{mark}. This behavior is included for backward compatiability, the \section{mark} behavior makes for files that are more readily processed by LaTeX.

IDSTREAM

It is also possible to feed an Ingrid stream to a fortran program. IDSTREAM is the setup routine for this: it returns an integer ID which is later used by FGGET to actually extract the data.

IDSTREAM calls usually come after the FGFILE call, so that any variables defined in the Ingrid input file can be used by IDSTREAM. This means that the libraries opened by the createIngrid call are not necessarily still open.

One example of using IDSTREAM follows. Here we assumes that the input file has been read, and it has left SOURCES on the stack.

REAL TAUX(NX,NY)

ID = IDSTREAM('HELLERMAN taux X Y 2 REORDER')
...
DO I = 1 , NT
    CALL FGGET(ID,TAUX)
    ...
END DO

This example extracts taux from the HELLERMAN climatology, in chunks of XY with X varying the fastest. Since HELLERMAN is a climatology, NT should be 12. This could be insured by slightly different coding, namely

REAL TAUX(NX,NY)

CALL FGINTERP('HELLERMAN taux X Y 2 REORDER')
CALL FGINTERP('nchunk')
CALL FGIpopinteger(NT)

ID = IDSTREAM('X Y 2 REORDER')
...
DO I = 1 , NT
    CALL FGGET(ID,TAUX)
    ...
END DO

Here we set NT to the number of chunks in the stream, insuring that all the data is read. Note that the X Y 2 REORDER inside the IDSTREAM call is unecessary, but it does not hurt (the reordering filtered is appended only if necessary), and it does insure that the stream returned by IDSTREAM has the desired blocking.

A more common situation is to regrid the data to a (possibly different) model grid. Suppose we have a model grid XM, YM and TM. We then might have something like

REAL TAUX(NX,NY)

CALL FGINTERP('HELLERMAN taux X Y 2 REORDER')
CALL FGINTERP('X XM REGRID Y YM REGRID T TM REGRID')

ID = IDSTREAM('X Y 2 REORDER')
...
DO I = 1 , NT
    CALL FGGET(ID,TAUX)
    ...
END DO
Many functions in Ingrid do reorder the data stream: REGRID does not, so the second reordering is unecessary.

DoTasks and DoRVARS

DoTasks should be called at least once after FGFILE is called, in order to let any tasks run that do not depend on data from the model. I would usually call it once before the model starts running, and once just before the final STOP is executed, just to be sure that all tasks have had a chance to complete.

If there are any RECEEP variables, then DoRVARS must be called once per timestep.

Data access routines

The two routines used to get data to and from a FORTRAN program are FGGET and FGPUT respectively. As discussed above (see NewVar), these routines are called with two arguments: ID, which is an integer ID gotten from NewVar, and ARRAY, which is the array of values. For example,
CALL FGPUT( ID, VARARRAY)
passes data to Ingrid where VARARRAY has been filled with the appropriate values.

Note that NewVar is called exactly once for a given variable. Usually one will call NewVar in a setup routine, saving the ID in a common block. Then that ID is taken from the common block when FGPUT or FGGet is called.

FGPUT

There are two ways to pass data into Ingrid. The first way is new and preferred. First you create an ID with a call to NewVar
ID = NewVar('shortname', 'name', 'XYZT','XYZ')
where `XYZT' is replaced by whatever independent variables correspond to this variable (SetIVAR1 or SetIVAR2 should have been called once for each independent variable). 'XYZ' are the variables within each chunk (i.e. supplied within a single call to FGPUT), while the remaining variables ('T') vary across calls.

Then that ID is used to pass the array into Ingrid each time the array is calculated,

CALL FGPUT( ID, VARARRAY)
where VARARRAY has been filled with the appropriate values.

Note that NewVar is called exactly once for a given variable. Usually one will call NewVar in a setup routine, saving the ID in a common block. Then that ID is taken from the common block when FGPUT is called.

BufferNeeded

BufferNeeded(ID) allows a program to ask whether the array it is calculating is going to be used. It returns .FALSE. if Ingrid makes no use of the next realization of the buffer ID. This provides a way to avoid unnecessary calculations.

LOGICAL BufferNeeded

IF(BufferNeeded(ID))THEN
    DO I = 1 , N
        VAR(I) = ...
    END DO
END IF
CALL FGPUT( ID, VAR)

Note that FGPUT should be called even if BufferNeeded returns .FALSE.: this lets Ingrid kept track of the current realization number for that buffer.

Arguments to RECEEP(ST, KC, MQ, IJLIM, NSCRCE)

RECEEP is a subroutine you write that gets called when Ingrid needs data from the model RVAR variable.

INPUTS

OUTPUTS

While RECEEP is more efficient if you are not using a variable (FGPUT needs to check a value to find out there is nothing to do while RECEEP simply never executes), when you are using a variable FGPUT is much more efficient, and the overhead of checking a single variable is not all that high anyway. So new code really should use FGPUT.

Note that the COMMON block MGRID is not defined in Ingrid (unlike SUMRY), so that if your RECEEP depends on MGRID to get the model grid, it will not work.

linking to the Ingrid library

In order to link your program to ingrid, minimally you need the following libraries:
f77 -o myprog myprog.f  -lingrid -lnetcdf -lsun -lgrfps
If you called createHDF, you also need -ldf. If you called createEOS, you also need -leos.

Extending Ingrid

The design of Ingrid is intended to make it easy to add extensions. Almost everything in Ingrid can be considered a filter, and the filters come in two parts: a setup routine, which gets Ingrid parameters and directly corresponds to an Ingrid word, and the routine that actually does the calculation with the data, which has a fairly simple set of arguments and has had most things set up for it by the setup routine. The setup routine can be fairly inefficient: it is only called at the beginning and does not handle any large arrays of data. The calculation routine should be as efficient as possible: it has to process all the data.

Programmer's interface to fginterp

One of the major goals of designing this interpreter is to have a clean interface between Fortran and the interpreter, i.e. it should be very easy to add new functions written in Fortran (or C, for that matter), to the interpreter.

There are two parts to the interface: telling Ingrid how to call the new Fortran function, and letting the Fortran function get its arguments from Ingrid.

Telling Ingrid how to call Fortran functions

Informing Ingrid about a new function is done by FGIoper, namely
        EXTERNAL SUB
        CALL FGIoper('name',SUB)
defines the Fortran subroutine SUB to have the name `name' in the current object dictionary. SUB has no arguments. If you want to collect definitions in an object (a good idea since the root object--Ingrid--only has a finite amount of room for new definitions), you could do something like the following:
        EXTERNAL SUB1, SUB2, SUB3
        CALL FGINTERP('/MyWords: null 10 object def')
        CALL FGINTERP('MyWords: /:MyWords {pop} def')
        CALL FGIoper('name1',SUB1)
        CALL FGIoper('name2',SUB2)
        CALL FGIoper('name3',SUB3)
        CALL FGINTERP(':MyWords')
The first call to FGINTERP defines an object called MyWords:. The second call to FGINTERP opens that object (by putting it on the stack), and then defines a word :MyWords which will close the object (by popping it off the stack). Since MyWords: is still on the stack, the three calls to FGIoper define three new words in MyWords:, namely name1, name2, and name3. The final call to FGINTERP uses the newly defined :MyWords to close the object.

You could then use your new words by saying something like

\begin{ingrid}
MyWords:  name1 name2   :MyWords
\end{ingrid}
in an input file to Ingrid (FGFILE) or in calls to FGINTERP.

Passing Ingrid arguments to and from Fortran routines

Arguments are passed on a stack. The stack consists of typed elements, elements that have a structure FGIelements. The necessary definitions are in the include file fginterp.h.

There is a set of FORTRAN functions to pop elements from Ingrid's stack. They are all logical functions that return true if the element on the top of the stack is the given type or (in the case of integer and real) can be converted to that type.

FGIpopreal(X)
( real/integer -- ) extracts a real number X from the stack.
FGIpopinteger(I)
( real/integer -- ) extracts an integer I from the stack.
FGIpopname(ID)
( name -- ) extracts the name with integer id ID from the stack.
FGIpopstring(N,IPTR)
( string -- ) extracts the string of length N from the stack. IPTR points to the first character.
FGIpopintegerarray(N,IPTR)
( integerarray -- ) extracts the integerarray of length N from the stack. IPTR points to the first element.
FGIpoprealarray(N,IPTR)
( realarray -- ) extracts the realarray of length N from the stack. IPTR points to the first element.

There are also a set of routines that push elements onto Ingrid's stack. They are all subroutines.

FGIpushnull
( -- null ) puts a null element onto the stack.
FGIpushflag(LOGICAL)
( -- true/false ) puts a logical element onto the stack.
FGIpushreal(X)
( -- real ) puts a real number X onto the stack.
FGIpushinteger(I)
( -- integer ) puts an integer I onto the stack.
FGIpushname(STRING)
( -- name ) puts the name STRING onto the stack.
FGIpushstring(STRING)
( -- string ) puts the character array onto the stack. The data is copied for future reference by Ingrid.
FGIpushintegerarray(IS,N)
( -- integerarray ) puts the integerarray IS of length N onto the stack. Ingrid copies all the values.
FGIpushrealarray(XS,N)
( -- realarray ) puts the realarray XS of length N onto the stack. Ingrid copies all the values.

Ingrid can be convinced to use arrays without copying them. Keep in mind that Ingrid will most likely need to reference the data much later (i.e. in an FGPUT or FGGET call), so the data must be put in a static array or separately allocated. It is preferable to just let Ingrid allocate such arrays, so the use of the following routines is discouraged.

FGIpushintegerarray0(IS,N)
( -- integerarray ) puts the integerarray IS of length N onto the stack. Ingrid uses the values in place.
FGIpushrealarray0(XS,N)
( -- realarray ) puts the realarray XS of length N onto the stack. Ingrid uses the values in place.

An example of a subroutine that receives and passes arguments to Ingrid follows:

      SUBROUTINE SUB
#include "fginterp.h"

C gets an argument from the stack
      IF(.NOT.FGIpopreal(MYARG))THEN
         WRITE(6,*) 'bad argument to SUB'
      ENDIF

C puts argument on stack
      CALL FGIpushreal(OUTPUT)

      RETURN
      END
In this example, SUB takes one argument of type real or integer and puts it into an integer MYARG, and outputs one argument of type real.

Extracting information from objects is implicit in what we have said so far, but lets be more specific. Create the following object

/MyObject null 10 object def
MyObject
     /CreationTime 100. def
     /Creator (Fred Flintstone) def
     /Data [ 0 1 2 3 4 5 6 ] def
pop
Suppose we are writing a subroutine that takes that object as its argument, and from that object it needs to extract the information which is stored with the tag `CreationTime'. One would do the following (assuming that the object to be extracted from is already on the stack when the subroutine is called):
C puts CreationTime on stack
      CALL FGINTERP('CreationTime')
C copies it and pops creation time from the stack
      CALL FGIpopreal(TIME)
C pops object from stack, too
      CALL FGIpop
My plan is to create many different kinds of objects, and have operators that can manipulate them. One rather important object is the equivalent of a netCDF file, except that the data is put out in a stream rather than necessarily being stored in a file (i.e. a netCDF stream). I call these objects streams. There are many operators that can operate on these streams: in particular you can sample, print, plot, and create netCDF files.

One common configuration is to have a child object whose dictionary has only words that return data for the particular realization: the words of the parent manipulate that data. In particular, each stream object is a child of the parent stream object, and its dictionary contains words which return pointers to the actual data.

In SUMRY parlance, this is all SUMRYA stuff: setting up the work that actually needs to be done. There is also SUMRYB work, i.e. the calculations that are performed on the actual data. This is done by setting up a network of buffers connected by tasks: data is placed in the buffers, the tasks are then invoked, placing their output in buffers. A new round of tasks can then be invoked. Ultimately, tasks without output buffers are called: these generate plots or output files and end the sequence. There is no equivalent to SUMRYC -- all work is done as soon as the data is available, so that there is no work to be done at the end.

The routines invoked by the Ingrid interpreter will schedule the work so that when SUMRYB (or equivalent) is called, the proper calculations are made.

Go to the previous, next section.