20 Creating Your Own Private Mappings (IntraMaps)

 20.1 The Need for Extensibility
 20.2 The IntraMap Model
 20.3 Limitations of IntraMaps
 20.4 Writing a Transformation Function
 20.5 Registering a Transformation Function
 20.6 Creating an IntraMap
 20.7 Restricted Implementations of Transformation Functions
 20.8 Variable Numbers of Coordinates
 20.9 Adapting a Transformation Function to Individual IntraMaps
 20.10 Simplifying IntraMaps
 20.11 Writing and Reading IntraMaps
 20.12 Managing Transformation Functions in Libraries

20.1 The Need for Extensibility

However many Mapping classes are provided by AST, sooner or later you will want to transform coordinates in some way that has not been foreseen. You might want to plot a graph in some novel curvilinear coordinate system (perhaps you already have a WCS system in your software and just want to use AST for its graphical capabilities). Alternatively, you might need to calibrate a complex dataset (like an objective prism plate) where each position must be converted to world coordinates with reference to calibration data under the control of an elaborate algorithm.

In such cases, it is clear that the basic pre-formed components provided by AST for building Mappings are just not enough. What you need is access to a programming language. However, if you write your own software to transform coordinate values, then it must be made available in the form of an AST class (from which you can create Objects) before it can be used in conjunction with other AST facilities.

At this point you might consider writing your own AST class, but this is not recommended. Not only would the internal conventions used by AST take some time to master, but you might also find yourself having to change your software whenever a new version of AST was released. Fortunately, there is a much easier route provided by the IntraMap class.

20.2 The IntraMap Model

To allow you to write your own Mappings, AST provides a special kind of Mapping called an IntraMap. An IntraMap is a sort of “wrapper” for a coordinate transformation function written in C. You write this function yourself and then register it with AST. This, in effect, creates a new class from which you can create Mappings (i.e. IntraMaps) which will transform coordinates in whatever way your transformation function specifies.

Because IntraMaps are Mappings, they may be used in the same way as any other Mapping. For instance, they may be combined in series or parallel with other Mappings using a CmpMap6), they may be inverted (§5.6), you may enquire about their attributes (§4.5), they may be inserted into FrameSets (§13), etc. They do, however, have some important limitations of which you should be aware before we go on to consider how to create them.

20.3 Limitations of IntraMaps

By now, you might be wondering why any other kind of Mapping is required at all. After all, why not simply write your own coordinate transformation functions in C, wrap them up in IntraMaps and do away with all the other Mapping classes in AST?

The reason is not too hard to find. Any transformation function you write is created solely by you, so it is a private extension which does not form a permanent part of AST. If you use it to calibrate some data and then pass that data to someone else, who has only the standard version of AST, then they will not be able to interpret it.

Thus, while an IntraMap is fine for use by you and your collaborators (who we assume have access to the same transformation functions), it does not address the need for universal data exchange like other AST Mappings do. This is where the “Intra” in the class name “IntraMap” comes from, implying private or internal usage.

For this reason, it is unwise to store IntraMaps in datasets, unless they will be used solely for communication between collaborating items of software which share conventions about their use. A private database describing coordinate systems on a graphics device might be an example where IntraMaps would be suitable, because the data would probably never be accessed by anyone else’s software. Restricting IntraMap usage to within a single program (i.e. never writing it out) is, of course, completely safe.

If, by accident, an IntraMap should happen to escape as part of a dataset, then the unsuspecting recipient is likely to receive an error message when they attempt to read the data. However, AST will associate details of the IntraMap’s transformation function and its author (if provided) with the data, so that the recipient can make an intelligent enquiry to obtain the necessary software if this proves essential.

20.4 Writing a Transformation Function

The first stage in creating an IntraMap is to write the coordinate transformation function. This should have a calling interface like the astTranP function provided by AST (q.v.). Here is a simple example of a suitable transformation function which transforms coordinates by squaring them:

#include "star/ast.h"
#include <math.h>

void SqrTran( AstMapping *this, int npoint, int ncoord_in,
              const double *ptr_in[], int forward, int ncoord_out,
              double *ptr_out[] ) {
   int point, coord;
   double x;

/* Forward transformation. */
   if ( forward ) {
      for ( point = 0; point < npoint; point++ ) {
         for ( coord = 0; coord < ncoord_in; coord++ ) {
            x = ptr_in[ coord ][ point ];
            ptr_out[ coord ][ point ] = ( x == AST__BAD ) ? AST__BAD : x * x;
         }
      }

/* Inverse transformation. */
   } else {
      for ( point = 0; point < npoint; point++ ) {
         for ( coord = 0; coord < ncoord_in; coord++ ) {
            x = ptr_in[ coord ][ point ];
            ptr_out[ coord ][ point ] =
               ( x < 0.0 || x == AST__BAD ) ? AST__BAD : sqrt( x );
         }
      }
   }
}

As you can see, the function comes in two halves which implement the forward and inverse coordinate transformations. The number of points to be transformed (“npoint”) and the numbers of input and output coordinates per point (“ncoord_in” and “ncoord_out”—in this case both are assumed equal) are passed to the function. A pair of loops then accesses all the coordinate values. Note that it is legitimate to omit one or other of the forward/inverse transformations and simply not to implement it, if it will not be required. It is also permissible to require that the numbers of input and output coordinates be fixed (e.g. at 2), or to write the function so that it can handle arbitrary dimensionality, as here.

Before using an incoming coordinate, the function must first check that it is not set to the value AST__BAD, which indicates missing data (§5.9). If it is, the same value is also assigned to any affected output coordinates. The value AST__BAD is also generated if any coordinates cannot be transformed. In this example, this can happen with the inverse transformation if negative values are encountered, so that the square root cannot be taken.

There are very few restrictions on what a coordinate transformation function may do. For example, it may freely perform I/O to access any external data needed, it may invoke other AST facilities (but beware of unwanted recursion), etc. Typically, you may also want to pass information to it via global variables. Remember, however, that whatever facilities the transformation function requires must be available in every program which uses it.

Generally, it is not a good idea to retain context information within a transformation function. That is, it should transform each set of coordinates as a single point and retain no memory of the points it has transformed before. This is in order to conform with the AST model of a Mapping.

If an error occurs within a transformation function, it should use the astSetStatus function (§4.15) to set the AST status to an error value before returning. This will alert AST to the error, causing it to abort the current operation. The error value AST__ITFER is available for this purpose, but other values may also be used (e.g. if you wish to distinguish different types of error).

20.5 Registering a Transformation Function

Having written your coordinate transformation function, the next step is to register it with AST. Registration is performed using astIntraReg, as follows:

void SqrTran( AstMapping *, int, int, const double *[], int, int, double *[] );

const char *author, *contact, *purpose;

...

purpose = "Square each coordinate value";
author =  "R.F. Warren-Smith & D.S. Berry";
contact = "http://www.starlink.ac.uk/cgi-bin/htxserver/sun211.htx/?xref_SqrTran";

astIntraReg( "SqrTran", 2, 2, SqrTran, 0, purpose, author, contact );

Note that you should also provide a function prototype to describe the transformation function (the implementation of the function itself would suffice, of course).

The first argument to astIntraReg is a name by which the transformation function will be known. This will be used when we come to create an IntraMap and is case sensitive. We recommend that you use the actual function name here and make this sufficiently unusual that it is unlikely to clash with any other functions in most people’s software.

The next two arguments specify the number of input and output coordinates which the transformation function will handle. These correspond with the Nin and Nout attributes of the IntraMap we will create. Here, we have set them both to 2, which means that we will only be able to create IntraMaps with 2 input and 2 output coordinates (despite the fact that the transformation function can actually handle other dimensionalities). We will see later (§20.8) how to remove this restriction.

The fourth argument should contain a set of flags which describe the transformation function in a little more detail. We will return to this shortly (§20.7 & §20.10). For now, we supply a value of zero.

The remaining arguments are character strings which document the transformation function, mainly for the benefit of anyone who is unfortunate enough to encounter a reference to it in their data which they cannot interpret. As explained above (§20.3), you should try and avoid this, but accidents will happen, so you should always provide strings containing the following:

(1)
A short description of what the transformation function is for.
(2)
The name of the author.
(3)
Contact details, such as an e-mail or WWW address.

The idea is that anyone finding an IntraMap in their data, but lacking the necessary transformation function, should be able to contact the author and make a sensible enquiry in order to obtain it. If you expect many enquiries, you may like to set up a World Wide Web page and use that instead (in the example above, we use the WWW address of the relevant part of this document).

20.6 Creating an IntraMap

Once a transformation function has been registered, creating an IntraMap from it is simple:

AstIntraMap *intramap;

...

intramap = astIntraMap( "SqrTran", 2, 2, "" );

We simply use the astIntraMap constructor function and pass it the name of the transformation function to use. This name is the same (case sensitive) one that we associated with the function when we registered it using astIntraReg20.5).

You can, of course, register any number of transformation functions and select which one to use whenever you create an IntraMap. You can also create any number of independent IntraMaps using each transformation function. In this sense, each transformation function you register effectively creates a new “sub-class” of IntraMap, from which you can create Objects just like any other class. However, an error will occur if you attempt to use a transformation function that has not yet been registered.

The second and third arguments to astIntraMap are the numbers of input and output coordinates. These define the Nin and Nout attributes for the IntraMap that is created and they must match the corresponding numbers given when the transformation function was registered.

The final argument is the usual attribute initialisation string. You may set attribute values for an IntraMap in exactly the same way as for any other Mapping4.6, and also see §20.9).

20.7 Restricted Implementations of Transformation Functions

You may not always want to use both the forward and inverse transformations when you create an IntraMap, so it is possible to omit either from the underlying coordinate transformation function. Consider the following, for example:

void Poly3Tran( AstMapping *this, int npoint, int ncoord_in,
                const double *ptr_in[], int forward, int ncoord_out,
                double *ptr_out[] ) {
   double x;
   int point;

/* Forward transformation. */
   for ( point = 0; point < npoint; point++ ) {
      x = ptr_in[ 0 ][ point ];
      ptr_out[ 0 ][ point ] = ( x == AST__BAD ) ? AST__BAD :
         6.18 + x * ( 0.12 + x * ( -0.003 + x * 0.0000101 ) );
   }
}

This implements a 1-dimensional cubic polynomial transformation. Since this is somewhat awkward to invert, however, we have only implemented the forward transformation. When registering the function, this is indicated via the “flags” argument to astIntraReg, as follows:

void Poly3Tran( AstMapping *, int, int, const double *[], int, int, double *[] );

...

astIntraReg( "Poly3Tran", 1, 1, Poly3Tran, AST__NOINV,
             purpose, author, contact );

Here, the fifth argument has been set to the flag value AST__NOINV to indicate the lack of an inverse. If the forward transformation were absent, we would use AST__NOFOR instead. Flag values for this argument may be combined using a bitwise OR if necessary.

20.8 Variable Numbers of Coordinates

In our earlier examples, we have used a fixed number of input and output coordinates when registering a coordinate transformation function. It is not necessary to impose this restriction, however, if the transformation function can cope with a variable number of coordinates (as with the example in §20.4). We indicate the acceptability of a variable number when registering the transformation function by supplying the value AST__ANY for the number of input and/or output coordinates, as follows:

astIntraReg( "SqrTran", AST__ANY, AST__ANY, SqrTran, 0,
             purpose, author, contact );

The result is that an IntraMap may now be created with any number of input and output coordinates. For example:

AstIntraMap *intramap1, *intramap2;

...

intramap1 = astIntraMap( "SqrTran", 1, 1, "" );
intramap2 = astIntraMap( "SqrTran", 3, 3, "Invert=1" );

It is possible to fix either the number of input or output coordinates (by supplying an explicit number to astIntraReg), but more subtle restrictions on the number of coordinates, such as requiring that Nin and Nout be equal, are not supported. This means that:

intramap = astIntraMap( "SqrTran", 1, 2, "" );

will be accepted without error, although the transformation function cannot actually handle such a combination sensibly. If this is important, it would be worth adding a check within the transformation function itself, so that the error would be detected when it came to be used.

20.9 Adapting a Transformation Function to Individual IntraMaps

In the examples given so far, our coordinate transformation functions have not made use of the “this” pointer passed to them (which identifies the IntraMap whose transformation we are implementing). In practice, this will often be the case. However, the presence of the “this” pointer allows the transformation function to invoke any other AST function on the IntraMap, and this permits enquiries about its attributes. The transformation function’s behaviour can therefore be modified according to any attribute values which are set. This turns out to be a useful thing to do, so each IntraMap has a special IntraFlag attribute reserved for exactly this purpose.

Consider, for instance, the case where the transformation function has access to several alternative sets of internally-stored data which it may apply to perform its transformation. Rather than implement many different versions of the transformation function, you may switch between them by setting a value for the IntraFlag attribute when you create an instance of an IntraMap, for example:

intramap1 = astIntraMap( "MyTran", 2, 2, "IntraFlag=A" );
intramap2 = astIntraMap( "MyTran", 2, 2, "IntraFlag=B" );

The transformation function may then enquire the value of the IntraFlag attribute (e.g. using astGetC and passing it the “this” pointer) and use whichever dataset is required for that particular IntraMap.

This approach is particularly useful when the number of possible transformations is unbounded or not known in advance, in which case the IntraFlag attribute may be used to hold numerical values encoded as part of a character string (effectively using them as data for the IntraMap). It is also superior to the use of a global switch for communication (e.g. setting an index to select the “current” data before using the IntraMap), because it continues to work when several IntraMaps are embedded within a more complex compound Mapping, when you may have no control over the order in which they are used.

20.10 Simplifying IntraMaps

A notable disadvantage of IntraMaps is that they are “black boxes” as far as AST is concerned. This means that they have limited ability to participate in the simplification of compound Mappings performed, e.g., by astSimplify6.7), because AST cannot know how they interact with other Mappings. In reality, of course, they will often implement such specialised coordinate transformations that the simplification possibilities will be rather limited anyway.

One important simplification, however, is the ability of a Mapping to cancel with its own inverse to yield a unit Mapping (a UnitMap). This is important because Mappings are frequently used to relate a dataset to some external standard (a celestial coordinate system, for example). When inter-relating two similar datasets calibrated using the same standard, part of the Mapping often cancels, because it is applied first in one direction and then the other, effectively eliminating the reference to the standard. This is often a useful simplification and can lead to greater efficiency.

Many transformations have this property of cancelling with their own inverse, but not necessarily all. Consider the following transformation function, for example:

void MaxTran( AstMapping *this, int npoint, int ncoord_in,
              const double *ptr_in[], int forward, int ncoord_out,
              double *ptr_out[] ) {
   double hi, x;
   int coord, point;

/* Forward transformation. */
   if ( forward ) {
      for ( point = 0; point < npoint; point++ ) {
         hi = AST__BAD;
         for ( coord = 0; coord < ncoord_in; coord++ ) {
            x = ptr_in[ coord ][ point ];
            if ( x != AST__BAD ) {
               if ( x > hi || hi == AST__BAD ) hi = x;
            }
         }
         ptr_out[ 0 ][ point ] = hi;
      }

/* Inverse transformation. */
   } else {
      for ( coord = 0; coord < ncoord_out; coord++ ) {
         for ( point = 0; point < npoint; point++ ) {
            ptr_out[ coord ][ point ] = ptr_in[ 0 ][ point ];
         }
      }
   }
}

This function takes any number of input coordinates and returns a single output coordinate which is the maximum value of the input coordinates. Its inverse (actually a “pseudo-inverse”) sets all the input coordinates to the value of the output coordinate.33

If this function is applied in the forward direction and then in the inverse direction, it does not in general restore the original coordinate values. However, if applied in the inverse direction and then the forward direction, it does. Hence, replacing the sequence of operations with an equivalent UnitMap is possible in the latter case, but not in the former.

To distinguish these possibilities, two flag values are provided for use with astIntraReg to indicate what simplification (if any) is possible. For example, to register the above transformation function, we might use:

void MaxTran( AstMapping *, int, int, const double *[], int, int, double *[] );

...

astIntraReg( "MaxTran", AST__ANY, 1, MaxTran, AST__SIMPIF,
             purpose, author, contact );

Here, the flag value AST__SIMPIF supplied for the fifth argument indicates that simplification is possible if the transformation is applied in the inverse direction followed by the forward direction. To indicate the complementary case, the flag AST__SIMPFI would be used instead. If both simplifications are possible (as with the SqrTran function in §20.4), then we would use the bitwise OR of both values.

In practice, some judgement is usually necessary when deciding whether to allow simplification. For example, seen in one light our SqrTran function (§20.4) does not cancel with its own inverse, because squaring a coordinate value and then taking its square root can change the original value, if this was negative. Therefore, replacing this combination with a UnitMap will change the behaviour of a compound Mapping and should not be allowed. Seen in another light, however, where the coordinates being processed are intrinsically all positive, it is a permissible and probably useful simplification.

If such distinctions are ever important in practice, it is simple to register the same transformation function twice with different flag values (use a separate name for each) and then use whichever is appropriate when creating an IntraMap.

20.11 Writing and Reading IntraMaps

It is most important to realise that when you write an IntraMap to a Channel15.3), the transformation function which it uses is not stored with it. To do so is impossible, because the function has been compiled and loaded into memory ready for execution before AST gets to see it. However, AST does store the name associated with the transformation function and various details about the IntraMap itself.

This means that any program attempting to read the IntraMap (§15.4) cannot make use of it unless it also has independent access to the original transformation function. If it does not have access to this function, an error will occur at the point where the IntraMap is read and the associated error message will direct the user to the author of the transformation function for more information.

However, if the necessary transformation function is available, and has been registered before the read operation takes place, then AST is able to re-create the original IntraMap and will do so. Registration of the transformation function must, of course, use the same name (and, in fact, be identical in most particulars) as was used in the original program which wrote the data.

This means that a set of co-operating programs which all have access to the same set of transformation functions and register them in identical fashion (see §20.12 for how this can best be achieved) can freely exchange data that contain IntraMaps. The need to avoid exporting such data to unsuspecting third parties (§20.3) must, however, be re-iterated.

20.12 Managing Transformation Functions in Libraries

If you are developing a large suite of data reduction software, you may have a need to use IntraMaps at various points within it. Very probably this will occur in unrelated modules which are compiled separately and then stored in a library. Since the transformation functions required must be registered before they can be used, this makes it difficult to decide where to perform this registration, especially since any particular data reduction program may use an arbitrary subset of the modules in your library.

To assist with this problem, AST allows you to perform the same registration of a transformation function any number of times, so long as it is performed using an identical invocation of astIntraReg on each occasion (i.e. all of its arguments must be identical). This means you do not have to keep track of whether a particular function has already been registered but could, in fact, register it on each occasion immediately before it is required (wherever that may be). In order that all registrations are identical, however, it is recommended that you group them all together into a single function, perhaps as follows:

void MyTrans( void ) {

   ...

   astIntraReg( "MaxTran", AST__ANY, 1, MaxTran, AST__SIMPIF,
                purpose, author, contact );

   ...

   astIntraReg( "Poly3Tran", 1, 1, Poly3Tran, AST__NOINV,
                purpose, author, contact );

   ...

   astIntraReg( "SqrTran", 2, 2, SqrTran, 0,
                purpose, author, contact );
}

You can then simply invoke this function wherever necessary. It is, in fact, particularly important to register all relevant transformation functions in this way before you attempt to read an Object that might be (or contain) an IntraMap20.11). This is because you may not know in advance which of these transformation functions the IntraMap will use, so they must all be available in order to avoid an error.

33Remember that “ptr_in” identifies the original “output” coordinates when applying the inverse transformation and “ptr_out” identifies the original “input” coordinates.