The Lattice Expression Language (LEL) makes it possible to do arithmetic on lattices (in particular on images [which are just lattices plus coordinates]) in AIPS++. An expression can be seen as a lattice (or image) in itself. It can be used in any operation where a normal image is used.
To summarize, the following functionality is supported:
The first section explains the syntax. The last sections show the interface to LEL using Python or C++. The Python interface makes it possible to embed Python variables and expressions in a LEL command. At the end some examples are given. If you like, you can go straight to the examples and hopefully immediately be able to do some basic things.
LEL operates on lattices, which are a generalization of arrays. As said above a particular type of lattice is an image; they will be used most often. Because lattices can be very large and usually reside on disk, an expression is only evaluated when a chunk of its data is requested. This is similar to reading only the requested chunk of data from a disk file.
LEL is quite efficient and can therefore be used well in C++ and Python code. Note however, that it can never be as efficient as carefully constructed C++ code.
Note 216 gives a detailed description how LEL is implemented using various C++ classes.
A LEL expression can be as simple or as complex as one likes using the standard
arithmetic, comparison, and logical operators. Parentheses can be used to group
subexpressions.
The operands in an expression can be lattices, constants, functions, and condition
masks. lattice regions and masks. E.g.
The last example shows how a boolean expression can be used to form a mask on a lattice. Only the pixels fulfilling the boolean condition will be used when calculating the mean.
In general the result of a LEL expression is a lattice, but it can be a scalar too. If is is a scalar, it will be handled correctly by C++ and Python functions using it as the source in, say, an assignment to another lattice.
LEL fully supports masks. In most cases the mask of a subexpression is formed by and-ing the masks of its operands. It is fully explained in a later section.
LEL supports the following data types:
All these data types can be used for scalars and lattices.
LEL will do automatic data type promotion when needed. E.g. when a Double
and a Complex are used in an operation, they will be promoted to DComplex. It
is also possible to promote explicitly using the conversion functions (FLOAT,
DOUBLE, COMPLEX and DCOMPLEX). These functions can also be used to
demote a data type (e.g. convert from Double to Float), which can sometimes be
useful for better performance.
Region is a specific data type. It indicates a region of any type (in pixel or world coordinates, relative, fractional). A region can only be applied to a lattice subexpression using operator [].
Scalar constants of the various data types can be formed as follows (which is similar to Python):
Note that a full complex constant has to be enclosed in parentheses when, say, a multiplication is performed on it. E.g.
The functions pi() and e() should be used to specify the constants pi and e. Note that they form a Double constant, so when using e.g. pi with a Float lattice, it could make a lot of sense to convert pi to a Float. Otherwise the lattice is converted to a Double, which is time-consuming. However, one may have very valid reasons to convert to Double, e.g. to ensure that the calculations are accurate enough.
The following operators can be used (with their normal meaning and precedence):
Note that ^ has a higher precedence than the unary operators.
E.g. -3^2 results in -9.
The operands of these operators can be 2 scalars, 2 lattices, or a lattice and a scalar. When 2 lattices are used, they should in principle conform; i.e. they should have the same shape and coordinates. However, LEL will try if it can extend one lattice to make it conformant with the other. It can do that if both lattices have coordinates and if one lattice is a true subset of the other (thus if one lattice has all the coordinate axes of the other lattice and if those axes have the same length or have length 1). If so, LEL will add missing axes and/or stretch axes with length 1
In the following tables the function names are shown in uppercase, while the
result and argument types are shown in lowercase. Note, however, that function
names are case-insensitive. All functions can have scalar and/or lattice
arguments.
When a function can have multiple arguments (e.g. atan2), the operands are
automatically promoted where needed.
Several functions can operate on real or complex arguments. The data types of such arguments are given as ’numeric’.
Note that the trigonometric functions need their arguments in radians.
The result of these functions is a scalar.
will halve the size of axis 1 and 2.
gives . This can be used to form, for example, (biased) polarized intensity images when lat1 and lat2 are Stokes Q and U images.
gives . This can be used to form, for example, linear polarization position angle images when lat1 and lat2 are Stokes Q and U images, respectively.
where s1 and s2 are the source fluxes in the lattices and f1 and f2 are the frequencies of the spectral axes of both lattices. Similar to e.g. operator + the lattices do not need to have the same shape. One can be extended/stretched as needed.
The first example replaces each masked-off element in lat1 by 0.
The second example replaces it by the corresponding element in lat2. A
possible mask of lat2 is not used.
The examples shows that scalars and lattices can be freely mixed. When
all arguments are scalars, the result is a scalar. Otherwise the result is a
lattice.
Note that the mask of the result is formed by combining the mask of the
arguments in an appropriate way as explained in the section about mask
handling.
masks image such that only the pixels with an index 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 18, or 20 on the second axis are set to True.
The following special syntax exists for this function.
where i is the axis number. So the example above can also be written as:
Negated versions of this function exist as:
Note that, where necessary, up-conversions are done automatically. Usually it may only be needed to do a down-conversion (e.g. Double to Float).
When a lattice (e.g. an image) is used in an expression, its name has to be given. The name can be given directly if it consists of the characters -.$ ̃ and alphanumeric characters.
If the name contains other characters or if it is a reserved word (currently only T and F are reserved), it has to be escaped. Escaping can be done by preceeding the special characters with a backslash or by enclosing the string in single or double quotes. E.g.
Note that when LEL is used from Python, it is also possible to use a Python image variable as a lattice operand (e.g. $im). This is explained in the section describing the Python binding. It means that in Python a name starting with a $ should be escaped too.
A boolean mask associated with an image indicates whether a pixel is good (mask value True) or bad (mask value False). If the mask value is bad, then the image pixel is not used for computation (e.g. when finding the mean of the image).
An image can have zero (all pixels are good) or more masks. One mask can be designated as the default mask. By default it will be applied to the image (from Python, designation of the default mask is handled by the ?? function of the ?? tool).
When using LEL, the basic behaviour is that the default mask is used. However, by qualifying the image name with a suffix string, it is possible to specify that no mask or another mask should be used. The suffix is a colon followed by the word nomask or the name of the alternative mask.
The first example uses the default mask (if the image has one). The second example uses no mask (thus all pixels are designated good) and the third example uses mask othermask.
Note that even if the image name, colon and mask are enclosed in quotes, the colon is seen as the separator between image and mask name. However, if the colon in a quoted string is escaped with a backslash, the colon is seen as part of the name and is not treated as a separator.
It is also possible to use a mask from another image like
This syntax is explained in the section describing regions
We have seen in the previous section that lattices (in this case images) can have an associated mask. These masks are stored with the image – they are persistent.
It is also possible to create transient masks when a LEL expression is executed (dawn, usually). This is done with the operator [] and a boolean expression. For example,
creates a mask for lat1 indicating that only its elements fulfilling the boolean condition should be taken into account in the sum function. Note that the mask is local to that part of the expression. So in the expression
the second sum function takes all elements into account. Masking can also be applied to more complex expressions and it is recursive.
The first example applies the mask generated by the [] operator to the expression lat1+lat2. The second example shows the recursion (which ANDs the masks). It is effectively a (slower) implementation of the first example in this subsection. In the last example, the expression inside the parentheses is only evaluated where the condition [lat1<5] is true and the resulting expression has a mask associated with it.
Please note that it is possible to select pixels on an axis by means of the function INDEXIN (or by the INDEXi IN expression) as shown in the previous section about miscellaneous functions.
As explained in the previous subsections, lattices can have a mask. Examples are
a mask of good pixels in an image, a mask created using a boolean condition and
the operator [], or a mask defining a region within its bounding box.
A pixel is bad when the image has a mask and when the mask value for that
pixel is False. Functions like max ignore the bad pixels.
Note that in a MeasurementSet a False mask value indicates a good visibility.
Alas this is a historically grown distinction in radio-astronomy.
Image masks are combined and propagated throughout an expression. E.g. when two lattices are added, the mask of the result is formed by and-ing the masks of the two lattices. That is, the resultant mask is True where the mask of lattice one is true AND the mask of lattice 2 is True. Otherwise, the resultant mask is False.
In general the mask of a subexpression is formed by and-ing the masks of the operands. This is true for e.g. +, *, atan2, etc.. However, there are a few special cases:
the sum function will only sum those elements for which the mask of lat1 and lat2 is valid and for which the condition is true.
Let us say both lat1 and lat2 have masks. The operand lat1<0 is true if the mask of lat1 is true and the operand evaluates to true, otherwise it is false. Apply the same rule to the operand lat2 > 0. The AND operator gives true if the left and right operands are both true. If the left operand is false, the right operand is no longer relevant. It is, in fact, 3-valued logic with the values true, false, and undefined.
Thus, the full expression generates a lattice with a mask. The mask is true when the condition in the [] operator is true, and false otherwise. The values of the output lattice are only defined where its mask is true.
Consider the following more or less equivalent examples:
The first two use the default mask of image2 as the mask for image1.
The latter uses mask0 of image2 as the mask for image1. It is equivalent to the
first two examples if mask0 is the default mask of image2.
It is possible that the entire mask of a subexpression is false. For example, if the mean of such a subexpression is taken, the result is undefined. This is fully supported by LEL, because a scalar value also has a mask associated with it. One can see a masked-off scalar as a lattice with an all false mask. Hence an operation involving an undefined scalar results in an undefined scalar. The following functions act as described below on fully masked-off lattices:
You should also be aware that if you remove a mask from an image, the values of the image that were previously masked bad may have values that are meaningless.
A region-of-interest generally specifies a portion of a lattice which you are interested in for some astronomical purpose (e.g. what is the flux density of this source). Quite a rich variety of regions are supported in AIPS++. There are simple regions like a box or a polygon, and compound regions like unions and intersections. Regions may contain their own “region masks”. For example, with a 2-d polygon, the region is defined by the vertices, the bounding box and a mask which says whether a pixel inside the bounding box is inside of the polygon or outside of the polygon.
In addition, although masks and regions are used somewhat differently by the user, a mask is really a special kind of region; they are implemented with the same underlying code.
Like masks, regions can be persistently stored in image. From Python, regions are generated, manipulated and stored with the ?? tool.
We saw in the previous section how the condition operator [] could be used to generate masks with logical expressions. This operator has a further talent. A region of any type can be applied to a lattice with the [] operator. You can think of the region as also effectively being a logical expression. The only difference with what we have seen before is that it results in a lattice with the shape of the region’s bounding box. If the lattice or the region (as in the polygon above) has a mask, they are and-ed to form the result’s mask.
All types of regions supported in AIPS++ can be used, thus:
The documentation in the classes LCRegion, LCSlicer, and WCRegion) gives you more information about the various regions.
At this moment a region can not be defined in LEL itself. It is only possible to use regions predefined in an image or another table which is indicated using two colons.
When using Python (as will normally be done), it is also possible to use a region defined in Python using the $-notation. This is explained in more detail in the section discussing the interface to LEL.
A predefined region can be used by specifying its name. There are three ways to specify a region name:
Examples are
In the first example region reg1 is looked up in image myimage.data. It is assumed that reg1 is not the name of an image or lattice. It results in a lattice whose shape is the shape of the bounding box of the region. The mask of the result is the and of the region mask and the lattice mask.
In the second example it is stated explicitly that reg1 is a region by using the :: syntax. The region is looked up in otherimage, because that is the last table used in the expression. The result is a lattice with the shape of the bounding box of the region.
In the third example the region is looked up in myimage.data. Note that the this and the previous example also show that a region can be applied to a subexpression.
In the fourth example we have been very cunning. We have taken advantage of the fact that masks are special sorts of regions. We have told the image myimage.data not to apply any of its own masks. We have then used the [] operator to generate a mask from the mask stored in a different image, myotherimage. This effectively applies the mask from one image to another. Apart from copying the mask, this is the only way to do this.
Unions, intersections, differences and complements of regions can be
generated and stored (in C++ and Python). However, it is also possible to
form a union, etc. in LEL itself. However, that can only be done if the
regions have the same type (i.e. both in world or in pixel coordinates).
The following operators can be used:
The normal AIPS++ rules are used when a region is applied:
When giving a LEL expression, it is important to keep an eye on performance issues.
LEL itself will do some optimization:
the subexpression mean(lat2) is executed only once and not over and over again when the user gets chunks.
The user can optimize by specifying the expression carefully.
should be written as
because in that way the scalars form a scalar subexpression which is
calculated only once. Note that the subexpression parentheses are needed in
the first case, because multiplications are done from left to right.
In the future LEL will be optimized to shuffle the operands when possible
and needed.
The result of atan2 is single precision, because both operands are single precision. However, pi is double precision, so the result of atan2 is promoted to double precision to make the addition possible. Specifying the expression as:
avoids that (expensive) data type promotion.
In many of the expressions we have looked at in the examples, a mask has been generated. What happens to this mask and indeed the values of the expression depends upon the implementation. If for example, the function you are invoking with LEL writes out the result, then both the mask and result will be stored. On the other hand, it is possible to just use LEL expressions but never write out the results to disk. In this case, no data or mask is written to disk. You can read more about this in the interface section.
There are two interfaces to LEL. One is from Python and the other from C++. It depends upon your needs which one to use. Most high level users of AIPS++ will access LEL only via the Python interface.
The LEL interface in Python is provided in the pyrap.images package. The main constructor makes it possible to open an image expression as a virtual image.
Sometimes you need to double quote the file names in your expression. For example, if the images reside in a different directory as in this example.
Images created/opened in Python can directly be used in a LEL expression by prefixing their name with a $. Other Python variables and even Python expressions can be used in the same way using $variable or $(expression) in the LEL command. A variable can be a standard numeric scalar. An expression has to result in a numeric scalar.
A somewhat artificial example:
The substitution mechanism is described in more detail in pyrap.util.
This consists of 2 parts.
This example does the same as the Python one shown above.
forms an expression to clip the image. Note that the expression is written
as a normal C++ expression. The overloaded operators and functions in
class LatticeExprNode ensure that the expression is formed in the correct
way.
Note that a LatticeExprNode object is usually automatically converted to
a templated LatticeExpr object, which makes it possible to use it as a
normal Lattice.
So far the expression is only formed, but not evaluated. Evaluation is only
done when the expression is used in an operation, e.g. as the source of the
copy operation shown below.
The following examples show some LEL expressions (equally valid in C++ or Python).
Note that LEL is readonly; i.e. it does not change any value in the images given. A function in the image client has to be used to do something with the result (e.g. storing in another image).
Here follows a sample Python session showing some of the LEL capabilities and how Python variables can be used in LEL.
In the near or more distant future LEL will be enhanced by adding new features and by doing optimizations.