Degenerate Conic

Algorithms • Modern Fortran Programming • Orbital Mechanics

Jun 17, 2022

Quadrature Quandary

slatec

In today's episode of "What where the Fortran dudes in the 1980s thinking?": consider the following code, which can be found in various forms in the two SLATEC quadrature routines dgaus8 and dqnc79:

K = I1MACH(14)
ANIB = D1MACH(5)*K/0.30102000D0
NBITS = ANIB

Note that the "mach" routines (more on that below) return:

  • I1MACH(10) = B, the base. This is equal to 2 on modern hardware.
  • I1MACH(14) = T, the number of base-B digits for double precision. This will be 53 on modern hardware.
  • D1MACH(5) = LOG10(B), the common logarithm of the base.

First off, consider the 0.30102000D0 magic number. Is this supposed to be log10(2.0d0) (which is 0.30102999566398120)? Is this a 40 year old typo? It looks like they ended it with 2000 when they meant to type 3000? In single precision, log10(2.0) is 0.30103001. Was it just rounded wrong and then somebody added a D0 to it at some point when they converted the routine to double precision? Or is there some deep reason to make this value very slightly less than log10(2.0)?

But it turns out, there is a reason!

If you look at the I1MACH routine from SLATEC you will see constants for computer hardware from the dawn of time (well, at least back to 1975. It was last modified in 1993). The idea was you were supposed to uncomment the one for the hardware you were running on. Remember how I said IMACH(10) was equal to 2? Well, that was not always true. For example, there was something called a Data General Eclipse S/200, where IMACH(10) = 16 and IMACH(14) = 14. Who knew?

So, if we compare two ways to do this (in both single and double precision):

! slatec way:
 anib_real32  = log10(real(b,real32))*k/0.30102000_real32;   nbits_real32  = anib_real32
 anib_real64  = log10(real(b,real64))*k/0.30102000_real64;   nbits_real64  = anib_real64

! naive way:
 anib_real32_alt  = log10(real(b,real32))*k/log10(2.0_real32);   nbits_real32_alt  = anib_real32_alt
 anib_real64_alt  = log10(real(b,real64))*k/log10(2.0_real64);   nbits_real64_alt  = anib_real64_alt

! note that k = imach(11) for real32, and imach14 for real64

We see that, for the following architectures from the i1mach routine, the naive way will give the wrong answer:

  • BURROUGHS 5700 SYSTEM (real32: 38 instead of 39, and real64: 77 instead of 78)
  • BURROUGHS 6700/7700 SYSTEMS (real32: 38 instead of 39, and real64: 77 instead of 78)

The reason is because of numerical issues and the fact that the real to integer assignment will round down. For example, if the result is 38.99999999, then that will be rounded down to 38. So, the original programmer divided by a value slightly less than log10(2) so the rounding would come out right. Now, why didn't they just use a rounding function like ANINT? Apparently, that was only added in Fortran 77. Work on SLATEC began in 1977, so likely they started with Fortran 66 compilers. And so this bit of code was never changed and we are still using it 40 years later.

Numeric inquiry functions

In spite of most or all of the interesting hardware options of the distant past no longer existing, Fortran is ready for them, just in case they come back! The standard includes a set of what are called numeric inquiry functions that can be used to get these values for different real and integer kinds:

Name Description
BIT_SIZE The number of bits in an integer type
DIGITS The number of significant digits in an integer or real type
EPSILON Smallest number that can be represented by a real type
HUGE Largest number that can be represented by a real type
MAXEXPONENT Maximum exponent of a real type
MINEXPONENT Minimum exponent of a real type
PRECISION Decimal precision of a real type
RADIX Base of the model for an integer or real type
RANGE Decimal exponent range of an integer or real type
TINY Smallest positive number that can be represented by a real type

So, the D1MACH and I1MACH routines are totally unnecessary nowadays. Modern versions that are simply wrappers to the intrinsic functions can be found here, which may be useful for backward compatibility purposes when using old codes. I tend to just replace calls to these functions with calls to the intrinsic ones when I have need to modernize old code.

Updating the old code

In modern times, we can replace the mysterious NBITS code with the following (maybe) slightly less mysterious version:

 integer,parameter :: nbits = anint(log10(real(radix(1.0_wp),wp))*digits(1.0_wp)/log10(2.0_wp))

 ! on modern hardware, nbits is: 24 for real32
 !                               53 for real64
 !                              113 for real128

Where:

  • wp is the real precision we want (e.g., real32 for single precision, real64 for double precision, and real128 for quad precision).
  • radix(1.0_wp) is equivalent to the old I1MACH(10)
  • digits(1.0_wp) is equivalent ot the old I1MACH(14)

Thus we don't need the "magic" number anymore, so it's time to retire it. I plan to make this change in the refactored Quadpack library, where these routines are being cleaned up and modernized in order to serve for another 40 years. Note that, for modern systems, this equation reduces to just digits(1.0_wp), but I will keep the full equation, just in case. It will even work on the Burroughs, just in case those come back in style.

quadpack

Final thought

If you search the web for "0.30102000" you can find various translations of these old quadrature routines. My favorite is this Java one:

//  K = I1MACH(14) --- SLATEC's i1mach(14) is the number of base 2
//  digits for double precision on the machine in question.  For
//  IEEE 754 double precision numbers this is 53.

      k = 53;

//  r1mach(5) is the log base 10 of 2 = .3010299957

//      anib = .3010299957*k/0.30102000E0;

//      nbits = (int)(anib);

      nbits = 53;

Great job guys!

Acknowlegments

Special thanks to Fortran-lang Discourse users @urbanjost, @mecej4, and @RonShepard in this post for helping me get to the bottom of the mysteries of this code.

See also