Sections in this page
A continual refrain in Jean Meeus' excellent series of books on astronomical computation is the risk of errors due to the fact that angles in astronomical formulae are invariably given in degrees but programming languages require radians to be input to trigonometrical functions. It is vital to keep track of this in programs and ensure that conversions between units happen correctly. There is nothing to tell from a numerical value by itself what the units are supposed to be.
A first defence against radian/degree confusion is to include the unit names in the names of variables. Eg,
latitudeDegrees = 54.9; raHrs = 2.3;
That helps to keep track of what the units are supposed to be at any point in a calculation but it does not ensure that they are in fact correct.
So for taking a cosine you might write
cosLat = cos (toRadians (latitudeDegrees));
and the "Degrees" in the variable name helps you to remember to call the function to convert it to radians first. But it does not guarantee that you will do that. Importantly, the compiler cannot detect such an error because whether the toRadians () function is called or not, the argument to cos () is a number and that's all the compiler requires.
No matter how careful you are this approach still leaves room for errors, as I know only too well.
In object-oriented languages there is a better remedy: make angles into objects rather than merely numbers. The class definition for Angle can then contain trigonometrical functions that know what units the angle is in and therefore can always do the right thing:
Angle latitude = new Angle (45.0, DEGREES); cosLat = latitude.cos ();
Now the conversion to, and use of, appropriate units is hidden inside the class/object. We don't need to know whether internally the value is held in degrees or radians (though for performance in intensive geometrical calculations it probably ought to be radians). Provided that the class has been thoroughly unit-tested the kind of mistake we were discussing simply cannot occur. (In the example DEGREES is meant to be a constant - in Java it would be an enum.)
Astronomical quantities such as right ascension (RA) and declination (Dec) are angles and so they too should be declared as classes: subclasses of Angle. Dec is simply an Angle that is restricted to the range -90..90 degrees (inclusive). RA introduces the possibility that an angle may be measured in hours and so the units list in Angle should at least contain DEGREES, RADIANS and HOURS. There are other possibilities too, in some applications.
In the object-oriented language Java (and as far as I know in all commonly used programming languages) this idea has NOT been adopted in the standard library. The Java class called Math contains static trigonometrical functions requiring angles in radians as their arguments. I think a much better basis for astronomical computing should declare Angle as outlined above. Angle may use Math internally but any further geometrical programming should only use objects of class Angle. The application programmer should never directly use Math for trigonometry. Inverse functions should be defined as functions (in Java, static methods) creating new Angle objects:
Angle theta = Angle.atan (y / x);
Or, better because the range of the result can be determined:
Angle theta = Angle.atan2 (y, x);
A further benefit of using Angle as a class, and therefore as a data type, is that where functions take several parameters which would otherwise just be a list of numbers, it then becomes clear that particular ones are angles. That again reduces errors, of the kind where the parameters are written in the wrong order.
For example, we might define a class to represent positions in the sky. Suppose we call it SkyPoint. Now which of the following constructors would be preferable?
SkyPoint pt = new SkyPoint (1.23, 4.56, 2000.0);
SkyPoint pt = new SkyPoint (new RA (1.23, HOURS), new Dec (4.56, DEGREES), new Epoch ('J', 2000.0));
In the first example we merely have 3 floating point parameters and it would be easy not only to get the units wrong but also to get them in the wrong order. In the second example we make use of the strong type-checking of a language such as Java to eliminate both kinds of error. Or rather, if we should make any such mistake the compiler can detect it, tell us about it, and prevent the application from running.
After all, the worst kind of error is not one which causes the system to crash but one which results in wrong answers being shown without any warning.
We can go on from there to build a useful library (often called an API, or Application Programming Interface) for doing spherical astronomy. I have already implied that Epoch would be a useful class and there are several others. For example, SkyVector would represent the great circle arc between two SkyPoints. We could create a SkyVector from two SkyPoints (pt1 and pt2) like this:
SkyVector vec = pt1.calculateSeparation (pt2);
A SkyVector would contain two Angle objects: the separation (as seen from the centre of the sphere) and the position angle (PA, on the sphere, anticlockwise from north). This is how astronomers measure the relative positions of pairs of objects, such as double stars.
I have indeed done all of that in the API for GRIP. You will find the following public classes there:
- net.grelf.Angle.Units (an enum)
- net.grelf.astro.RA extends Angle
- net.grelf.astro.Dec extends Angle
Yes, performance is affected by creating numerous objects and removing them after use. But I do use these object techniques in my application Hopper to draw real-time star charts on my laptop and it works fine with no perceptible delays when I scroll and pan the charts at my telescope. Such a chart involves thousands of angles at any one time, and SkyVectors to plot the positions of stars that are within visible radius of the centre of the screen.
On balance the benefit from the control of mistakes far outweighs the performance detriment.