Skip to content

Instantly share code, notes, and snippets.

@dkirkby
Last active March 6, 2021 18:45
Show Gist options
  • Save dkirkby/24ba28eeb096f2477a13c8e8d909a93f to your computer and use it in GitHub Desktop.
Save dkirkby/24ba28eeb096f2477a13c8e8d909a93f to your computer and use it in GitHub Desktop.
Floating point representations and rounding
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)
# 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