Getting Started Documentation Glish Learn More Programming Contact Us
Version 1.9 Build 1556
News FAQ
Search Home


next up previous
Next: Data Type Conversions Up: NOTE 216 - Lattice Expression Language Implementation Previous: Class Structures

Subsections


How It Works

Let us look at an example expression and examined how the system works.

For example,

   Lattice<Float> a;
   Lattice<Float> b;
   Lattice<Double> c;
   a.copyData(b+c);

Thus, we evaluate the sum of the Lattices b and c and fill the Lattice a with the result. Note that the result of b+c is a Double Lattice which will be assigned to a Float Lattice.

There are two distinct steps in this; the first is the creation of the expression tree. The second is the evaluation of the tree after its creation. Both occur at run time; the creation first, and then, via the copyData call, the evaluation.

The tree is a structure which can be thought of as representing the hierarchy of operations. For our example above, it looks like

     +
   b   c

Really, b and c are not operations, the actual software operation is something that gets the values of the Lattice into core from the Lattice. The tree is more accurately written as

                     +
  getLatticeValues       getLatticeValues
     b                          c

but we will use the short-hand tree expression style.

A more complex expression like (a+sin(b)+2) / 10 would be

               / 
         +          10.0
    +        a
sin    2.0
b

The tree is evaluated bottom up. Conceptually, the sin of the Lattice b is evaluated and 2.0 added to the resultant Lattice. Then that is added to the Lattice a, and that resultant Lattice is divided by 10.0.

Tree Creation

Let us consider the creation of the expression tree first. The Lattice::copyData member function expects as its argument a Lattice. Thus, the expression in the argument has to find a way to be converted to a Lattice. It is the LatticeExpr class that knows how to evaluate expressions involving Lattices, and LatticeExpr inherits from Lattice. So any LatticeExpr object is a valid argument for the copyData call. We need to show that b+c is a Lattice; in this case a LatticeExpr object, derived from Lattice.

Internally, LatticeExpr contains a LatticeExprNode object, so let us consider class LatticeExprNode first. LatticeExprNode exists to handle type conversions for mixed type expressions. It is a non-templated class and is not derived from any other class. It contains, as private data members, a variety of pointers to the class LELInterface. LELInterface is an abstract base class, from which are derived concrete classes. These derived classes are constructed in the tree, and when the expression is evaluated, they enable one to evaluate expressions such as binary expressions, or functions, or get chunks of a Lattice etc. These derived classes are (mostly) templated, and the LatticeExprNode class contains one LELInterface pointer object for each conceivable type (Float, Double, Complex, and DComplex). The appropriate type for the LELInterface pointer and the templated derived LELInterface object it is pointing to is the type of the data that it is manipulating. For example, if a LELLattice object is constructed from a Lattice<Float> then the appropriate type is Float.

Now, expressions like b and c can be converted to LatticeExprNode objects via constructors such as

   LatticeExprNode(const Lattice<Float>& lattice);
   LatticeExprNode(const Lattice<Double>& lattice);

Recall that the LatticeExprNode object contains private data members (LELInterface pointers) of many different types. Only the data member of the relevant type will be assigned. For example, the Float constructor looks like

   LatticeExprNode::LatticeExprNode (const Lattice<Float>& lattice)
   : donePrepare_p (False),
     dtype_p       (TpFloat),
     pExprFloat_p  (new LELLattice<Float> (lattice)) 
   {
      pAttr_p = &pExprFloat_p->getAttribute();
   }

The constructor notes that no optimizations (see later) have been performed, and also notes what type of data it is being asked to handle. Now look at the new statement. A pointer (pExprFloat$\scriptstyle \tt p$ of type LELInterface<Float>) to an object of class LELLattice is created (LELLattice is a class derived from LELInterface, the abstract base class).

Thus, from the expressions b or c, we can create LatticeExprNode objects from the Lattice objects associated with b and c. We must now look at the full expression, b+c. Remember that Lattice b is of type Float and Lattice c is of type Double, and the output Lattice a is of type Float (and therefore copyData is expecting a Lattice<Float> for its argument).

LatticeExprNode has an operator + function declared as

   friend LatticeExprNode operator+ (const LatticeExprNode& left,  
                                     const LatticeExprNode& right);

The friend keyword makes it a globally accessible operator. Now, you can see that it takes two other LatticeExprNode objects, in our case, those that we made from b and c.

The + operator returns another LatticeExprNode object; it is defined as

   LatticeExprNode operator+ (const LatticeExprNode& left,
                              const LatticeExprNode& right)
   {
      return LatticeExprNode::newNumBinary (LELBinaryEnums::ADD, left, right);
   }

where the static function LatticeExprNode::newNumBinary has returned the desired LatticeExprNode object (which embodies the two subexpression, b and c).

Now, recall that what we really want in the copyData call is an object of type LatticeExpr (which is a Lattice). Currently we have a LatticeExprNode object. So there has to be an automatic conversion from the non-templated LatticeExprNode object to the templated LatticeExpr object. This is done with one of the operators in LatticeExprNode from the list

   operator LatticeExpr<Float>();
   operator LatticeExpr<Double>();
   operator LatticeExpr<Complex>();
   operator LatticeExpr<DComplex>();
   operator LatticeExpr<Bool>();

For reasons that we don't understand, this could not be made to work yet with a constructor of the type

   LatticeExpr (const LatticeExprNode& expr)

These operators are are in reality casting operators. For example, if you had

   Double x; 
   Int i;  
   x = (Double)i;

the i would be cast to a Double. Similarly,

   LatticeExprNode node; 
   LatticeExpr<Float> expr; 
   expr = (LatticeExpr<Float>)node;

converts the node to the expr.

Since they are in class LatticeExprNode, they expect to operate on a LatticeExprNode object. The name of the operator is the same as the return type: LatticeExpr<T> This is in general a dangerous practice, as one gets automatic conversions that weren't wanted sometimes. But we seem to have no choice for now.

Now, for our example, the type that Lattice::copyData is expecting is a Float, because that is the type of the Lattice a. Therefore, the casting operator that will be invoked is

   LatticeExprNode::operator LatticeExpr<Float>()
  {
      return LatticeExpr<Float> (LatticeExprNode(makeFloat()), 0);
   }

So a LatticeExpr constructor of the form

   LatticeExpr (const LatticeExprNode& expr, uInt iDummy)

is explicitly invoked by this casting operator. First however, the makeFloat function is invoked explicitly to convert the data in the LatticeExprNode object to the correct internal type, which is Float for our example. Actually, the return type from makeFloat is a CountedPtr<LELInterface<Float>>. Therefore, to convert that to a LatticeExprNode, the constructor

   LatticeExprNode(LELInterface<Float>* expr);

is automatically invoked. This is given to the LatticeExpr constructor and finally returned.

Now returning to the newNumBinary static function above, there is another subtlety being handled. Here is where we handle some additional type conversion. We know that Lattice a wants a Float Lattice in the Lattice::copyData function. We saw above that the newNumBinary static function produced a LatticeExprNode which was automatically converted to a LatticeExpr object of type Float. The thing we didn't see yet was how the handling of the mixed type expression b+c was dealt with by newNumBinary. That is, we don't know yet what the type of b+c was, although we know that makeFloat was able to handle it, whatever it was. So let us look inside newNumBinary.

This function is implemented as

   LatticeExprNode LatticeExprNode::newNumBinary (LELBinaryEnums::Operation oper,
                                                  const LatticeExprNode& left,
                                                  const LatticeExprNode& right)   
   {   
      DataType dtype = resultDataType (left.dataType(), right.dataType());
      switch (dtype) {
      case TpFloat:
         return new LELBinary<Float> (oper, left.makeFloat(),
                                      right.makeFloat());
      case TpDouble:
         return new LELBinary<Double> (oper, left.makeDouble(),
                                       right.makeDouble());
      case TpComplex:
         return new LELBinary<Complex> (oper, left.makeComplex(),
                                        right.makeComplex());
      case TpDComplex:
         return new LELBinary<DComplex> (oper, left.makeDComplex(),
                                         right.makeDComplex());
      default:
         throw (AipsError
               ("LatticeExpr: Bool argument used in numerical binary operation"));
      }
      return LatticeExprNode();
   }

This function returns an expression of one type, as the two expressions that go into it may have different types. Indeed, in our case, the left expression is a Float and the right a Double. The function LatticeExprNode::resultDataType says that mixing these two types should result in a Double so as not to lose precision. Therefore, the left and right expressions are converted to a Double expression and the LELBinary object that is created is a Double (see also the section on type conversions).

In addition, it can be seen that the return statements are returning pointers to objects of type LELBinary, which is derived from LELInterface. Yet, the function newNumBinary actually returns an object of type LatticeExprNode. So what is happening is an implicit conversion via a constructor. It's one of the private constructors

   LatticeExprNode(LELInterface<Float>* expr);
   LatticeExprNode(LELInterface<Double>* expr);
   LatticeExprNode(LELInterface<Complex>* expr);
   LatticeExprNode(LELInterface<DComplex>* expr);
   LatticeExprNode(LELInterface<Bool>* expr);

that is doing the work.

Evaluation

How does copyData manage to extract the result of the expression evaluation ? The copyData function ultimately calls the Lattice function getSlice (via an iterator) to fish out the data from its Lattice argument. getSlice is therefore implemented in LatticeExpr (as it inherits from Lattice where getSlice is declared). We have seen that LatticeExpr has one private data member, and it is of type LatticeExprNode. The implementation of LatticeExpr::getSlice is to call the eval function of its LatticeExprNode private data member (recall that LatticeExprNode has a variety of pointers like CountedPtr<LELInterface<Float>> for each data type). LatticeExprNode has many self-similar eval functions, one for each type (Float, Double etc). Although the LatticeExprNode object does know for what type it was constructed, it actually chooses the correct version of the eval function by the argument signature. This works because a buffer is included in the eval interface (this is where the result of the expression is put), and the buffer is of the appropriate type.

So invoking eval of LatticeExprNode invokes eval of the object (which has been derived from LELInterface) and is pointed to by the appropriately typed CountedPtr<LELInterface<T>>. In our example involving adding two Lattices together, those derived classes would be LELLattice (to read the data from the Lattice) and LELBinary (to add the data). For LELLattice, its eval function actually then uses the getSlice function on the actual Lattice from which it was constructed (b or c) to fish out the data. The LELBinary eval function will add the numbers together.

Finally, since copyData is actually iterating through the LatticeExpr (Lattice) object in optimally sized chunks. The Lattice expression is evaluated chunk by chunk (usually tile by tile). This means that there are no large temporary Lattices stored.

   virtual void eval (Array<T>& result,
                      const PixelRegion& region) const = 0;

The derived classes make the actual implementation. The result of the evaluation of the expression is put in the result array. If the result of the expression evaluation is known to be a scalar (figured out at tree construction time) then the getScalar function is used to get the value instead.

   virtual T getScalar() const = 0;

Let's look at eval implementations for LELBinary and LELLattice. First, the piece for LELBinary relevant to the + operator.

   template <class T>
   void LELBinary<T>::eval(Array<T>& result,
                           const PixelRegion& region) const
   {
      switch(op_p) {
      case LELBinaryEnums::ADD :
          if (pLeftExpr_p->isScalar()) {
             pRightExpr_p->eval(result, region);
             result += pLeftExpr_p->getScalar();
          } else if (pRightExpr_p->isScalar()) {
             pLeftExpr_p->eval(result, region);
             result += pRightExpr_p->getScalar();
          } else {
             Array<T> temp(result.shape());
             pLeftExpr_p->eval(result, region);
             pRightExpr_p->eval(temp, region);
             result += temp;
          }
          break;

Three cases are handled here: (array,array), (scalar,array) and (array,scalar). The case of (scalar,scalar) is handled similarly in LELBinary::getScalar.

The important thing to see here is that the process is recursive. Each of the left and right expressions are evaluated first, before the + operation is done. So for example, since our example is the (array,array) case, we have

          Array<T> temp(result.shape());
          pLeftExpr_p->eval(result, region);
          pRightExpr_p->eval(temp, region);
          result += temp;

Both the left and right expressions are LELLattice objects. Evaluating them results in filling the result array with the values from the Lattice in the region. Then the two arrays (result and temp) are added to make the binary operation result. The LELLattice eval function looks like

   template <class T>
   void LELLattice<T>::eval(Array<T>& result,
                            const PixelRegion& region) const
   {
   // The rwRef function will make a copy when needed (i.e. when ptr
   // contains a reference to the original data).
    
      COWPtr<Array<T> > ptr;
      pLattice_p->getSlice(ptr, region.box(), False);
      result.reference(ptr.rwRef());
   }
the Lattice function getSlice is used to recover the pixels into the array result. Note we use a COWPtr so that for say an ArrayLattice, the array references the data only saving a copy, unless it is actually written to. There is no LELLattice::getScalar function as it doesn't make any sense. If you try to call it, you will throw an exception.


next up previous
Next: Data Type Conversions Up: NOTE 216 - Lattice Expression Language Implementation Previous: Class Structures
Please send questions or comments about AIPS++ to aips2-request@nrao.edu.
Copyright © 1995-2000 Associated Universities Inc., Washington, D.C.

Return to AIPS++ Home Page
2006-10-15