From 1779f11b443bdca98e83d8c5398ad17469039591 Mon Sep 17 00:00:00 2001 From: Robert Smallshire Date: Fri, 6 Mar 2015 18:58:38 +0100 Subject: [PATCH] Adds a new IBMFloat type which losslessly stores IBM 32 bit floats --- segpy/ibm_float.py | 72 +++++++++++++++++++++++++++++++++++++++++++--- segpy/util.py | 9 +++++- test/test_float.py | 63 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 138 insertions(+), 6 deletions(-) diff --git a/segpy/ibm_float.py b/segpy/ibm_float.py index 1a53701..e5ecd66 100644 --- a/segpy/ibm_float.py +++ b/segpy/ibm_float.py @@ -7,9 +7,11 @@ LARGEST_NEGATIVE_NORMAL_IBM_FLOAT = -5.397605346934028e-79 SMALLEST_POSITIVE_NORMAL_IBM_FLOAT = 5.397605346934028e-79 MAX_IBM_FLOAT = 7.2370051459731155e+75 -_IBM_FLOAT32_BITS_PRECISION = 24 -_L24 = long_int(2) ** _IBM_FLOAT32_BITS_PRECISION -_F24 = float(pow(2, _IBM_FLOAT32_BITS_PRECISION)) +IBM_FLOAT32_MAX_BITS_PRECISION = 24 +IBM_FLOAT32_MIN_BITS_PRECISION = 21 # The first 3 bits of the mantissa may be zero +IBM_FLOAT32_EPSILON = pow(2.0, -(IBM_FLOAT32_MIN_BITS_PRECISION - 1)) +_L24 = long_int(2) ** IBM_FLOAT32_MAX_BITS_PRECISION +_F24 = float(pow(2, IBM_FLOAT32_MAX_BITS_PRECISION)) def ibm2ieee(big_endian_bytes): @@ -35,7 +37,7 @@ def ibm2ieee(big_endian_bytes): def ieee2ibm(f): - """Covert a float to four big-endian bytes representing an IBM float. + """Convert a float to four big-endian bytes representing an IBM float. Args: f (float): The value to be converted. @@ -112,3 +114,65 @@ def ieee2ibm(f): d = mantissa & 0xff return byte_string((a, b, c, d)) + + +class IBMFloat(object): + __slots__ = ['_data'] + + def __init__(self, b): + """Initialise IBMFloat from an IEEE float. + + Args: + b: A byte sequence containing exactly four bytes + + Raises: + ValueError: If b does not contain exactly four values in the range 0-255. + """ + data = bytes(b) + num_bytes = len(data) + if num_bytes != 4: + raise ValueError("{} cannot be constructed from {} values".format(self.__class__.__name__, num_bytes)) + self._data = data + + @classmethod + def from_float(cls, f): + """Construct an IBMFloat from an IEEE float. + + Args: + f (float): The value to be converted. + + Returns: + An IBMFloat. + + Raises: + OverflowError: If f is outside the representable range. + ValueError: If f is NaN or infinite. + FloatingPointError: If f cannot be represented without total loss of precision. + """ + return cls(ieee2ibm(f)) + + @classmethod + def from_bytes(cls, b): + return cls(b) + + def __float__(self): + return ibm2ieee(self._data) + + def __bytes__(self): + return self._data + + def __repr__(self): + return "{}({!r})".format(self.__class__.__name__, self._data) + + def __bool__(self): + return not self.is_zero() + + def is_zero(self): + return all(b == 0 for b in self._data) + + def __nonzero__(self): + return not self.is_zero() + + def is_subnormal(self): + return (not self.is_zero()) and (self._data[1] < 32) + diff --git a/segpy/util.py b/segpy/util.py index 0b16647..7b4508e 100644 --- a/segpy/util.py +++ b/segpy/util.py @@ -230,4 +230,11 @@ def round_up(integer, multiple): def underscores_to_camelcase(s): """Convert text_in_this_style to TextInThisStyle.""" - return ''.join(w.capitalize() for w in s.split('_')) \ No newline at end of file + return ''.join(w.capitalize() for w in s.split('_')) + + +def almost_equal(x, y, epsilon): + max_xy_one = max(1.0, abs(x), abs(y)) + e = epsilon * max_xy_one + delta = abs(x - y) + return delta <= e \ No newline at end of file diff --git a/test/test_float.py b/test/test_float.py index 2388dab..a04e903 100644 --- a/test/test_float.py +++ b/test/test_float.py @@ -1,8 +1,12 @@ import unittest +from hypothesis import given +from hypothesis.descriptors import integers_in_range, floats_in_range + from segpy.portability import byte_string from segpy.ibm_float import (ieee2ibm, ibm2ieee, MAX_IBM_FLOAT, SMALLEST_POSITIVE_NORMAL_IBM_FLOAT, - LARGEST_NEGATIVE_NORMAL_IBM_FLOAT, MIN_IBM_FLOAT) + LARGEST_NEGATIVE_NORMAL_IBM_FLOAT, MIN_IBM_FLOAT, IBMFloat, IBM_FLOAT32_EPSILON) +from segpy.util import almost_equal class Ibm2Ieee(unittest.TestCase): @@ -134,5 +138,62 @@ class Ibm2IeeeRoundtrip(unittest.TestCase): self.assertEqual(ibm_start, ibm_result) +class TestIBMFloat(unittest.TestCase): + + def test_zero_from_float(self): + zero = IBMFloat.from_float(0.0) + self.assertTrue(zero.is_zero()) + + def test_zero_from_bytes(self): + zero = IBMFloat.from_bytes(b'\x00\x00\x00\x00') + self.assertTrue(zero.is_zero()) + + def test_subnormal(self): + ibm = IBMFloat.from_float(1.6472184286297693e-83) + self.assertTrue(ibm.is_subnormal()) + + def test_smallest_subnormal(self): + ibm = IBMFloat.from_float(5.147557589468029e-85) + self.assertEqual(bytes(ibm), byte_string((0x00, 0x00, 0x00, 0x01))) + + def test_too_small_subnormal(self): + with self.assertRaises(FloatingPointError): + IBMFloat.from_float(1e-86) + + def test_nan(self): + with self.assertRaises(ValueError): + IBMFloat.from_float(float('nan')) + + def test_inf(self): + with self.assertRaises(ValueError): + IBMFloat.from_float(float('inf')) + + def test_too_large(self): + with self.assertRaises(OverflowError): + IBMFloat.from_float(MAX_IBM_FLOAT * 10) + + def test_too_small(self): + with self.assertRaises(OverflowError): + IBMFloat.from_float(MIN_IBM_FLOAT * 10) + + @given(float) + def test_bool(self, f): + self.assertEqual(bool(IBMFloat.from_float(f)), bool(f)) + + @given(integers_in_range(0, 255), + integers_in_range(0, 255), + integers_in_range(0, 255), + integers_in_range(0, 255)) + def test_bytes_roundtrip(self, a, b, c, d): + b = byte_string((a, b, c, d)) + ibm = IBMFloat.from_bytes(b) + self.assertEqual(bytes(ibm), b) + + @given(floats_in_range(MIN_IBM_FLOAT, MAX_IBM_FLOAT)) + def test_floats_roundtrip(self, f): + ibm = IBMFloat.from_float(f) + self.assertTrue(almost_equal(f, float(ibm), epsilon=IBM_FLOAT32_EPSILON)) + + if __name__ == '__main__': unittest.main()