Integer root of floating point value

There was this interesting discussion on the Python list:

https://groups.google.com/g/comp.lang.python/c/N--YDk7lhkE

Basically, something like a**(1.0/3.0) lose precision when a is large because 1.0/3.0 doesn’t have a finite representation in floating point.

Now, Python has implemented a cbrt function that doesn’t incur in the loosing of the precision, and have been suggested to implement a function like root(a, n) that may calculating the n-root without this problem.

Going back to Fortran,
I’m suggesting to implement in Fortran a new intrinsic that we may call powerroot(value, m, n) that may calculate value**(real(m)/real(n)) without loosing precision do to incorrectness of real(m)/real(n) and may avoid overflows.

I’m sorry I don’t know which algorithm one may use, so it is just an half proposal. :slight_smile:

Hi, this is why to compute nth roots I use either root() from the mpmath package, or root() from the gmpy2 package. I’m pretty sure that these algorithms are available in some C libraries that can be wrapped.

1 Like

You might be interested in this and this stdlib issues.

1 Like

@egio,

Fortran stdlib appears the right place to try out such things where the experimental attempts can eventually turn into proposals for the language. So please look there first and consider trying out options there

Expressions like this are evaluated as exp(log(a)*1.0/3.0) or maybe exp(log(a)/3.0)with some optimization. Unless there is something wrong with the exp() and log() evaluations, from where would the error arise?

Of course, efficiency is a separate issue. There may be faster ways to compute specific values, including cube roots, but however they are evaluated they should all agree to the last bit or two, which is all that you can expect from floating point arithmetic. Are there data anywhere that shows otherwise?

@RonShepard

See this code, that use “quadruple precision” as a reference

program main
    USE ISO_FORTRAN_ENV, only : qp => real128
    real(qp) :: a, b
    real :: c
    do i = 0, 30
        a = 10.0_qp ** i
        b = a**(1.0_qp/3.0_qp)
        c = real(a)**(1.0/3.0)
        write(*,*) i, spacing(real(b)), real(c-b) 
    end do
end program

As a result, the deviation between the single and quadruple precision computation gets larger than the single precision spacing() for values of a larger than 1e3 (compiled with gfortran)

           0  1.1920929E-07  0.0000000E+00
           1  2.3841858E-07 -9.0931627E-09
           2  4.7683716E-07  3.3112110E-07
           3  9.5367432E-07  9.5367432E-07
           4  1.9073486E-06  2.7700912E-06
           5  3.8146973E-06  5.2185596E-06
           6  7.6293945E-06  1.5258789E-05
           7  1.5258789E-05  2.7700913E-05
           8  3.0517578E-05  8.2703176E-05
           9  6.1035156E-05  1.8310547E-04
          10  2.4414062E-04  6.1270251E-04
          11  4.8828125E-04  1.0101373E-03
          12  9.7656250E-04  2.9296875E-03
          13  1.9531250E-03  6.6153063E-03
          14  3.9062500E-03  1.4007622E-02
          15  7.8125000E-03  3.1250000E-02
          16  1.5625000E-02  7.7871814E-02
          17  3.1250000E-02  0.1791387    
          18  6.2500000E-02  0.4375000    
          19  0.2500000       1.059968    
          20  0.5000000       2.166387    
          21   1.000000       5.000000    
          22   2.000000       11.09968    
          23   4.000000       23.66387    
          24   8.000000       56.00000    
          25   16.00000       114.9968    
          26   32.00000       284.6387    
          27   64.00000       640.0000    
          28   256.0000       1405.968    
          29   512.0000       2974.387    
          30   1024.000       7168.000    

This is already in a proposal I’ve submitted. IEEE-754 Recommended Math Functions

ROOTN(X, N)

Description. The principal Nth root of X.

Class. Elemental function.

Arguments.
X shall be of type real.
N shall be of type integer.

Result Characteristics. Same as X.

Result Value. The result has a value equal to a processor-dependent
approximation to X**(1.0/n).

Example. ROOTN(27.0, 3) has the value 3.0 (approximately).

3 Likes

Here is another version of your code.

program root
   use, intrinsic :: iso_fortran_env, only: real128
   real :: a, b, c, db, dc, qb
   do i = 0, 30
      a = 10.0 ** i
      b = a**(1.0/3.0)
      qb = real((10.0_real128 ** i) ** (1.0_real128 / 3.0_real128))
      c = exp(log(a)/3.0)
      db = (b - qb) / spacing(b)
      dc = (c - qb) / spacing(c)
      write(*,'(i2,*(1x,f5.1))') i, db, dc
   enddo
 end program root

The output is

gfortran root.f90 && a.out
 0   0.0   0.0
 1   0.0   0.0
 2   1.0   0.0
 3   1.0   0.0
 4   1.0  -1.0
 5   1.0  -2.0
 6   2.0   1.0
 7   2.0  -3.0
 8   3.0  -1.0
 9   3.0   2.0
10   3.0  -1.0
11   2.0   0.0
12   3.0   1.0
13   3.0   3.0
14   4.0  -5.0
15   4.0  -4.0
16   5.0  -2.0
17   6.0   1.0
18   7.0   3.0
19   3.0  -5.0
20   4.0  -4.0
21   5.0  -3.0
22   6.0   0.0
23   6.0   1.0
24   7.0   3.0
25   8.0   6.0
26   9.0   9.0
27  10.0 -18.0
28   5.0  -9.0
29   6.0  10.0
30   7.0  -6.0

It is interesting that the ** expression differs from the exp(log) expression. Most of the errors are within 2 or 3 bits. The largest error is for 10.0**27, which for some reason has 4 and 5 bits in error.

1 Like

@RonShepard Note that ifort is optimizing a**(1.0/3.0) by taking the integer cubic root… Your code with ifort:

           0  0.0000000E+00  0.0000000E+00
           1  0.0000000E+00  0.0000000E+00
           2  0.0000000E+00  0.0000000E+00
           3  0.0000000E+00  0.0000000E+00
           4  0.0000000E+00  -1.000000    
           5  0.0000000E+00  -2.000000    
           6  0.0000000E+00   1.000000    
           7  0.0000000E+00  -3.000000    
           8  0.0000000E+00  -1.000000    
           9  0.0000000E+00   2.000000    
          10  0.0000000E+00  -1.000000    
          11  0.0000000E+00  0.0000000E+00
          12  0.0000000E+00   1.000000    
          13  0.0000000E+00   3.000000    
          14  0.0000000E+00  -5.000000    
          15  0.0000000E+00  -4.000000    
          16  0.0000000E+00  -2.000000    
          17  0.0000000E+00   1.000000    
          18  0.0000000E+00   3.000000    
          19  0.0000000E+00  -5.000000    
          20  0.0000000E+00  -4.000000    
          21  0.0000000E+00  -3.000000    
          22  0.0000000E+00  0.0000000E+00
          23  0.0000000E+00   1.000000    
          24  0.0000000E+00   3.000000    
          25  0.0000000E+00   6.000000    
          26  0.0000000E+00   9.000000    
          27  0.0000000E+00  -18.00000    
          28  -1.000000      -9.000000    
          29  0.0000000E+00   10.00000    
          30  0.0000000E+00  -6.000000    ```
1 Like

Just a matter of style, but would it possible to define a binary operator for the Nth root, just like the Nth power is? // would have been the perfect twin of **, but it’s already used for the string concatenation. What about */ ? (x */ n would be the approximation of x ** (1.0/n))

:thinking: Interesting idea. If we discuss the paper I will mention it. // could be used, as the interface would not conflict with it’s existing use. I’m not sure how much I like it, but we can think about it.

1 Like

Yes, and that is an interesting point. I will be sure we consider that for the actual edits to the standard.

1 Like

It would be highly misleading…

I agree that notation would be misleading for the root operator. In fact, I’ve wondered why ^ has not been introduced into fortran as a substitute for ** decades ago. I’ve always assumed it is being held back and saved for something better than exponentiation.

As for an operator notation for the integer root, something like n\/x could also work, although the combination of backslash and forward slash looks like an uppercase V with some fonts.

How about using the operator syntax: n .throot. x ?

You could change + to have documented behavior of multiplication if the second number is 63.1 but that doesn’t make that a good idea. Saying “It’s only misleading if you don’t bother to learn the language” ignores the fact that there are a lot of ways to design a language badly.

If introducing a new notation could be easily manageable by the compilers, why not simply ‘^/’ ? This looks natural and might even pave the way for getting ‘^’ to actually work as a replacement for ‘**’ ?

I added another value to these results where I did one Newton step in order to refine the values. This also reduces the errors down to at most one bit in the same way that ifort does by default.

program root
   use, intrinsic :: iso_fortran_env, only: real128
   real :: a, b, c, e, db, dc, qb, de
   do i = 0, 30
      a = 10.0 ** i
      b = a**(1.0/3.0)
      qb = real((10.0_real128 ** i) ** (1.0_real128 / 3.0_real128))
      c = exp(log(a)/3.0)
      e = (2.0*c+a/c**2)/3.0   ! refine with a newton step.
      db = (b - qb) / spacing(b)
      dc = (c - qb) / spacing(c)
      de = (e - qb) / spacing(e)
      write(*,'(i2,*(1x,f5.1))') i, db, dc, de
   enddo
 end program root

gfortran root.f90 && a.out
 0   0.0   0.0   0.0
 1   0.0   0.0   0.0
 2   1.0   0.0   0.0
 3   1.0   0.0   0.0
 4   1.0  -1.0   0.0
 5   1.0  -2.0   0.0
 6   2.0   1.0   0.0
 7   2.0  -3.0   1.0
 8   3.0  -1.0   1.0
 9   3.0   2.0   0.0
10   3.0  -1.0   0.0
11   2.0   0.0  -1.0
12   3.0   1.0   0.0
13   3.0   3.0  -1.0
14   4.0  -5.0   0.0
15   4.0  -4.0   0.0
16   5.0  -2.0  -1.0
17   6.0   1.0   0.0
18   7.0   3.0   0.0
19   3.0  -5.0   0.0
20   4.0  -4.0  -1.0
21   5.0  -3.0   0.0
22   6.0   0.0   0.0
23   6.0   1.0   0.0
24   7.0   3.0   0.0
25   8.0   6.0   0.0
26   9.0   9.0   0.0
27  10.0 -18.0   0.0
28   5.0  -9.0   0.0
29   6.0  10.0   0.0
30   7.0  -6.0   0.0

That refinement step requires a division. Maybe there are other refinement expressions that don’t require that. But on the other hand, a division is not too expensive after computing both a log() and an exp() for the general case.

I’m still puzzled by the unusually large errors in the 10.0**27 case. You can do that one in your head, so why does the computer struggle with it?

Ok, two decades then. In any case, this would be like allowing [ and ] to denote arrays in addition to (/ and /), it would be backwards compatible with prior source code.

Here is one way to avoid division, using one Newton-Raphson step to find the reciprocal, and using that reciprocal in another Newton-Raphson step for the cube root.

! Given N, find the cube root, x, and the reciprocal, y
! without using any division ops
!
program cubrt
implicit none
real x,y,N,dx,dy
integer i
N = 27.0
x = 2.0    ! approx cub.rt
y = 0.3    ! approx reciprocal of x
print '(i1,2es13.6)',0,x,y
do i = 1, 10
   dy = y * (1.0-x*y)
   y  = y + dy
   dx = (N*y*y - x)*0.333333333
   x = x + dx
   print '(i1,2es13.6)',i,x,y
   if (abs(dx) < 1e-7 .and. abs(dy) < 1e-7)exit
end do
end

Output:

0 2.000000E+00 3.000000E-01
1 2.920933E+00 4.200000E-01
2 2.896436E+00 3.247473E-01
3 2.996193E+00 3.440341E-01
4 2.998105E+00 3.334405E-01
5 3.000001E+00 3.335440E-01
6 2.999999E+00 3.333331E-01
7 3.000000E+00 3.333334E-01
8 3.000000E+00 3.333333E-01
9 3.000000E+00 3.333333E-01

A good working solution for the cube root problem would probably require a good way of generating sufficiently close first approximations without resorting to calling exp or log.

Similar issues occur in converting a binary floating point number to a decimal string. A good approximation for the base-10 log of the input number is needed.

2 Likes