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.
There are a series of routines that need to be called to setup a model to use Ingrid. They fall into several groups.
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.
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
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')
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')
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')
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.
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 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.
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.
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(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.
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.
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.
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.
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.
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.
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)
X from the stack.
FGIpopinteger(I)
I from the stack.
FGIpopname(ID)
ID from the stack.
FGIpopstring(N,IPTR)
N from the stack.
IPTR points to the first character.
FGIpopintegerarray(N,IPTR)
N from
the stack. IPTR points to the first element.
FGIpoprealarray(N,IPTR)
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
FGIpushflag(LOGICAL)
FGIpushreal(X)
X onto the stack.
FGIpushinteger(I)
I onto the stack.
FGIpushname(STRING)
STRING onto the stack.
FGIpushstring(STRING)
FGIpushintegerarray(IS,N)
IS of length N onto
the stack. Ingrid copies all the values.
FGIpushrealarray(XS,N)
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)
IS of length N onto
the stack. Ingrid uses the values in place.
FGIpushrealarray0(XS,N)
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 first, previous, next, last section, table of contents.