Go to the previous, next section.
There is a later section (Test Code) which provides a small working example of how a model might function with Ingrid.
CALL createIngridWhen 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 createHDFThis enables
writeHDF
, which allows one to write HDF files. This
requires adding the library -ldf
when linking.
Optionally one can also call
CALL createEOSThis enables some oceanic equation of state functions (see section Stream filters). This requires adding the library
-leos
when linking.
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
CALL ModelDESC('The model description')
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')
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')
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 DOMany functions in Ingrid do reorder the data stream: REGRID does not, so the second reordering is unecessary.
If there are any RECEEP variables, then DoRVARS must be called once per timestep.
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.
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.
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.
f77 -o myprog myprog.f -lingrid -lnetcdf -lsun -lgrfpsIf you called
createHDF
, you also need -ldf
. If you
called createEOS
, you also need -leos
.
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.
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.
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 ENDIn 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 popSuppose 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 FGIpopMy 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.