floating-pointprecisionnumerical-methodsfloating-accuracy

integer exact computation with logs


I need to compute ceil(log_N(i)) where log_N is the log with positive integer base N and i is also a positive integer.

The straight forward python implementation using floating point math fails at quite a few points:

import numpy as np

bases = [2, 3, 4, 5, 7, 8, 9, 11, 13, 16, 17, 19, 23, 25, 27, 29, 31, 32, 37, 41, 43, 47]

exponents = [1,2,3,4]

for nn in exponents:
  for p in bases:
    i = p**nn
    _ = np.ceil(np.log(i)/np.log(p)) #  = nn
    if _.astype(int) != nn:
      print(f"Error: log({i})/log({p}) = {_} != {nn}")

outputs:

Error: log(125)/log(5) = 4.0 != 3; delta = 1.0
Error: log(15625)/log(25) = 4.0 != 3; delta = 1.0
Error: log(103823)/log(47) = 4.0 != 3; delta = 1.0

removing the ceil and integer casting operations we see that in some cases we are off by ~4e-16

Error: log(125)/log(5) = 3.0000000000000004 != 3; delta = 4.440892098500626e-16
Error: log(4913)/log(17) = 2.9999999999999996 != 3; delta = 4.440892098500626e-16
Error: log(15625)/log(25) = 3.0000000000000004 != 3; delta = 4.440892098500626e-16
Error: log(29791)/log(31) = 2.9999999999999996 != 3; delta = 4.440892098500626e-16
Error: log(68921)/log(41) = 2.9999999999999996 != 3; delta = 4.440892098500626e-16
Error: log(103823)/log(47) = 3.0000000000000004 != 3; delta = 4.440892098500626e-16

which is only 2ish LSBs off but its not consistent one direction or the other.

Is there a way to always get this right every time?


Solution

  • In Squeak Smalltalk, there is a method named numberOfDigitsInBase: which does almost what you want : floor( log_N( i ) ) + 1

    For the implementation, we decided to go with exact arithmetic for several reasons:

    If I translate such method in Python, and adapt it to answer ceil( log_N( i ) ), that's something like:

    def ceil_log_N( i , b ):
    # compute ceil(log of i in base b) with exact arithmetic
    # assuming both arguments are strictly positive integers
      if i <= b :
        return 1
      if b & (b-1) == 0 : # if b.bit_count() == 1 :
        # fast return when base is a power of two
        return 1 + ((i-1).bit_length() - 1) // (b.bit_length() - 1)
      q = i
      ceil_log = 0
      while q >= b :
        n = q.bit_length() // b.bit_length()
        q = q // (b ** n)
        ceil_log = ceil_log + n
      if q == 0 or (q == 1 and i == (b ** ceil_log)):
        return ceil_log
      return ceil_log + 1
    

    The principle is to use bit_length()-1 as a rough approximation of log in base 2,
    then get log of i in base b as log2(i)/log2(b)

    Care is taken to get an underestimate as first guess.

    The number of loops should be small.

    EDIT
    If you actually want to compute number of digits to print in some base, then the code would be more like:

    def ndigits_in_base( i , b ):
    # return how many digits are necessary to print positive integer i in base b
      if i < b :
        return 1
      if b & (b-1) == 0 : # if b.bit_count() == 1 :
        # fast return when base is a power of two
        return 1 + (i.bit_length() - 1) // (b.bit_length() - 1)
      q = i
      ndigits = 0
      while q >= b :
        n = q.bit_length() // b.bit_length()
        q = q // (b ** n)
        ndigits = ndigits + n
      if q == 0 :
        return ndigits
      return ndigits + 1