How to pack four signed floats into a single integer?

1.9k views Asked by At

I need to be able to pack 4 float digits into a single integer and then unpack the integer into my 4 floats.

The example of floats (not more than 8-digit precision):

-0.02513393, -0.02394553, 0.04248389, 0.02388026

So, I thought that firstly I need to cast those floats to integers by multiplying each by 1000000000.

floats = [-0.02513393, -0.02394553, 0.04248389, 0.02388026]
integers = list(map(lambda i: int(i * 1000000000), floats))
# output: [-25133930, -23945530, 42483890, 23880260]

And then use bitwise operations to fit four numbers into one, something like this:

a, b, c, d = integers
packed = (a << 24) | (b << 16) | (c << 8) | d

However, this doesn't seem right because the values I'm trying to pack are signed.


Could you please prompt me with the right solution for packing such signed floats into a single integer and the correct way to unpack them?


I thought of adding 1 to the end of every negative value and 0 to the end of every positive value and to restore the integers into floats I would first check if there's a 1 I'd negate the value and then divide by 1000000000. But that's not elegant at all.

3

There are 3 answers

0
lvc On BEST ANSWER

If, per the comments, the width of the packed data doesn't matter, your general approach is workable with some tweaks.

  • First, 8 bits for each number isn't enough; you will have overlap between each one. Your floats are known to have only 8 digits precision, but that doesn't imply that they only have 8 significant bits in the binary representation. A good way to find out how wide they need to be is to consider a number you know they are all lower than (in your case, 1000000000), then the bit length of that number (30) is sufficient. So we have:

    packed = a << 90 | b << 60 | c << 30 | d

  • As you suspect, this still has problems with negative numbers. From the above, I can succesfully recover d with packed & 2**30-1 and c with (packed & 2**30-1 << 30 ) >> 30, but doing similar things for a and b gives me nonsense. So reduce it to the problem you have already solved. If you add a large enough number to each one so that they are all positive, you can treat them as unsigned - once again, you know they are less than 1000000000 , so there's the magic number. The fiddled numbers are all now less than 2000000000, so we need to adjust our field width. So we have:

    ceiling = 1000000000 packed = (a + ceiling) << 31*3 | (b + ceiling) << 31*2 | (c + ceiling) << 31 | d

And we can recover a as ((packed & 2**31-1<< 31*3) >> 31*3) - ceiling. For sake of readability, you may want to consider writing this as a loop.

0
unutbu On

Using NumPy, you could view a 4-element array of dtype float16 as an integer array of dtype int64:

In [125]: np.array([-0.02513393, -0.02394553, 0.04248389, 0.02388026], dtype=np.float16).view(np.int64)
Out[125]: array([2746396911566169711])

To unpack the int you could use view(np.float16):

In [126]: np.array([2746396911566169711]).view(np.float16)
Out[126]: array([-0.02513123, -0.02394104,  0.04248047,  0.02388   ], dtype=float16)

Note there is some loss of precision.


Using Python3.2 (or higher) and no NumPy, you could pack the floats into bytes, then use int.from_bytes to convert the bytes to an int. To unpack, use int.to_bytes and struct.unpack:

import struct

def floats_to_int(floats):
    return int.from_bytes(struct.pack('4d', *floats), 'big')

def int_to_floats(packed):
    return struct.unpack('4d', packed.to_bytes(4*8, 'big'))

floats = [-0.02513393, -0.02394553, 0.04248389, 0.02388026]
print(floats)
packed = floats_to_int(floats)
print(packed)
result = int_to_floats(packed)
print(result)

prints

[-0.02513393, -0.02394553, 0.04248389, 0.02388026]
3995686615650679380069295189325600154682811585786433559914521688689786263615
(-0.02513393, -0.02394553, 0.04248389, 0.02388026)
0
PM 2Ring On

As mentioned in the comments, your current strategy isn't working because you've mixed up 8 decimal digit precision with 8 bit precision.

(a << 24) | (b << 16) | (c << 8) | d

would work if those variables contained 8 bit data, i.e. integers in range(256). You need around 32 bits to store float data to 8 decimal digit precision.

Note that standard Python (aka CPython) uses IEEE 754 binary64 double precision for its floats.

But you could approximate your float data using 32 bit single-precision floats, and pack them using the standard struct module. Here's a short demo:

from struct import pack, unpack

# Make a list of 4 Python floats.
a = [i**0.5 for i in range(5, 9)]
print(a)

# Convert the Python floats to 32 bit floats and pack them into 16 bytes, big endian
fmt = '>ffff'
b = pack(fmt, *a)
print(b, len(b))

# Unpack the bytes back into floats 
z = unpack(fmt, b)
print(z)
print([u*u for u in z])

# Pack the bytes into an int, using big-endian packing
num = int.from_bytes(b, 'big')
print(num)

# Convert the int back to bytes
newb = num.to_bytes(16, 'big')
print(newb, newb == b)    

output

[2.23606797749979, 2.449489742783178, 2.6457513110645907, 2.8284271247461903]
b'@\x0f\x1b\xbd@\x1c\xc4q@)S\xfd@5\x04\xf3' 16
(2.2360680103302, 2.4494898319244385, 2.6457512378692627, 2.8284270763397217)
[5.00000014682206, 6.000000436701214, 6.999999612686736, 7.999999726171666]
85149038802136470295784196693032240371
b'@\x0f\x1b\xbd@\x1c\xc4q@)S\xfd@5\x04\xf3' True

Note that .from_bytes and .to_bytes are Python 3 features; the same operations in Python 2 are a little more verbose.