Last active
March 6, 2021 18:45
-
-
Save dkirkby/24ba28eeb096f2477a13c8e8d909a93f to your computer and use it in GitHub Desktop.
Floating point representations and rounding
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import struct | |
def decode_float(x): | |
"""Decode the fields of an IEEE 754 single or double precision float: | |
https://en.wikipedia.org/wiki/Single-precision_floating-point_format | |
https://en.wikipedia.org/wiki/Double-precision_floating-point_format | |
""" | |
if isinstance(x, np.floating): | |
bits = np.finfo(x.dtype).bits | |
assert bits in (32, 64), f'Unsupported numpy floating dtype: {type(x)}.' | |
else: | |
# Python float is always 64 bit. | |
bits = 64 | |
fmt = '!d' if bits == 64 else '!f' | |
bites = ''.join('{:0>8b}'.format(bite) for bite in struct.pack(fmt, x)) | |
# Split into IEEE 754 fields. | |
split = 12 if bits == 64 else 9 | |
sign, exponent, fraction = bites[:1], bites[1:split], bites[split:] | |
bitstring = f'{sign}.{exponent}.{fraction}' | |
# Convert each field back to a decimal integer. | |
sign, exponent, fraction = map(lambda b: int(b, 2), (sign, exponent, fraction)) | |
return f'{bitstring}({"-" if sign else "+"},{exponent},{fraction})={x}' | |
# Reproduce examples from the wikipedia pages | |
decode_float(np.float32(0.15625)) | |
decode_float(3 / 256) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Rounding works as expected with 64-bit floats but not with 32-bit floats that get converted to 64-bit before printing. | |
decode_float(np.round(np.float64(1/3), 4)) | |
#0.01111111101.0101010101001100100110000101111100000110111101101001(+,1021,1500599395839849)=0.3333 | |
decode_float(np.round(np.float32(1/3), 4)) | |
#0.01111101.01010101010011001001100(+,125,2795084)=0.33329999446868896 | |
# Custom JSON encoder that rounds any np.float32 values to a fixed decimal precision. | |
# This gives you the option to select fixed (float32) or full (float64) precision for each value. | |
# Note that some type of encoder is always required for json serialization of | |
# numpy types, even without rounding. | |
import json | |
class NumpyEncoder(json.JSONEncoder): | |
"""JSON encoder to use with numpy data with rounding of float32 values. | |
""" | |
FLOAT32_DECIMALS = 6 | |
def default(self, obj): | |
if isinstance(obj, np.float32): | |
# Convert to 64-bit float before rounding. | |
return float(np.round(np.float64(obj), self.FLOAT32_DECIMALS)) | |
elif isinstance(obj, np.floating): | |
return float(obj) | |
elif isinstance(obj, np.integer): | |
return int(obj) | |
elif isinstance(obj, np.ndarray): | |
if obj.dtype.fields is not None: | |
# convert a recarray to a dictionary. | |
new_obj = {} | |
for (name, (dtype, size)) in obj.dtype.fields.items(): | |
if dtype.base == np.float32: | |
new_obj[name] = np.round(obj[name], self.FLOAT32_DECIMALS) | |
else: | |
new_obj[name] = obj[name] | |
return new_obj | |
else: | |
if obj.dtype == np.float32: | |
# tolist converts to 64-bit native float so apply rounding first. | |
obj = np.round(obj.astype(np.float64), self.FLOAT32_DECIMALS) | |
return obj.tolist() | |
else: | |
return super().default(obj) | |
values = [1/2, 1/3, 1/5, 1/27] | |
data = dict(float32=np.array(values, np.float32), float64=np.array(values, np.float64)) | |
json.dumps(data, cls=NumpyEncoder) | |
#{"float32": [0.5, 0.333333, 0.2, 0.037037], "float64": [0.5, 0.3333333333333333, 0.2, 0.037037037037037035]} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment