Using VisAD Data Model in Everyday Programming

March, 2000

Introduction

Fundamental to VisAD is the Data Model.  The Data Model is a collection of VisAD interfaces and classes that allow the programmer to describe the data to be used in their program: the associated units and error estimates, how is it organized, related to other data, and so on.  Some Data Model  class objects contain no "real" data, but may contain meta data (data about the data, such as the units) or show how different data sets should be grouped for making a display.

While there are many extensions to this collection of classes and interfaces, we shall only introduce the fundamental Data object types and illustrate the parallels with quantities you may already be familiar with.  Each will have a source code example to allow you to experiment on your own.

If you are more comfortable with Python, then we've made a Python (Jython) version of this tutorial that shows the examples in that language.

Scalars

In VisAD the values of a Scalar may be either Real which is used for numeric quantities, or Text for strings of characters. In this discussion, we deal only with Real objects.

Real (actual) numbers

No doubt about it, if you've written Java code, you are already familiar with doing something like:
double a,b,c;
a = 10.;
b = 255.;
c = a + b;
System.out.println("sum = "+c);
If you are new to Java, but come from a Fortran background, then this might be more familiar:
REAL A,B,C
A = 10
B = 255
C = A + B
WRITE(*,*)'sum = ',C
Well, if you want to use the VisAD Data model, here's what you'd say (the complete program is provided):
import visad.*;
public class dataex1 {
  public static void main (String arg[]) {
    try {

      Real a,b,c;
      a = new Real(10.);
      b = new Real(255.);
      c = (Real) a.add(b);
      System.out.println("sum = "+c);

    } catch (Exception e) {System.out.println(e);}
  }
}
When you run this example, you get:
sum = 265.0
By doing this, of course, you are not going to be convinced that there is any advantage to using the VisAD Data model.  So, let's explore another form of constructor for visad.Real.

Estimating Errors

The form of constructor for Real
Real(double value, double error)
allows us to provide an error estimate for the value.  This estimate can then be propagated through mathematical operations to provide an error estimate.
Real a,b,c;
a = new Real(10.,1.);
b = new Real(255.,10.);
c = (Real) a.add(b);
System.out.println("sum = "+c);
When you run this example, you still get:
sum = 265.0
The VisAD default for most math operations, however, is to not propagate errors, so to make use of this, we must explicitly indicate how we want error estimate to propagate. This is done by using an alternate signature of the add method (and all other math operators):
Real a,b,c;
a = new Real(10.,1.);
b = new Real(255.,10.);
c = (Real) a.add(b, Data.NEAREST_NEIGHBOR, Data.INDEPENDENT);
System.out.println("sum = "+c);
System.out.println("error of sum is="+c.getError().getErrorValue());
When you run this example, you get:
sum = 265.0
error of sum is=10.04987562112089
The constants supplied to the add method are the type of interpolation and the type of error propagation. In this simple case, the type of interpolation is not really relevant, but as you will see later, VisAD Data types may contain finite approximations to continuous functions and when these are combined mathematically, may need to be resampled in order to match domains.

Using Units

Another powerful feature of the VisAD Data model is that it may handle units. If your quantities are physical and have associated Units, then you might prefer to create a VisAD MathType that explicitly defines the metadata characteristics of your quantities. A MathType is used to define the kind of mathematical object that the Data object approximates. Every Data object in VisAD must have a MathType. In the previous examples, a default MathType with the name "Generic" was implicitly used for our Real objects.

In the simplest form for dealing with Units, the constructor for a MathType which defines Real values is:

RealType(String name, Unit u, Set s)
which allows you to assign a unique name to this MathType, a Unit for this, and define a default Set. In practice, the Set is seldom used and should just be passed as null in most cases.

To make use of this, we modify the program to read as follows:

Real t1,t2,sum;

RealType k = new RealType("kelvin",SI.kelvin,null);
t2 = new Real(k,273.);
t1 = new Real(k,255.);

sum = (Real) t1.add(t2);

System.out.println("sum = "+sum+" "+sum.getUnit());
When you run this example, you get:
sum = 528.0 K
In this example, we were able to use an SI Unit (ampere, candela, kelvin, kilogram, meter, second, mole, radian, steradian). Note that we constructed two variables with the same MathType, that is the same name, Unit, and Set. The only thing that is different is the numeric value.

If you are using some other unit, VisAD provides mechanisms for making up Units for those. As an example, you can use the Parser.parse() method from the visad.data.netcdf.units package to create a VisAD Unit from a String name.

Real t1,t2,sum;

Unit degC = visad.data.netcdf.units.Parser.parse("degC");
RealType tc = new RealType("tempsC",degC,null);
t2 = new Real(tc,10.);

RealType k = new RealType("kelvin",SI.kelvin,null);
t1 = new Real(k,255.);

sum = (Real) t1.add(t2);

System.out.println("sum = "+sum+" "+sum.getUnit());
When you run this example, you get:
sum = 538.15 K
Observe that although we defined the value of variable y to be in degree Celsius, when we added the two variables together, the value of y was automatically converted to degrees Kelvin. As long as the units are transformable, VisAD handles this. If you attempt to combine quantities with incompatible units, an Exception is thrown.

If you'd like to get the value listing in Celsius, then change the println to read:

System.out.println("sum = "+sum.getValue(degC)+" "+degC);
When doing arithmetic on Real objects, you may need at some point to use a constant value for something. As with all VisAD Data objects, in order to perform these operations, the Units must all match. When you use the simplest form of constructor for Real to define a numeric value, VisAD sets its Unit to a default value which can then be used to do arithmetic with any other Real. To illustrate, let's modify the previous example to compute the average of the two temperature values:
Real t1,t2,average;

Unit degC = visad.data.netcdf.units.Parser.parse("degC");
RealType tc = new RealType("tempsC",degC,null);
t2 = new Real(tc,10.);

RealType k = new RealType("kelvin",SI.kelvin,null);
t1 = new Real(k,255.);

Real two = new Real(2.0);

average = (Real) t1.add(t2).divide(two);

System.out.println("average = "+average+" "+average.getUnit());
When you run this program, you get:
average = 269.075 K

Tuples

A Tuple object contains a collection of Data objects whose number, sequence and type are defined by the MathType of the Tuple. There is also a subclass of Tuple named RealTuple which is a collection of Real objects, but again whose number and sequence are fixed by the RealTupleType associated with the RealTuple. This object is like a fixed-length vector, like (x, y, z). Contrast this with a Java array which is a set of identical objects of that particular type. You will, in fact, find a constructor for VisAD's RealTuple where the data values are passed in as an array of Reals.

Making the MathTypes

Let's suppose we want to have a single object that keeps a collection of values of temperature, wind speed, and time together. The approach is to first define the MathTypes for each of these quantities. For example:
RealType temperature, speed, time;
   
Unit degC = visad.data.netcdf.units.Parser.parse("degC");
temperature = new RealType("temperature", degC, null);

Unit kts = visad.data.netcdf.units.Parser.parse("kts");
speed = new RealType("speed", kts, null);

Unit sec = visad.data.netcdf.units.Parser.parse("seconds");
time = new RealType("time", sec, null);

RealTupleType mydata = new RealTupleType(time, speed, temperature);

Using numbers

Now that we've defined the MathTypes, let's see how this works with some "real" data. Add the following lines of code to the above fragment:
double obsTemp = 32.;
double obsSpeed = 15.;
double obsTime = 4096.;

double[] values = {obsTime, obsSpeed, obsTemp};

RealTuple obs = new RealTuple(mydata, values);

System.out.println("obs = "+obs);

When you run all this now, you get:
obs = (4096.0, 15.0, 32.0)

Arithmetic with Tuples

Let us now suppose we have a second set of observed data and add this code onto the end of our example:
double obsTemp2 = -10.;
double obsSpeed2 = 7.;
double obsTime2 = 1234.;

double[] values2 = {obsTime2, obsSpeed2, obsTemp2};

RealTuple obs2 = new RealTuple(mydata, values2);
System.out.println("obs2 = "+obs2);
When you run this addition, you get:
obs = (4096.0, 15.0, 32.0)
obs2 = (1234.0, 7.0, -10.0)
Our main purpose is to average all the values together. Again we need to define a Real for our constant, and then do just what we did previously:
Real two = new Real(2.0);

RealTuple avg = (RealTuple) obs.add(obs2).divide(two);
System.out.println("avg = "+avg);

Finally when you run the complete example, you get:
obs = (4096.0, 15.0, 32.0)
obs2 = (1234.0, 7.0, -10.0)
avg = (2665.0, 11.0, 284.15)

Note that the temperature was converted to the base unit of kelvin.

Most important -- as you can see the arithmetic capability of VisAD applies to all types of Data objects in the same manner.

Although we have used a VisAD RealTuple, it is of course just a specific kind of a Tuple that only contains Reals. Tuples can be used to collect together all types of VisAD Data objects...including Sets and Functions.

Sets

As shown in the next section, Set objects are most often used to define the finite sampling of the domain of Field Objects (which approximates a function by interpolating its values at a finite subset of its domain). In VisAD, the Set class has many sub-classes for different ways of defining finite subsets of the Set's domain.  Near the top of the list is the DoubleSet which includes all the double precision values that can be represented in the computer's 64 bit word....it is a finite, but very large Set. Farther down the hierarchy, for example, is the Linear1DSet which consists of n values of a simple arithmetic progression between two specified values.

VisAD comes with lots of implementations of the Set interface, in order to represent lots of common topologies. In this introduction, however, we'll deal only with one form -- the Linear1DSet.

The Set object also defines the CoordinateSystem of the Field's domain and the Units of the domain's RealType components. The following section will deal more explicitly with Fields.

Making a Set

Working with Sets is deceptively easy. For example, a program that contains these two lines of code:
Linear1DSet s = new Linear1DSet(-33., 33., 5);
System.out.println("set s = "+s);
will produce this output when run:
Set s = Linear1DSet: Length = 5 Range = -33.0 to 33.0
The Linear1DSet defined in this case has associated MathType of "Generic". There is an alternate form of the constructor for a Linear1DSet that allows you to define the MathType for this set of numbers, as well.

Set methods

There are several usage methods available for working with Sets. For example, you may need an enumeration of the values of our little Linear1DSet. If you add the code:
float[][] sam = s.getSamples();

for (int i=0; i<sam[0].length; i++) {
  System.out.println("i = "+i+"  sample = "+sam[0][i]);
}
the program would produce the following output:
set s = Linear1DSet: Length = 5 Range = -33.0 to 33.0

i = 0  sample = -33.0
i = 1  sample = -16.5
i = 2  sample = 0.0
i = 3  sample = 16.5
i = 4  sample = 33.0
Other methods worth checking out include indexToDouble() which returns an array of doubles given an array of indices. There is also an inverse method, doubleToIndex().

Functions

A VisAD Function Data object represents a function from a domain to values of some specific type (a range). Field is the subclass of Function for functions represented by finite sets of samples of function values (for example, a satellite image samples a continuous radiance function at a finite set of pixel locations). This object is really the heart of VisAD's Data model when working with many forms of geophysical data, which tend to be samples of continuous fields.

In order to use Function objects, it is necessary to define the sampling using a Set of some kind for the domain and to supply appropriate samples for the corresponding range values. Let's make a small example. In this case, I want to define a Function that can convert temperatures from degrees Fahrenheit to Kelvin.
 

1. The MathTypes

First, we must define the appropriate MathTypes:
RealType domain = new RealType("temp_F");
RealType range = new RealType("temp_Kelvin");

FunctionType convertTemp = new FunctionType(domain, range);
Here, we have defined the RealType for our domain to represent degrees F, and for the range for degrees K. The FunctionType defines the mapping from the domain to the range.

2. The samples

Now we need to define and set the values for the samples of the Function. Let's say that we know the values of temperature in both units at -40F and 212F:
    Set domain_set = new Linear1DSet(-40., 212., 2);
    
We use a 1D Set because we are only defining a scalar at each point in the range (rather than a vector).

3. The FlatField object

Finally, we need to construct the VisAD Data object that will provide for the desired finite sampling. The FlatField, which is a subclass of Field designed for use with the Java primitive type double, provides just such a representation and functionality. So we can say:
FlatField convertData = new FlatField(convertTemp, domain_set);
Which constructs a FlatField which is defined by a FunctionType defined over the values of the domain_set.

First, create an array that contains the numeric values of the range samples at the two points in the domain_set:

      double[][] values = new double[1][2];
      values[0][0] = 233.15; // = -40F
      values[0][1] = 373.15; // = 212F
Then put the range samples into the FlatField using:
    convertData.setSamples(values);

4. Evaluating Functions

Okay, so now let's test our Function by providing a domain value (that is, a temperature in degrees F) that we want to convert:
Real e = new Real(14.0);
Data v = convertData.evaluate(e);

System.out.println("value for 14.0F = "+v);

double vf = (((Real)v).getValue() - 273.15)*9./5. + 32;
System.out.println("          or (doing the math) = "+vf);
When you run this whole example, you get:
value for 14.0F = 263.1499938964844
          or (doing the math) = 13.999989013671915

Sampling modes

By default, the evaluate() method in a Function uses the sampling mode called Data.WEIGHTED_AVERAGE. You may also sample using Data.NEAREST_NEIGHBOR, which in this case would give a different result, since the domain value of 14.0 would be closest to the domain sample at -40.0, which then means that 233.15 is the associated range value.

If we change the evalute() call to read:

      double v = convertData.evaluate(e, Data.NEAREST_NEIGHBOR, Data.NO_ERRORS);
then results in this output when you run the program now:
value for 14.0F = 233.14999389648438
          or (doing the math) = -40.00001098632808

Parting points...