Contents

2.10 TRIG package

The dimensioned numbers we have developed can be put to good use in a math package I call TRIG. The TRIG package specification shown in Listing 7 gives your Ada programs new levels of reliability and transportability.

It is more reliable because it is impossible to provide the wrong argument to a trigonometric function. I'm sure any reader who has written programs involving Sines and Cosines has, at least once, tried to take the sine of an angle expressed in degrees using a function expecting an input in radians, or vice-versa. An even more common mistake is multiplying by PI_OVER_180 when the angle should have been divided by that factor, or vice-versa. Those days are gone forever if you use the TRIG package.

2.10.1 Type Naming Convention

The TRIG package creates two data types, Deg and Rad, which you can use for angular variables. I could have called these data types Degrees and Radians, but that violates a simple convention I use when deriving dimensional units. Whenever I derive a dimensional unit from INTEGER_UNITS, I spell the units out completely. If the dimensional unit is derived from FLOAT_UNITS, then I use an abbreviation. Therefore, I know immediately that type Feet is an integer data type, but type Ft uses real numbers. This means I can tell its range and precision without having to search through the code to find out if the unit was derived from INTEGER_UNITS.Units or FLOAT_UNITS.Units. The convention is easy to remember because both abbreviations and real numbers have dots in them. Since the TRIG package uses real numbers, rather than integers, I used Deg and Rad for the type names.

2.10.2 Overloaded Function Names

Notice there are two overloaded versions of Sin. That is, there are two different functions with the same name. One Sin function works for type Rad and the other works for type Deg. When you declare an angular object you assign its dimensions by making it type Rad or Deg. When you call for the Sine of that angle, Ada will automatically select the correct Sin routine from the TRIG package based on the data type. When you take an inverse function, the TRIG package will automatically return degrees or radians, whichever is correct. If ANGLE_1 and ANGLE_2 are expressed in different units, you can always say ANGLE_1 := TRIG.Convert_Units(ANGLE_2); and you will get the correct result, no matter which one is in degrees and which one is in radians. (If ANGLE_1 and ANGLE_2 already are the same kind of units, both degrees or both radians, Ada will tell you so at compiler time with a fatal error message.) You won't need any more PI_OVER_180 constants cluttering up your program.

2.10.3 Portability

The TRIG package also makes your programs more portable because it can be used as a standard math package. You were probably surprised when you discovered Ada has no predefined math package. That may seem strange for a military programming language, but it makes good sense. Since Ada is strongly typed you would have to have separate math function for every real data type. Since the number of distinct real data types is unbounded, that would be a lot of math routines. A standard generic math package could be instantiated for each real data type (the way FLOAT_IO is), but that doesn't completely solve the problem. Some machines have an optional math coprocessor. Ada would have to have multiple versions of the math library, some of which would use the coprocessor and others wouldn't. Furthermore, some applications may need immediate answers good to three decimal places, while other applications need higher precision and have all the time in the world to compute the answer. If Ada had a built-in standard math library, a trade-off would have to be made, and the resulting math routines would be good for some applications but not others.

These are all good reasons for leaving the math routines out of the Ada language. I wouldn't have it any other way. Compiler vendors, however, wisely recognized that most of us programmers use math functions often in our application programs. Furthermore, we want to solve the problems we are being paid to solve, not reinvent the Sine function. We would abuse their sales representatives if they didn't include a math library as part of the programming environment, so most compilers today come with a math library. You will probably find a package called something like MATH_LIB or GENERAL_MATH in the system library.

Math libraries often cause portability problems. They generally have different names for the library as well as the functions. For example, the square root function might be called Sqr or Sqrt. Some of math libraries are generics and some have already been instantiated. Some math libraries have two functions, Sin and Sind, for taking sines in radians and degrees. Other libraries only have a radian sine function. Suppose your program used Sind to find the sine of angles expressed in degrees, and you transported it to another system that used Sind for a double-precision sine in radians. How long would it take you to find that bug?

I run into these problems a lot because I use so many different Ada compilers. I solved the problem by using the TRIG package as a shell over the underlying math library. Once I get the TRIG package ported to a new system, then all my other programs can be transported without any math modifications.

The TRIG package specification remains the same on every system, but the body has to be specially tailored for each system. Listing 8 shows the TRIG package body for the DEC compiler. It simply calls the corresponding VAX/VMS math library routine for each function. The TRIG package body for the Meridian compiler in Listing 9 uses a slightly different approach, just for the sake of illustrating a different way of doing it. (Mathematicians know the method I used in the Meridian body is inferior, and we will talk about that much later when I show you how the routine was tested.) I pretended that there wasn't any Cos or Tan function available and derived them from the Sin function. (If you are using an embedded computer, you might just have a sine lookup table, or a coprocessor that only computes sines.) In the fall of 1987, Alsys did not officially provide a math library. The Version 3.2 distribution disk contained a math library contributed by a customer. Listing 10 uses that unofficial Alsys math library. No matter what the package body implementation is, the package specification doesn't change, and that makes all the programs that use the TRIG package portable.

2.10.4 Reciprocal Functions

When I wrote this package I had to decide what to put in, and what to leave out. I chose to leave out the reciprocal functions (secant, cosecant, and cotangent). I can't remember the last time I used one of these, so it didn't seem worth the memory space to put them in. If I had put them in, they probably would have just returned 1/Sin, 1/Cos, or 1/Tan. If the application program used the Secant in an equation like X := Y*Secant(THETA);, it would compute X := Y*(1/Sin(THETA); which involves an extra operation and an extra function call (unless the optimizer takes them out). It would be better to simply write X := Y/Sin(THETA);. (Dividing by Secant(THETA) would be even more foolish.) I decided it was better to leave out the reciprocal functions than be tempted to use them.

This TRIG package may not exactly fit your needs, but it is built in such a way that you can modify it to do exactly what you want. You can derived Deg and Rad from any floating point type you desire. You can change the body to use any desired algorithm. Since it isn't part of the language you are free to do whatever you want.

2.10.5 Special Cases

I made some decisions concerning special cases, and those decisions might not be appropriate for your application. Since the TRIG body is given in source code, you are free to change it however you like.

The first special case is the tangent of 90 degrees. The theoretical value is infinite. DEC Ada raises FLOAT_MATH_LIB.FLOOVEMAT. I could have handled this by simply raising NUMERIC_ERROR in its place and let the application program figure out how to handle it, but instead I decided to return a very big number. The problem is, how big is "very big?" I decided that since I had previously decided limits for DIM_FLOAT, I might as well use the same maximum and minimum here. I return DIM_FLOAT.Last for the Tangent of 90 degrees, and DIM_FLOAT.First for the Tangent of 270 degrees. If that's not appropriate for your application, feel free to change it.

The second special case is the two argument arctangent where both arguments are zero. What is the bearing from the origin to the origin? It is undefined. You could say that it is a stupid question that doesn't deserved to be answered, but consider this: People who are trying to drop bombs on you generally try to fly right over your head, and sometimes they succeed. When that happens the elevation is 90 degrees and the azimuth is Atan(0.0,0.0). It could be an important moment in your life, and you want the right answer. If you are flying an airplane and pull a vertical loop, there are moments when you are flying straight up or straight down, so your heading is Atan(0.0,0.0).

My first approach to the problem was to define Atan(0.0,0.0) to be 0.0 because I wanted to avoid raising an exception. That drove an aircraft simulation nuts when it tried to simulate a loop if the aircraft wasn't flying due north or due south. I decided it was better to raise the INVALID_ARGUMENT exception and let the application program handle it. (The application program handled it by using the previous heading, and that worked fine.) Your situation may require a different solution, and you are free to do whatever you like with the source code.

2.10.6 Which Way is Up?

The four quadrant arctangent function is a common cause of programming errors because it is easy to confuse the arguments. To help you appreciate the problem, let me try to confuse you.

Suppose a position is expressed in polar coordinates. The distance from the origin is 10 and the angle is 30 degrees. If we need to convert this to cartesian coordinates, we compute X = 10 * Cos(30) = 8.66. Then we find Y = 10 * Sin(30) = 5.00. No problem. But suppose we want to convert that point back to polar coordinates. Do we use Atan(8.66,5.00) or Atan(5.00,8.66)?

The common convention is that Atan(A1, A2) returns the arctangent of A1/A2, so the first argument should be the Y component and the second argument should be the X component. So, if the X coordinate is 8.66 and the Y coordinate is 5.00, the correct angle is found by computing Atan(5.00,8.66).

Now, what is the bearing of a target 8.66 miles East and 5.00 miles North? You think it is Atan(5.00,8.66)? Think again! Atan(5.00,8.66) yields 30 degrees, but the correct answer is 60 degrees.

Mathematicians compute the angle of a point in polar coordinates counterclockwise from the horizontal axis, as shown in Figure 12. Anyone who has ever used a compass (especially pilots and radar operators) knows 0 degrees is North and 90 degrees is East. Angles are measured clockwise from the vertical axis, as shown in Figure 13. Therefore, the bearing to a target 8.66 miles East and 5.00 miles North is given by Atan(8.66,5.00).

That why I called the arguments to the TRIG.Atan function EAST_OR_Y and NORTH_OR_X. I makes it easier for me to remember the correct parameter associations.

Things get even worse in three dimensions because there are so many right-handed coordinate systems. Mathematicians like to use X (right), Y (ahead), and Z (up) vectors. Airborne systems like to use NORTH, EAST, DOWN. Ground-based systems like to use EAST, NORTH, UP. Body-referenced coordinates can be X',Y',Z' or X",Y",Z" where the major body axis could be aligned with any of those vectors, depending on the programmer's whim.

Fortunately Ada allows you to define enumeration types that specify body directions in meaningful terms. If you want to use an array to represent a three dimensional velocity in body coordinates you can do this:

type Body_Coordinates is (BOW, STARBOARD, KEEL); type Velocities is array(Body_Coordinates) of Feet_per_second; MISSILE_SPEED : Velocities; MISSILE_SPEED(BOW) := (some_value); You don't have to use arrays to represent three dimensional quantities. For example, you could use a record to represent the attitude of the missile like this: type Attitudes is record PITCH, YAW, ROLL : Degrees; end record; MISSILE_ORIENTATION : Attitudes; MISSILE_ORIENTATION.PITCH := (some_value); In any case, you can avoid confusion by avoiding the use of the ambiguous X, Y, Z labels for each axis.
Contents | Next ...