D. M. Zarro (ADNET/GSFC)
CONTENTS
1.2 Putting data into an object
1.3 Getting data out of an object
2.1 Modifying object properties
2.3 How does inheritance work?
3. APPLYING OBJECTS TO SOLAR IMAGES
3.2 Image analysis with objects
1. INTRODUCTION
IDL objects were first introduced in version 5.0. The concept of object
oriented programming (OOP) is not new. It is the basis of many modern
languages such as C++ and Java. The idea behind OOP is that operations and data
go hand in hand. Why? Because if you are handed a piece of data without any way
of operating on it, then it would not be very useful to you. Furthermore, even if you
were given software to operate on the data, the data may still not be
useful without a set of rules to describe how the software interacts with it.
Consequently, the main thrust of OOP techniques is to keep operations and data
married together so that they understand each other. This concept is called
encapsulation. A valid question to ask is: what is wrong with
non-OOP programming techniques in which procedures (or functions) are written to
operate on data? For example, one can develop a reader to read a file, a plotter
to plot it, and a writer to save it, as in the following pseudocode:
IDL> read,file,data IDL> plot,data IDL> write,file,data
|
The short answer is that there is nothing wrong with
using non-OOP techniques. The long answer is that,
as data sets become complicated and operations more complex, simple
procedures are no longer simple. Procedures often become a series of
steps that are called repeatedly or they require enhancements to support
different datasets, which were not originally considered.
In the OOP world, these procedures are called methods.
These methods operate on data in exactly the same way as regular
non-OOP procedures but they are special in that they function
differently depending on the data in question. This concept is called
polymorphism and will be described in more detail in
section 2.3
The purpose of this tutorial is not to make the case for using OOP
techniques in IDL, but to illustrate situations in which such techniques are
more advantageous than non-OOP techniques. We start with demonstrating how
to create a very basic IDL data object, followed by how to interact and
manipulate it, and finally how to apply it to a real world example of reading
and displaying a FITS image. Although not a strong pre-requisite, we assume
that the reader is familiar with the use of IDL pointers and structures.
1.1 Creating an IDL object
An IDL object is simply a container in memory space. This container holds:
- data
- descriptions of the data
- procedures for operating on the data
The descriptions are called properties, and the procedures are
called methods. The interesting twist is that
both properties and methods can change as the data changes.
The first step in creating an object is deciding a name.
The name is important because it should signify something about the
the data. For example, if we are developing software to
sell different types of cars, such as Fords, Toyotas, and Hondas, then
the logical object name is probably car. This
generic name is
referred to as the object class. There is nothing special
or magic about the name, although it should be unique.
However, the name does become important later when we develop
different objects.
Since we are keeping things simple – and are already thinking in terms
of data – we name our object class data.
We define this class in a file named
data__define.pro that contains the following code:
function data::init return,1 end pro data__define void={data,ptr:ptr_new()} return end
|
So, what does the above all mean. Let’s take it in steps:
- CLASS filename: naming the file
data__define.pro is an IDL convention
for writing a class definition file. The class name has to be part
of the file name, followed by double underscore and the word
define.
If we were defining a car class, then the car class definition file
would be named car__define.pro. - INIT method: the first line in the file defines a method to initialize
the object. It is mandatory for all definition files.
The naming convention for this (and all methods) is class name, followed
by double colon and the method name (::init).
In this example, the init method doesn’t do very much. We will modify it
later to actually do something useful. For now, it simply returns
unity, which signifies that the object is successfully created. - CLASS definition: the second line defines an IDL structure in
which the data will be
contained. An IDL structure is a variable that can hold different data types such
as strings, floats, integers, and even other structures. In this example, it
contains a pointer field (called ptr) that we create with the
command:
ptr_new(). The latter function creates an address in memory where
future data will reside. The advantage of using a pointer is that we can
dynamically insert data even though we don’t know its size ahead of time.
In general, the class definition file is where the attributes
of the object are defined as structure tags.
These attributes are called properties.
In our data object example, we have a single property namely
ptr.
Having written and saved the above file into the current IDL
working directory, we create a data object as follows:
IDL> a=obj_new('data') IDL> help,a A OBJREF = ObjHeapVar37(DATA)
|
Creating the data object is called instantiation.
The IDL function call to obj_new(‘data’) looks for the
file data__define.pro, compiles it, and executes the
init and structure definition code that creates the data object.
Calling help on the output variable a shows
that it is an object of class type data.
In the language of OOP, the variable a is an
instance of the data class.
Currently, this object doesn’t
do very much other than waiting around for some data to
come along and some operations to perform on the data.
1.2 Putting data into an object
As currently defined, we cannot interact with the data object because
we have not defined any methods to communicate with it.
The next step is to write a method that allows us to insert data into
the object. We re-edit the file data__define.pro as follows:
function data::init ;-- allocate memory to pointer when initializing object self.ptr=ptr_new(/allocate) return,1 end ;---------------------------------------------------------------- pro data::set,value ;-- if data value exists, then insert into pointer location if n_elements(value) ne 0 then *(self.ptr)=value return end ;------------------------------------------------------------------ pro data__define void={data,ptr:ptr_new()} return end
|
Several things are going on that require explanation:
- SELF variable:
we have added the following line to data::init,self.ptr=ptr_new(/allocate)
What is self? When we are working inside an object, we reference the object using the variable name self.
The above line says:“Take the structure field named ptr,
which we have defined to be a pointer, and allocate new memory to it using
the IDL pointer function ptr_new(/allocate).”
We only need to do
this once when the data object is first created. - SET method: we name the method for inserting data
set. Any name will do, but following IDL convention
we precede it with the class name data::. This method
is a procedure that takes the variable data as a
command line argument. As is always good practice, we check that
the variable value exists before proceeding
by calling n_elements(value). If the latter returns a
non-zero value, then we insert the value into the pointer value
ptr. - Inserting data:
if you are confused by the syntax
*(self.ptr)=value, don’t worry. Here is the simple breakdown.
Recall that self is an internal reference to the
data object, which
is defined to be a structure with a tag (or field) named ptr.
We reference this pointer as self.ptr. The latter is not
an actual data value but an address to where data is located (or stored).
The asterisk symbol references the value of the data stored at the
pointer location. When the data object is first initialized, there is
no data at this location. The IDL statement
*(self.ptr)=value says:“Take my input data value and
insert (or copy) it to this pointer location.”
In the language of OOP, this action is called
setting the object property.
1.3 Getting data out of an object
In addition to inserting data, we need a method for extracting data
from the object. We shall call this method data::get,
and include it in the file data__define.pro as follows:
function data::init ;-- allocate memory to pointer when initializing object self.ptr=ptr_new(/allocate) return,1 end ;---------------------------------------------------------------- pro data::set,value ;-- if data value exists, then insert into pointer location if n_elements(value) ne 0 then *(self.ptr)=value return end ;---------------------------------------------------------------- function data::get,value ;-- if data value is stored in object pointer, then copy it out if n_elements(*(self.ptr)) ne 0 then value=*(self.ptr) return,value end ;------------------------------------------------------------------ pro data__define void={data,ptr:ptr_new()} return end
|
The get method differs from
set in that we have defined it to be a function.
The choice of function versus procedure is more of a matter
of convenience than convention.
The function first checks
for the existence of a data value at the pointer location
self.ptr. If data is present, then the value
*(self.ptr)
is copied to the output variable value.
If there is no data value, then an undefined value is returned.
1.4 Destroying an object
When finished with using an object, it is recommended that the
memory allocated to the object be released. All objects should therefore
have a method that will take care of cleaning up after themselves.
The IDL naming convention for this method is ::cleanup. In the case
of the data object, this cleanup method would involve freeing the pointer
property of any allocated data. To implement a cleanup method, we include
the following lines in the file data__define.pro:
function data::cleanup ;-- free memory allocated to pointer when destroying object ptr_free,self.ptr return end
|
The IDL procedure ptr_free flushes the pointer
variable self.ptr of any saved data and re-initializes it.
This method is not called directly. Instead it is called automatically
when the object is destroyed using the IDL obj_destroy procedure
as follows:
IDL> obj_destroy,a ;-- destroy object IDL> help,a A OBJREF = ObjHeapVar4 ;-- object is now null
|
2. WORKING WITH OBJECTS
Having defined a data class, we next demonstrate how it can be
used in common applications, and how it can be extended to
perform different functions.
2.1 Modifying object properties
Because we have used a pointer as its property,
the data object can accomodate any data type.
For example, let’s create a 2-dimensional float array
and insert it and extract it as follows:
IDL> image=findgen(512,512) IDL> a=obj_new('data') ;-- create object variable a IDL> a->set,image ;-- insert image IDL> image2=a->get() ;-- extract image IDL> help,image2 IMAGE2 FLOAT = Array[512, 512]
|
This example introduces the arrow syntax for calling methods.
The statement a->set,image says:
“Call the method named
set on the object variable named a, and pass
the argument variable named image.”
We apply the same syntax when calling the get method
except that we invoke it as a function and return the value into the
output variable image2.
Next, let’s try inserting and extracting a data structure such as the
system variable !d:
IDL> a->set,!d IDL> var=a->get() IDL> help,var,/st ** Structure !DEVICE, 17 tags, length=88: NAME STRING 'X' X_SIZE LONG 640 Y_SIZE LONG 512 X_VSIZE LONG 640 Y_VSIZE LONG 512 X_CH_SIZE LONG 6 Y_CH_SIZE LONG 12 X_PX_CM FLOAT 40.0000 Y_PX_CM FLOAT 40.0000 N_COLORS LONG 16777216 TABLE_SIZE LONG 256 FILL_DIST LONG 1 WINDOW LONG 0 UNIT LONG 0 FLAGS LONG 328124 ORIGIN LONG Array[2] ZOOM LONG Array[2]
|
In this example, we insert the variable !d into the
object and then retrieve it into the variable var.
Note that we only create the object variable once, and recycle it as
necessary. We can of course create as many data objects as we like
and store different data types accordingly. Just remember to give them
different variable names.
2.2 Adding object methods
As we have already seen, we can add new methods to an object by
editing its class definition file. We can make the data object
more useful by giving it the ability to read data
from a file and plot the data. In the following example, we open the file
data__define.pro and add two methods
that we conveniently call data::read and
data::plot:
pro data::read,file if n_elements(file) ne 1 then return ;-- at least one file name entered check=findfile(file,count=count) ;-- check if file exists if count ne 1 then return ;-- bail if not there image=fltarr(512,512) ;-- assume a 512x512 image file openr,lun,file,/get_lun ;-- open file readf,lun,image ;-- read image free_lun,lun ;-- close file self->set,image ;-- insert image into object return end ;--------------------------------------------------------------------- pro data::plot value=self->get() ;-- extract data value from object dsize=size(value) ;-- determine data dimensions if dsize[0] eq 2 then tvscl,congrid(value,512,512) ;-- if 2-dimensional, CONGRID and TVSCL it return end
|
The read method accepts a file name as its argument. As is good
practice, we check if the filename argument is entered and
use IDL’s findfile to test if the file actually
exists. We subsequently open the file, read the data,
and insert into the object using the
set method call: self->set,value. Note that because we are
referencing the object itself, we use the self variable
name for the object.
The plot method extracts the data value from
itself via the get method call: self->get().
It checks that the data is a 2-dimensional image using the
IDL size function, and
plots it with a call to IDL’s tvscl command
and congrid, which expands the image to a 512×512 array size.
To demonstrate this sequence of steps,
let’s create an image file image.dat in the current directory and
deploy the data object as follows:
IDL> image=findgen(512,512) ;-- create 512x512 image array IDL> file='image.dat' IDL> openw,lun,file,/get_lun ;-- write image to file IDL> printf,lun,image IDL> free_lun,lun IDL> .run data__define ;-- recompile class definition file IDL> a->read,file ;-- read image and display it IDL> a->plot
|
The call to the plot method produces the following simple image:
Note that it is not necessary to reinitialize the data object since
we are using the same object variable to store the image data.
However, it is necessary to recompile the class definition file
since we have added new methods to it.
2.3. How does inheritance work?
Looking at the last two lines in the previous example, it doesn’t appear from
our data object example that we have advanced very far using OOP techniques.
In particular, compared to the opening example of reading and plotting
data using non-OOP procedures, it seems that a lot of effort was invested in
writing object methods that essentially reproduce the same functionality as
conventional procedure calls.
The real power of OOP techniques comes not in what we have just
done, but in what we are about to do. The data object that we have created
is a building block that can be used to develop objects that
perform more complicated functions. Consider the following problem:
“I have an image in a FITS file that I would like to read and display. I would
also like to remember the name of the FITS file so that I can track
it.”
The data object that we have created provides a convenient storage facility
for FITS image data, but it lacks the required FITS file reader
and it doesn’t have a way of remembering the file name. We could
of course re-edit the data class definition file to add this functionality,
but that would involve much more work.
The OOP solution to the problem is to define a new class that somehow
inherits the functionality of the data class. The following is how to do it.
We define a new class called fits by creating
a file named fits__define.pro, which contains the
lines:
pro fits::read,file if n_elements(file) ne 1 then return ;-- at least one file name entered check=findfile(file,count=count) ;-- check that file exists if count eq 0 then return image=mrdfits(file) ;-- call Astronomy library FITS reader self->set,image ;-- insert image data into property self.filename=file ;-- save filename in property return end ;------------------------------------------------------------------------- pro fits__define void={fits,filename:'', inherits data} ;-- inherit from data class return end
|
Several new concepts are happening here. Let’s start
from the bottom up.
- FITS__DEFINE: the procedure that defines our fits data
structure looks very different from the original data structure in
data__define.pro. It appears to be missing
the data pointer property. Actually, the latter is
is not missing. In OOP language, it is inherited from the
data class via the statement inherits data.
In addition
to inheriting data’s property, the fits class inherits all of its
methods: init, cleanup, set, get, read, and plot.
Hence, inheritance has saved us from writing a lot of extra code.
Note that the fits data structure contains a new property called
filename, which is a string data type. This
property will be used to store the FITS file name. - FITS::READ: object inheritance
imports data’s simple read method
data::read, which clearly will not work on a FITS file.
To overcome this problem, we write a new method that uses the
Astronomy library routine
mrdfits.pro
to read
the FITS file and insert the resulting image data into the object pointer
by using the inherited set method.
The fits read method thus overrides data’s
read method.
This new method also saves the FITS filename as a property of the
fits object once the data is saved.
In OOP language, the fits class is
a derived class of data,
and a fits object is referred to as child of
the parent data object.
The following example demonstrates how to use the fits class to create
an object to read a Big Bear H-alpha image
contained in the file
bbso_halph_fl_20040310_173531.fts:
IDL> file='bbso_halph_fl_20040310_173531.fts' IDL> f=obj_new('fits') ;-- create object IDL> help,f F OBJREF = ObjHeapVar37(FITS) IDL> f->read,file ;-- read file IDL> f->plot ;-- plot data IDL> image=f->get() ;-- extract image IDL> help,image IMAGE INT = Array[512, 512]
|
We create a fits object using
the obj_new function, and feed it the FITS file name.
After
reading, we plot the image, and
extract the image data using the get method.
As defined, the get method inherited from the data class
only allows us to extract the data value from the property pointer. However,
we would also like to extract the filename that is associated with the
data, which is also a property. To include this functionality, we
override the get method in data__define.pro
with a new get method in fits__define.pro as follows:
function fits::get,filename filename=self.filename ;-- copy filename in variable image=self->data::get() ;-- call DATA's GET method to return data return,image end
|
The above lines illustrate the simplicity and elegance of inheritance.
We have added a new output argument filename in which we
return the string value of the filename, which is saved in
the property self.filename. Since we also wish to
return the data value, we include a call to the get method that we have
already defined for the data class. There is no need to rewrite the latter.
Hence, in the last example, we can execute the following:
IDL> data=f->get(filename) IDL> help,data,filename DATA INT = Array[512, 512] FILENAME STRING = 'bbso_halph_fl_20040310_173531.fts'
|
Note also that the fits object methods (read and
get)
retain the same names as their parent data method names. The difference
is in their behaviors, which depends upon which data type is being operated
on. This ability to behave differently depending upon class (or data type) is called
polymorphism.
3. APPLYING OBJECTS TO SOLAR IMAGES
The example classes in this tutorial demonstrate how
objects can be
designed, created, and used.
If interested in experimenting with these
classes, you can download complete definition files via the following links:
and
Although useful for illustrative purposes,
these classes are too simplistic for
handling more complicated solar datasets.
For example, not all solar datasets conform
to the FITS format standard. Variations
in the use of header keyword names and values
often require the use of special readers.
Moreover, different datasets usually
require the application of instrument-specific processing algorithms
inorder to be useful.
This dependence of operations upon the properties of the dataset
naturally lends itself to the use of objects as a tool for analyzing
solar data.
3.1 Using MAP objects
IDL maps are described in
maps.html.
An IDL map is a structure data type that
stores data and associated identifying information. A map
structure is not a true object since it does not contain methods.
Nevertheless, it is useful
for displaying and, in particular, overlaying solar images from
different instruments.
The following example illustrates how to
to read, process, and display the quicklook
SOHO/EIT file
efr20040309.072550
using a map and procedures available in the IDL
SolarSoft library:
IDL> files='efr20040309.072550' IDL> read_eit,files,index,data ;-- read EIT QL file IDL> eit_prep,index,data=data,outindex,outdata ;-- prep data IDL> index2map,outindex,outdata,emap ;-- create map IDL> eit_colors,195 ;-- load 195 color table IDL> plot_map,emap,/log,grid=20,/limb ;-- plot map
|
This example highlights several points. Even though we
are primarily interested in
reading and plotting an EIT image, we have to
know and perform several steps:
- read the EIT FITS file using the special reader
read_eit. - process the EIT image
using the procedure eit_prep, which performs dark current
subtraction and flatfielding. - convert the processed image to a map structure
emap via the procedure
index2map. - plot the map structure emap
using the procedure plot_map, which is
called with the keywords /limb, /log, and grid=20
inorder to display a log-scaled image with a heliographic grid and limb.
In addition, we have to remember to pass the variables: index, data
as arguments to these procedures, and know that our EIT dataset is
a 195 A image that has a custom color table (which we load with the procedure
eit_colors). That’s a lot to remember each time. It would be nice
if we could somehow encapsulate the above procedures and data into an
object and only have to remember as little as necessary to get the job done.
What we need is a map class that will allow us to define
map objects to store data such as EIT images and
provide methods to manipulate them. Such a class already exists. It is defined
in the file
map__define.pro. The map class is analogous to
our example data class except that it uses a map
structure to store data and its corresponding properties (such as pointing).
Now consider the following OOP approach to the same example:
IDL> file='efr20040309.072550' IDL> eobj=obj_new('eit') ;-- create an EIT object IDL> eobj->read,file ;-- read EIT file IDL> eobj->plot ;-- plot EIT image
|
The above example produces exactly the same plot output as the conventional
example, but what is different? Let’s take it in steps:
- We have created an object variable eobj by
calling the object creation function obj_new(‘eit’)
with the class name eit. But where did this class
come from? The answer is that we have already created a class
definition file
eit__define.pro in which we have
incorporated the EIT data as a property.
The eit class definition file is slightly more complicated
than the fits and data classes defined earlier.
The eit class inherits from a slightly more complicated
fits class that is defined in
fits__define.pro,
which in turn inherits from the map class.
The map class
works as a storage device in much the same way as the simple
data class that we defined
in our earlier example.
This inheritance relationship is illustrated in the diagram below.
- After creating the eit object, we read the
EIT file using the read method. This method
is nothing other than the read_eit procedure, which we have incorporated
into eit__define.pro. This is polymorphism
at work. Because our object is aware that it belongs to the
eit class, its read method correctly calls the corresponding
eit reader, followed by eit_prep to process the image.
Hence, the EIT image is automatically
read, processed, and saved into the eit object. - With the EIT image in memory, we display it
using the plot method. This method is the
plot_map procedure that we have already incorporated into
map__define.pro and, hence, is inherited
by the eit object. We have also modified the plot method
slightly to include a call to eit_colors in order
to load the corresponding image color table.
In summary, the object variable eobj object is a map object
since the eit class is derived from a map class.
The details of the relationship between
eit and map classes are not overly important
to the average user who is interested in performing basic data analysis.
The main point is that, by using an eit map object,
the overhead of remembering
several instrument-specific procedure names is significantly reduced.
3.2 Image analysis with objects
The eit class has a get method that
provides access to the EIT image data and map structure as
follows:
IDL> edata=eobj->get(/data) IDL> help,edata EDATA INT = Array[1024, 1024] IDL> emap=eobj->get(/map) IDL> help,/st,emap ** Structure <40e0fb08>, 13 tags, length=2097256, refs=2: DATA INT Array[1024, 1024] XC FLOAT -8.15294 YC FLOAT 21.0663 DX FLOAT 2.63500 DY FLOAT 2.63500 TIME STRING ' 9-Mar-2004 07:24:58.402' ID STRING ' Rocket Science EIT 195' ROLL_ANGLE FLOAT 0.00000 ROLL_CENTER FLOAT Array[2] DUR FLOAT 12.5970 XUNITS STRING 'arcsecs' YUNITS STRING 'arcsecs' SOHO BYTE 1
|
The pointing information contained within a map structure allows us to
analyze different images regardless of the image source. By making a
map structure a property of a map object, we
simplify the steps involved in performing typical image processing operations.
We will conclude this tutorial by demonstrating three such operations:
rotating an image; correcting for differential solar rotation; and overlaying
images.
- Rotation: to rotate (or roll) an image clockwise about its center,
we pass the angle of rotation as an argument to the method rotate.IDL> robj=eobj->rotate(45) IDL> robj->plot
In this example, we rotate the EIT image contained in
the eit map object eobj by 45 degrees to create
a new map object robj which produces the image:
- Solar differential rotation: to differentially rotate an image we pass the
time interval over which to rotate
as an argument to the method drotate.IDL> dobj=eobj->drotate(5,/days) IDL> dobj->plot
In this example, we solar rotate the EIT image
forward in time by 5 days to produce
the image:
- Overlaying images: to overlay two images, we first plot a base image
using the plot method and overplot a second image
as a contour using plot
with the keyword /over.
We demonstrate this operation by overlaying
a SOHO/MDI image on an EIT
image. But
first we need to create an mdi map object.
We have already defined an mdi class by writing a
definition file
mdi__define.pro.
As with the eit class, the mdi class inherits
from a fits class (which inherits from a map class).
Because of this common inheritance, an mdi object has the same
plot method as an eit object.
This class allows us to create
an mdi object to read and display a high-resolution MDI image
in the file
mdi_maglc_re_20040310_2102.fts:IDL> file='mdi_maglc_re_20040310_2102.fts' IDL> mobj=obj_new('mdi') ;-- create an MDI object IDL> mobj->read,file ;-- read MDI file IDL> mobj->plot ;-- plot MDI image
SOHO/MDI
Note how the steps for reading and plotting the MDI
image are identical to those for EIT. The internal details of
how these steps are performed are handled by the
corresponding methods. Having created eit and mdi
objects, the final step of overlaying their corresponding images
is reduced to the following single line:IDL> eobj->plot,/over,/drotate ;-- overlay EIT image on previous MDI image ; (correcting for solar differential rotation)
EIT/MDI overlay
In this example, we used the /drotate keyword to differentially
rotate the EIT image contours to the same time as the
base MDI image. - Extracting a sub-field: the
extract method extracts a sub-field graphically
or via keywords.IDL> mobj=obj_new('mdi') IDL> mobj->read,'mdi_maglc_fd_20040522_0005.fts.gz' IDL> cobj=mobj->extract() IDL> cobj=mobj->extract(xrange=[-500,500],yrange=[-500,500])
In this example, we read an MDI full-disk image in the file
mdi_maglc_fd_20040522_0005.fts.gz, and call extract
without any keywords. A box-cursor is used to select a sub-field.
Alternatively, the keywords: xrange and yrange
can be used to specify the sub-field.
I see that at the end of your set methods, you have an empty return (i.e. you just write return). When I try this, I get the error ‘Return statement in functions must have 1 value.’ when I compile. Any ideas why?