StrangerSide
StrangerSide

Reputation: 19

Converting large (decimal module) floats into binary representation in python?

So, I'm trying to get long series of bits from a decimal number as defined by the python decimal module, currently this one:

Decimal('3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303820')

(Just pi to 200 digits, by the way.)

I want the whole thing in binary representation, so I can convert it into an array of hexadecimal numbers representing everything below the 3.

Any ideas on how to do this? I kept looking through the module but it only seems to provide strictly decimal-based functions.

Upvotes: 1

Views: 767

Answers (2)

sOvr9000
sOvr9000

Reputation: 41

My apologies for the bump, but for anyone who still needs an answer to this (using Decimal), it's possible to write your own conversion method. It would not be highly efficient, but it can be accurate enough if you temporarily increase precision while generating the binary digits. Digits in any base (greater than one) are simply coefficients on the powers of the given base. Commented code below outlines the entire process to convert any number from decimal to any other base (if the new base is greater than one, of course). This algorithm works for any arbitrary-precision python module that can support logarithms and floating point numbers.

from decimal import *

def convert_base(x, new_base, preferred_digits=200):
    # ensure that x and new_base both have arbitrary precision
    x = Decimal(x)
    new_base = Decimal(new_base)

    # compute the log floor of x -- this is the exponent of the power of new_base of which the leading digit of x (in base new_base) is the coefficient
    # for example: pi in base 10 is 3.14159... but in base 2 it's 1*2 + 1*1 + 0*0.5 + ... which means exponent=1 because that sum starts with 2 raised to the power of exponent=1 (1*2^1 + ...)
    # further, log(pi) = 1.65... which means the floor is floor(log(pi)) = 1, so again, p=1.
    # in other words, exponent represents the largest exponent on the power of new_base where the power of new_base is less than or equal to the given x.
    exponent = (x.log10()/Decimal(new_base).log10()).__floor__()

    # we need a copy of this value to return it
    largest_exponent = exponent

    # start at the value of x
    # we will work our way down to 0, or get very close to it
    # ideally, we'll come up with a sufficient number of digits
    v = x
    
    # the list of digits that this function will return after populating it
    digits = []

    # keep decreasing v until it's extremely close to 0 (such that it becomes 0 with the provided precision) or the number of digits we generate reaches or exceeds our preferred_digits
    while v > 0 and len(digits) < preferred_digits:
        # the current power of new_base
        power = new_base ** exponent

        # compute the coefficient of the power (this is the next digit to add to digits)
        # we want this to be an integer, so we'll floor it
        coefficient = int(v / power)
        digits.append(coefficient)

        # since v is a sum of powers of new_base, we can now subtract power * coefficient from v, and we'll be left with the exact same problem as before, but we have obtained one digit
        # so we just rinse and repeat until we have enough digits, or all that there are in the converted representation
        v -= power * coefficient

        # to continue the process, we must make sure that the power of new_base is decreasing (so we increment p, the exponent of new_base, as power has -p as the exponent)
        # the whole point of using -p as the exponent to find the power of the new_base is to use multiplication in this loop instead of division (so it's slightly faster)
        exponent -= 1

    # we return digits, but also the largest exponent so we know where to put the "point" in the number
    # every number in any base is represented by a mantissa and an exponent.  here, largest_exponent is the exponent of the number's representation in base new_base
    # digits are part of the mantissa, so we've essentially converted x into base new_base
    return digits, largest_exponent


# we can also convert back to decimal from any base
# we just sum the products of the digits and the corresponding powers of from_base, based on exponent
def convert_to_decimal(digits, exponent, from_base):
    # again, from_base can have arbitrary precision
    from_base = Decimal(from_base)

    # start with a zero sum
    # this sum will yield our original x that we passed to convert_base()
    x = 0

    # create a copy of the exponent, but we do this so we don't have to modify the argument of the function itself (just a good practice)
    p = exponent

    # now we iterate over the digits, decreasing exponent (p) each time
    for d in digits:
        # compute the power
        power = from_base ** p

        # multiply by coefficient (current digit)
        product = power * d

        # add to the total sum
        x += product

        # decrease the exponent (p)
        p -= 1

    # the sum is now a decimal representation of number that has the given mantissa (digits) and exponent (exponent) in the given base (from_base)
    return x




digits, exponent = convert_base(Decimal('3.1415926535'), 2)

print(digits)
print(exponent)

reconverted = convert_to_decimal(digits, exponent, 2)

print(reconverted)

# some rounding errors of course, but that's nearly impossible to avoid
# one simple workaround is to temporarily increase the digits of precision by 2 or 3 digits for the conversion processes

Okay, so I've technically answered the question. But I really want to emphasize that the decimal module for Python is not the best way to go for arbitrary precision floating point numbers. I strongly recommend mpmath. I've used it in countless personal projects and it never fails me! Here's the same algorithm using mpmath.

from mpmath import *

def convert_base(x, new_base, preferred_digits=200):
    x = mpf(x)
    new_base = mpf(new_base)
    exponent = int(floor(log(x)/log(new_base)))
    largest_exponent = exponent
    v = x
    digits = []
    while v > 0 and len(digits) < preferred_digits:
        power = new_base ** exponent
        coefficient = int(v / power)
        digits.append(coefficient)
        v -= power * coefficient
        exponent -= 1
    return digits, largest_exponent

def convert_to_decimal(digits, exponent, from_base):
    from_base = mpf(from_base)
    x = 0
    p = exponent
    for d in digits:
        power = from_base ** p
        product = power * d
        x += product
        p -= 1
    return x



digits, exponent = convert_base(mpf('3.1415926535'), 2)

print(digits)
print(exponent)

reconverted = convert_to_decimal(digits, exponent, 2)

print(reconverted)

# still some rounding errors with mpmath, but they're not displayed because it's smart

We can even compare execution times.

import timeit

decimal_time = timeit.timeit('using_decimal.convert_base(3.1415926535, 2, 200)', number=10000, setup='import using_decimal')
mpmath_time = timeit.timeit('using_mpmath.convert_base(3.1415926535, 2, 200)', number=10000, setup='import using_mpmath')

print(f'decimal time: {decimal_time} seconds')
print(f'mpmath time: {mpmath_time} seconds')
print('Speed improvement: {:+.1f}%'.format((decimal_time-mpmath_time)*100./mpmath_time))
decimal time: 4.2381332 seconds
mpmath time: 3.4054762 seconds
Speed improvement: +24.5%

Hoping this helps!

Upvotes: 2

casevh
casevh

Reputation: 11404

It can be done with gmpy2.

>>> import gmpy2
>>> pi=gmpy2.const_pi(precision=100)
>>> "{0:A}".format(pi)
'0X3.243F6A8885A308D313198A2EP+0'
>>> 

Use help(gmpy2.mpfr().__format__) for details on the formatting options supported by the mpfr type.

Disclaimer: I maintain gmpy2.

Upvotes: 1

Related Questions