diff --git a/segpy/ibm_float.py b/segpy/ibm_float.py index 254b82a..8f307c4 100644 --- a/segpy/ibm_float.py +++ b/segpy/ibm_float.py @@ -224,9 +224,29 @@ class IBMFloat(Real): def from_bytes(cls, b): return cls(b) + @classmethod + def ldexp(cls, fraction, exponent): + """Make an IBMFloat from fraction and exponent. + + The is the inverse function of IBMFloat.frexp() + + Args: + fraction: A Real in the range -1.0 to 1.0. + exponent: An integer in the range -256 to 255 inclusive. + """ + if not (-1.0 <= fraction <= 1.0): + raise ValueError("ldexp fraction {!r} out of range -1.0 to +1.0") + + if not (-256 <= exponent < 256): + raise ValueError("ldexp exponent {!r} out of range -256 to 256") + + ieee = fraction * 2**exponent + return IBMFloat.from_float(ieee) + @property def signbit(self): - return self._data[0] & 0x80 + """True if the value is negative, otherwise False.""" + return bool(self._data[0] & 0x80) def __float__(self): return ibm2ieee(self._data) @@ -244,25 +264,40 @@ class IBMFloat(Real): return not self.is_zero() def is_zero(self): - return all(b == 0 for b in self._data) + return self.int_mantissa == 0 def __nonzero__(self): return not self.is_zero() def is_subnormal(self): - return (not self.is_zero()) and (self._data[1] < 16) + if self.is_zero(): + # Only one of the many possible representations of zero is considered 'normal' - all the zeros + return not all(b == 0 for b in self._data) + + return self._data[1] < 16 # TODO: Replace magic number with constant def zero_subnormal(self): return IBM_FLOAT_ZERO if self.is_subnormal() else self def frexp(self): - raise NotImplemented - # return the mantissa and exponent + """Obtain the fraction and exponent. + + Returns: + A pair where the first item is the fraction in the range -1.0 and +1.0 and the + exponent is an integer such that f = fraction * 2**exponent + """ + sign = -1 if self.signbit else 1 + mantissa = sign * self.int_mantissa / _F24 + exp_2 = self.exp16 * 4 + return mantissa, exp_2 def __pos__(self): return self def __neg__(self): + if self.is_zero(): + return IBM_FLOAT_ZERO + data = self._data return IBMFloat((data[0] ^ 0b10000000, data[1], @@ -270,6 +305,9 @@ class IBMFloat(Real): data[3])) def __abs__(self): + if self.is_zero(): + return IBM_FLOAT_ZERO + data = self._data return IBMFloat((data[0] & 0b01111111, data[1], @@ -278,7 +316,9 @@ class IBMFloat(Real): def __eq__(self, rhs): if not isinstance(rhs, IBMFloat): - return NotImplemented + nlhs = self.normalize() if self.is_subnormal() else self + nrhs = rhs.normalize() + if # TODO: Consider forcing normalisation return self._data == rhs._data @@ -322,10 +362,12 @@ class IBMFloat(Real): return float(self) <= float(rhs) def __ceil__(self): - return ceil(float(self)) + t = trunc(self) + return t if self.signbit else t + 1 def __floor__(self): - return floor(float(self)) + t = trunc(self) + return t - 1 if self.signbit else t @property def exp16(self): @@ -344,26 +386,17 @@ class IBMFloat(Real): exponent_16 = self.exp16 mantissa = self.int_mantissa - # print("sign =", sign) - # print("exponent_16", exponent_16, hex(exponent_16), bin(exponent_16)) - # print("mantissa", mantissa, hex(mantissa), bin(mantissa)) - num_nybbles_to_preserve = min(exponent_16, MAX_BITS_PRECISION_IBM_FLOAT // BITS_PER_NYBBLE) num_bits_to_clear = MAX_BITS_PRECISION_IBM_FLOAT - num_nybbles_to_preserve * BITS_PER_NYBBLE clear_mask = 2**num_bits_to_clear - 1 preserve_mask = (2**MAX_BITS_PRECISION_IBM_FLOAT - 1) & ~clear_mask - # print("num_nybbles_to_preserve", num_nybbles_to_preserve) - # print("num_bits_to_clear", num_bits_to_clear) - # print("clear_mask ", bin(clear_mask)) - # print("preserve_mask", bin(preserve_mask)) - truncated_mantissa = mantissa & preserve_mask magnitude = truncated_mantissa * pow(16, exponent_16) >> MAX_BITS_PRECISION_IBM_FLOAT return sign * magnitude def normalize(self): - """Attempt to normalize the floating point value. + """Normalize the floating point value. Returns: A normalized IBMFloat equal in value to this object. @@ -371,12 +404,12 @@ class IBMFloat(Real): Raises: FloatingPointError: If the number could not be normalized. """ + if self.is_zero(): + return IBM_FLOAT_ZERO + exponent_16 = self.exp16 mantissa = self.int_mantissa - if mantissa == 0: - return IBM_FLOAT_ZERO - while mantissa < (1 << 20): new_exponent_16 = exponent_16 - 1 if not (-64 <= new_exponent_16 < 64): @@ -387,7 +420,7 @@ class IBMFloat(Real): exponent_16_biased = exponent_16 + EXPONENT_BIAS - sign = self.signbit << 7 + sign = int(self.signbit) << 7 a = sign | exponent_16_biased b = (mantissa >> 16) & 0xff @@ -412,7 +445,7 @@ class IBMFloat(Real): return IBMFloat.from_float(p) if isinstance(rhs, IBMFloat) else p def __int__(self): - raise trunc(self) + return trunc(self) IBM_FLOAT_ZERO = IBMFloat.from_bytes(IBM_ZERO_BYTES) diff --git a/test/test_float.py b/test/test_float.py index e3a7d12..59617f4 100644 --- a/test/test_float.py +++ b/test/test_float.py @@ -2,7 +2,8 @@ from math import trunc, floor import unittest from hypothesis import given, assume -from hypothesis.descriptors import integers_in_range, floats_in_range +from hypothesis.descriptors import integers_in_range, floats_in_range, just +import math from segpy.portability import byte_string from segpy.ibm_float import (ieee2ibm, ibm2ieee, MAX_IBM_FLOAT, SMALLEST_POSITIVE_NORMAL_IBM_FLOAT, @@ -212,6 +213,22 @@ class TestIBMFloat(unittest.TestCase): ibm = IBMFloat.from_float(ieee) self.assertEqual(trunc(ibm), i) + @given(integers_in_range(MIN_EXACT_INTEGER_IBM_FLOAT, MAX_EXACT_INTEGER_IBM_FLOAT - 1)) + @given(floats_in_range(0.0, 1.0)) + def test_ceil(self, i, f): + assume(f != 1.0) + ieee = i + f + ibm = IBMFloat.from_float(ieee) + self.assertEqual(math.ceil(ibm), i + 1) + + @given(integers_in_range(MIN_EXACT_INTEGER_IBM_FLOAT, MAX_EXACT_INTEGER_IBM_FLOAT - 1)) + @given(floats_in_range(0.0, 1.0)) + def test_floor(self, i, f): + assume(f != 1.0) + ieee = i + f + ibm = IBMFloat.from_float(ieee) + self.assertEqual(math.floor(ibm), i) + def test_normalise_subnormal_expect_failure(self): # This float has an base-16 exponent of -64 (the minimum) and cannot be normalised ibm = IBMFloat.from_float(1.6472184286297693e-83) @@ -251,17 +268,70 @@ class TestIBMFloat(unittest.TestCase): normalized = ibm.normalize() self.assertFalse(normalized.is_subnormal()) + @given(integers_in_range(128, 255), + integers_in_range(0, 255), + integers_in_range(0, 255), + integers_in_range(4, 23)) + def test_zero_subnormal(self, b, c, d, shift): + mantissa = (b << 16) | (c << 8) | d + assume(mantissa != 0) + mantissa >>= shift + assert mantissa != 0 + + sa = EXPONENT_BIAS + sb = (mantissa >> 16) & 0xff + sc = (mantissa >> 8) & 0xff + sd = mantissa & 0xff + + ibm = IBMFloat.from_bytes((sa, sb, sc, sd)) + assert ibm.is_subnormal() + z = ibm.zero_subnormal() + self.assertTrue(z.is_zero()) @given(integers_in_range(0, 255), integers_in_range(0, 255), integers_in_range(0, 255), integers_in_range(0, 255)) def test_abs(self, a, b, c, d): - b = byte_string((a, b, c, d)) - ibm = IBMFloat.from_bytes(b) + ibm = IBMFloat.from_bytes((a, b, c, d)) abs_ibm = abs(ibm) self.assertGreaterEqual(abs_ibm.signbit, 0) + @given(integers_in_range(0, 255), + integers_in_range(0, 255), + integers_in_range(0, 255), + integers_in_range(0, 255)) + def test_negate_non_zero(self, a, b, c, d): + ibm = IBMFloat.from_bytes((a, b, c, d)) + assume(not ibm.is_zero()) + negated = -ibm + self.assertNotEqual(ibm.signbit, negated.signbit) + + def test_negate_zero(self): + zero = IBMFloat.from_float(0.0) + negated = -zero + self.assertTrue(negated.is_zero()) + + @given(floats_in_range(MIN_IBM_FLOAT, MAX_IBM_FLOAT)) + def test_signbit(self, f): + ltz = f < 0 + ibm = IBMFloat.from_float(f) + self.assertEqual(ltz, ibm.signbit) + + @given(floats_in_range(-1.0, +1.0), + integers_in_range(-256, 255)) + def test_ldexp_frexp(self, fraction, exponent): + try: + ibm = IBMFloat.ldexp(fraction, exponent) + except OverflowError: + assume(False) + else: + f, e = ibm.frexp() + self.assertTrue(almost_equal(fraction * 2**exponent, f * 2**e, epsilon=EPSILON_IBM_FLOAT)) + + + + if __name__ == '__main__':