""" Simulation of binary floating point representation at arbitrary fixed or infinite precision (including greater than 64 bit). :name: Simfloat :author: Robert Clewley :version: 0.1 :contact: rob clewley gmail com :date: August 2008 :license: New BSD ***** Features * Primarily intended for teaching purposes, e.g. in a numerical analysis course. Demonstrates representation formats and facilitates exploration of the distribution of represented values along the real number line. * Not intended to be the most efficient implementation, but transparency of implementation helpful for learning about issues in IEEE representation, e.g. denormalized numbers. * Open source code, released under the BSD license. Please improve this code for clarity, bug-fixes and feature improvements, but only to improve efficiency where it is not at the expense of a reader's easy comprehension of the algorithms and binary representation. * Can represent any (sign, characteristic, significand) IEEE 754-style format, and is not restricted to representations with total precision less than 64 bits. The internal representations and arithmetic are done using arbitrary precision and do not depend on the python 'float' (64 bit) class. * Rounding modes offered are 'up', 'down', 'floor', 'ceiling', 'half_up', and 'half_down'. * Arithmetic operations are only permitted between numbers represented in exactly the same format. * A binary integer class is also implemented with its associated integer arithmetic and shift operations (no logical operations). * max, min, sqrt, power functions are defined for the appropriate numeric classes. See the following for details, and references therein: http://en.wikipedia.org/wiki/Floating_point http://en.wikipedia.org/wiki/Rounding http://en.wikipedia.org/wiki/IEEE_754 http://en.wikipedia.org/wiki/Binary_numeral_system ***** Known issues * Binary floating point arithmetic not simulated in its native form, but through internal use of arbitrary precision Decimal floating point numbers. It would be preferable to have a pure binary arithmetic implementation that demonstrates shifting of exponents, etc. before addition. * Does not directly simulate the algorithmic implementations by which IEEE 754 arithmetic and rounding is performed in CPUs. e.g. rounding up is *not* achieved by adding 0.5 and then truncating in this code. * Creation of higher precision floats is slow due to python implementation of frexp function. * Boolean operations on binary floating point numbers are not supported at this time. * Please note that float(Decimal("Inf")) will not work in Python 2.5 due to a problem in Python itself. This has been fixed in Python 2.6 and above (http://bugs.python.org/issue3188). * ContextClass does not support initialization from numpy float128 values. Needs additional code to extract byte-by-byte hex representation from the 'data' attribute of such a value, and conversion into equivalent binary string. * eval(repr(x)) when x is a ContextClass instance creates a Binary object with the same representation (fixed precision, rounding) as x, not another ContextClass instance. However, x == eval(repr(x)). * decimal context precision value must not be changed by the user to be less than the precision required by any ContextClass instances (Binary numbers of a fixed precision), otherwise arithmetic on those numbers may be inaccurate. In Python 2.5 and above, local context could be established for these calculations using the with statement (see here for implementation details: http://docs.python.org/lib/decimal-decimal.html). See binary_py25.py. * mod, floordiv, divmod methods are not supported. * Can be slow to evaluate expressions involving high precision values. * Can hash a binary context (ContextClass instance), but cannot hash a decimal.context instance. * Requires numpy to be installed, in order to use numpy.sign, numpy.zeros and provide compatibility with numpy.float32, numpy.float64, and numpy.float128 values. The sign function and array use provide better speed in key parts of the algorithms, but could be easily replaced to make numpy installation optional. * Does not support use of minifloats (IEEE-754 style formats with very low bit lengths for the exponent and characteristic) for integer-only representations. (This is a common application of such values, according to http://en.wikipedia.org/wiki/Minifloat.) ***** Usage ** Constructors: >>> context = define_context(5, 12, ROUND_DOWN) Equivalent binary float values in a given context: >>> x = Binary('-0.1111', context) # binary fraction assumed by default >>> x = Binary(Decimal("-0.9375"), context) # decimal fraction The following represent the same binary number (in context) but are a way to directly specify the representation in terms of the underlying context >>> a = context('1 01110 111000000000') # (sign, characteristic, significand) >>> a = context('101110111000000000') # (sign, characteristic, significand) These alternative forms are also valid, for convenience: >>> a = context(Decimal("-0.9375")) >>> a = context(Binary('-0.1111')) If the python float literal -0.9375 is exactly representable in the context, then this is also equivalent: >>> a = context(-0.9375) >>> a = context(numpy.float64(-0.9375)) Otherwise, the resulting representation in a will be to the "nearest" representable value under the rules of the context's precision and rounding mode. The values in a are instances of the context, and are not Binary class instances. Note that a context instance cannot be initialized directly using a string literal for a binary fraction, to avoid ambiguity with the primary use case, namely with input strings for (sign, characteristic, significand). The Binary constructor can also represent *arbitrary* precision binary values in the absence of a given context. After any of the above definitions of x: >>> x.context However: >>> y = Binary('0.110100101010101011110001111100001e5') >>> y.context is None True Note that infinite binary precision is not possible to specify from a Decimal object, in case the binary representation is non-terminating. >>> b = Binary(Decimal("0.1")) ValueError: Cannot create arbitrary precision binary value without a representation context ** Views: For a context instance x (not a Binary instance), we slightly break the tradition that eval(repr(x)) is identically the same type as x. However, eval(repr(x)) == x and the evaluation leads to a Binary object with the same context. >>> x = context(Decimal("-0.9375")) >>> x # default 'view' is as binary Binary("-0.1111E0", (5, 12, ROUND_DOWN)) >>> bx = eval(repr(x)) >>> bx == x True >>> bx.context >>> bx.context == x.context # bx really quacks like a duck True >>> x.as_binary() # output always in scientific notation with 0 before radix Binary("-0.1111E0") >>> x.as_binary() == bx # x.as_binary() keeps the same context True >>> x.as_decimal() Decimal("-0.9375") >>> print x 1 01110 111000000000 ** Comparisons: Can only compare like representations from a given context. >>> d = double(1) >>> q = quadruple(1) >>> d == q ValueError: Mismatched precision If you want to compare the actual values that these objects represent, convert them to a Binary number first >>> bd = Binary(double(1)) >>> bq = Binary(quadruple(1)) >>> bd == bq True >>> bd == 1.0 # cannot compare with python-native floats TypeError: Invalid object for comparison >>> bd > 1 False >>> bd == Decimal("1") True ** Conversions: For high precision floats with long mantissas, convert them accurately to Decimal type. Note that Decimal(str(f)) will not be accurate for floats f with mantissas longer than the displayable length of f by the str function. Thus float(Decimal(str(f))) != f for some f. To avoid this, create the representation of the float in the appropriate context by double(f) which will be a precise representation of f in double precision, where f can be a python float, numpy.float64. For numpy.float32 use single(f). Binary values in one context can be converted (coerced) to another thus: >>> xs = Binary('-1111.001', single) >>> xd = Binary(xs, double) or >>> xd = Binary(double(xs)) ** Arithmetic: Can only perform arithmetic on representations from a given context with other instances of the same context (including Binary instances with identical context). To perform arbitray precision arithmetic with mixed representations, first convert to Binary type, which does not care about the precision of the operands: the operation returns a new Binary value of the precise result, without context if neither operand had context, otherwise with the highest precision context. In a tie, arithmetic is not possible unless the rounding is the same. >>> bd = Binary(double(1.5)) >>> bq = Binary(quadruple(0.1)) >>> c = bd + bq >>> print c 0.11001100110011001100110011001100110011001100110011001101E1 >>> c.__class__ >>> c.context # coerced to the higher precision Also see test_simfloat.py for many more examples and validations. """ import numpy as npy import math import decimal from decimal import Decimal __all__ = ['Decimal', 'Binary', 'ContextClass', 'BinaryIntClass', 'quadruple', 'double', 'define_context', 'frexp', 'dec_context', 'single', 'half', 'test', 'binstr2dec', 'decfrac2binrep', 'decint2binstr', 'binfracstr2decfrac', 'dec2binstr', 'binvalstr2dec', 'ROUND_UP', 'ROUND_DOWN', 'ROUND_CEILING', 'ROUND_FLOOR', 'ROUND_HALF_UP', 'ROUND_HALF_DOWN', 'BinaryOverflow', 'BinaryUnderflow', 'BinaryOverflow', 'BinaryNegativeValue', 'BinaryRemainderValue', 'BinaryException'] # Rounding # up = always away from 0 ROUND_UP = 'ROUND_UP' # down = always towards 0 (truncate) ROUND_DOWN = 'ROUND_DOWN' # ceiling = always towards +inf ROUND_CEILING = 'ROUND_CEILING' # floor = always towards -inf ROUND_FLOOR = 'ROUND_FLOOR' # half up = to nearest, 0.1b (0.5d) rounds away from 0 (standard and default) ROUND_HALF_UP = 'ROUND_HALF_UP' # half down = to nearest, 0.1 (0.5d) rounds towards 0 ROUND_HALF_DOWN = 'ROUND_HALF_DOWN' _round_code = {ROUND_UP: 'U', ROUND_DOWN: 'D', ROUND_CEILING: 'C', ROUND_FLOOR: 'F', ROUND_HALF_UP: 'HU', ROUND_HALF_DOWN: 'HD'} dec_context = decimal.getcontext() dec_context.prec = 128 class BinaryIntClass(object): """Abstract class for non-negative binary integer values only, with a fixed number of digits given by class attribute 'digits'. Set digits in a concrete subclass. The initialization value string will cause an exception if the binary value is longer than the class' digits setting. """ digits = None def __init__(self, value): """Initialize with a value made up of a string of binary digits. """ dec_value = int(value, base=2) if dec_value < 0: raise BinaryNegativeValue("Invalid binary value: out of range of %d-digit number"%self.digits) elif dec_value > self.largest: raise BinaryOverflow("Invalid binary value: out of range of %d-digit number"%self.digits) self.bin_value = pad(value, self.digits) self.dec_value = Decimal(dec_value) self.tuple_rep = tuple([int(bit) for bit in self.bin_value]) def __repr__(self): return '%s("%s")' % (self.__class__.__name__, self.bin_value) def __str__(self): return self.bin_value def as_decimal(self): return self.dec_value def as_binary(self): return 'Binary("%s")' % self.bin_value def as_tuple(self): return self.tuple_rep def _op_return_class(self, other): try: other_digits = other.digits except AttributeError: raise TypeError("Invalid Binary object") if other_digits > self.digits: new_class = other.__class__ else: new_class = self.__class__ return new_class def max(self, other): return self.dec_value.max(other) def min(self, other): return self.dec_value.min(other) def __add__(self, other): new_class = self._op_return_class(other) result = self.dec_value + other.dec_value try: return new_class(decint2binstr(result)) except ValueError: raise BinaryOverflow(result) def __sub__(self, other): new_class = self._op_return_class(other) result = self.dec_value - other.dec_value if result < 0: raise BinaryNegativeValue(result) return new_class(decint2binstr(result)) def __mul__(self, other): new_class = self._op_return_class(other) result = self.dec_value * other.dec_value try: return new_class(decint2binstr(result)) except ValueError: raise BinaryOverflow(result) def __div__(self, other): new_class = self._op_return_class(other) result_dm = divmod(self.dec_value, other.dec_value) if result_dm[1] != 0: raise BinaryRemainderValue(result_dm) return new_class(decint2binstr(result_dm[0])) def __rshift__(self, y): result = self.dec_value >> y return self.__class__(decint2binstr(result)) def __lshift__(self, y): result = self.dec_value << y try: return self.__class__(decint2binstr(result)) except ValueError: raise BinaryOverflow(result) def __hash__(self): """Hash as per the precise decimal representation. """ return hash(self.dec_value) def next(self): try: return self.__class__(decint2binstr(int(self.bin_value, base=2) + 1)) except BinaryOverflow: raise BinaryOverflow(int(self.bin_value, base=2) + 1) def prev(self): try: return self.__class__(decint2binstr(int(self.bin_value, base=2) - 1)) except: raise BinaryNegativeValue(int(self.bin_value, base=2) - 1) def __hash__(self): return hash(repr(self)) def __getitem__(self, i): return int(self.bin_value[i]) ## could provide boolean operator methods but not needed for arithmetic def __reduce__(self): return (self.__class__, (repr(self),)) class SignBit(BinaryIntClass): digits = 1 largest = 1 class BinaryCharacteristic(BinaryIntClass): """Abstract class for floating point representation's characteristic, a.k.a. exponent. """ def __init__(self, value): """value is a binary string representatino of the exponent (so may be negative)""" BinaryIntClass.__init__(self, value) # interpretation-related attributes interp_dec_value = self.dec_value - self.bias if interp_dec_value < self.exp_lowest: raise BinaryNegativeValue("Invalid binary value: out of range of %d-digit number"%self.digits) if interp_dec_value > self.exp_largest: raise BinaryOverflow("Invalid binary value: out of range of %d-digit number"%self.digits) self.interp_dec_value = interp_dec_value def interpret(self, denorm=False): """Interpret raw binary value as a signed decimal integer. """ if denorm: # bias needs to be adjusted by 1 return self.interp_dec_value + 1 else: return self.interp_dec_value class BinarySignificand(BinaryIntClass): """Abstract class for floating point representation's significand, a.k.a. fraction, or mantissa. """ def interpret(self): """Interpret raw binary value as a decimal fraction. """ return binfracstr2decfrac(self.bin_value) class ContextClass(object): """Abstract class for immutable binary floating point number using (sign, characteristic, significand) representation. """ def __init__(self, value): """Initialize with decimal value (either of type 'float' or 'Decimal') or a binary digit string of total length = .digits or the same string with spaces between sign, characteristic, and significand elements (total length .digits+2). """ try: assert self.significandClass.digits is not None except AssertionError: raise NotImplementedError("Create concrete sub-type of ContextClass") # round_dir may be overwritten by init_from_dec # round_dir = None => no rounding needed # 'next' => use next towards +inf # 'prev' => use next towards -inf round_dir = None if isinstance(value, (float, npy.float64)): if not npy.isfinite(value): val_str = str(value) # val_str may be '1.#INF' rather than 'inf' on some platforms val_str = val_str.replace('.', '').replace('#', '').replace('1', '') round_dir = self.init_from_dec(Decimal(val_str)) else: if value < 0: fval = -value s = '1' else: fval = value s = '0' # extract full binary representation of the value # (which is not accessible from str(value) or repr(value) valstr = pad(decint2binstr(npy.int64( \ npy.array(fval).view(long))), 64) e = valstr[1:12] f = valstr[12:] denorm = int(e) == 0 and int(f) > 0 if denorm: bias = 1022 expo = Decimal(2)**Decimal(int(e, base=2) - bias) frac = binfracstr2decfrac(f) else: bias = 1023 expo = Decimal(2)**Decimal(int(e, base=2) - bias) frac = 1 + binfracstr2decfrac(f) try: round_dir = self.init_from_dec((-1)**int(s) * expo * frac) except: raise BinaryOverflow("Invalid representation for this class: %s"%value) elif isinstance(value, npy.float32): if not npy.isfinite(value): val_str = str(value) # val_str may be '1.#INF' rather than 'inf' on some platforms val_str = val_str.replace('.', '').replace('#', '').replace('1', '') round_dir = self.init_from_dec(Decimal(val_str)) else: if value < 0: fval = -value s = '1' else: fval = value s = '0' # extract full binary representation of the value # (which is not accessible from str(value) or repr(value) valstr = pad(decint2binstr(npy.int32( \ npy.array(fval).view(int))), 32) e = valstr[1:9] f = valstr[9:] denorm = int(e) == 0 and int(f) > 0 if denorm: bias = 126 expo = Decimal(2) ** Decimal(int(e, base=2) - bias) frac = binfracstr2decfrac(f) else: bias = 127 expo = Decimal(2) ** Decimal(int(e, base=2) - bias) frac = 1 + binfracstr2decfrac(f) try: round_dir = self.init_from_dec((-1)**int(s) * expo * frac) except: raise BinaryOverflow("Invalid representation for this class: %s"%value) elif isinstance(value, (int, long, npy.int64, npy.int32)): try: round_dir = self.init_from_dec(Decimal(value)) except OverflowError: raise BinaryOverflow() elif isinstance(value, Decimal): try: round_dir = self.init_from_dec(value) except OverflowError: raise BinaryOverflow() elif isinstance(value, Binary): try: round_dir = self.init_from_dec(value.dec) except OverflowError: raise BinaryOverflow() elif isinstance(value, str): if len(value) == self.digits: # e.g. "11101001111010000" s = value[0] e = value[1:self.characteristicClass.digits+1] f = value[self.characteristicClass.digits+1:] elif len(value) == self.digits + 2: # e.g. "1 1101 001111010000" try: s, e, f = value.split(' ') except: raise ValueError("Invalid string representation for this class: %s"%value) else: raise ValueError("Invalid string representation for this class: %s"%value) try: self.init_from_string(s, e, f) except: raise ValueError("Invalid string representation for this class: %s"%value) else: raise TypeError("Invalid initialization type") self.tuple_rep = tuple((self.signbit.as_tuple(), self.characteristic.as_tuple(), self.significand.as_tuple())) # treat special cases for 0, Inf, NaN, and denormalized values if self.characteristic.dec_value == 0: if self.significand.dec_value == 0: # zeros if self.signbit.dec_value == 0: self.dec_value = Decimal("0") self.bin_value = "0" else: self.dec_value = Decimal("-0") self.bin_value = "-0" else: # denormalized values self.dec_value = (-1)**self.signbit.dec_value * \ 2**(self.characteristic.interpret(denorm=True)) * \ (0 + self.significand.interpret()) # lazy determination of bin_value when it is requested # (slow calculation) self.bin_value = "" elif self.characteristic.dec_value == self.characteristic.largest: if self.significand.dec_value == 0: # +/- Inf if self.signbit.dec_value == 0: self.dec_value = Decimal("Inf") self.bin_value = "Inf" else: self.dec_value = Decimal("-Inf") self.bin_value = "-Inf" else: # NaN self.dec_value = Decimal("NaN") self.bin_value = "NaN" else: # normalized values self.dec_value = (-1)**self.signbit.dec_value * \ 2**(self.characteristic.interpret()) * \ (1 + self.significand.interpret()) # lazy determination of bin_value when it is requested # (slow calculation) self.bin_value = "" if round_dir is not None: new = getattr(self, round_dir)() self.signbit = new.signbit self.characteristic = new.characteristic self.significand = new.significand self.bin_value = new.bin_value self.dec_value = new.dec_value self.tuple_rep = new.tuple_rep # for compatibility with Binary attribute self.context = self.__class__ def init_from_string(self, s, char_str, mant_str): self.signbit = SignBit(s) self.characteristic = self.characteristicClass(char_str) self.significand = self.significandClass(mant_str) def init_from_dec(self, d): """d is a Decimal object""" if d._isnan(): self.signbit = SignBit('0') self.characteristic = \ self.characteristicClass("1"*self.characteristicClass.digits) self.significand = \ self.significandClass("1"*self.significandClass.digits) return None elif d._isinfinity() != 0: self.signbit = SignBit(str(bin_sign(d._isinfinity()))) self.characteristic = \ self.characteristicClass("1"*self.characteristicClass.digits) self.significand = \ self.significandClass("0"*self.significandClass.digits) return None if d < 0: s = '1' d = -d elif d == 0: self.signbit = SignBit('0') self.characteristic = \ self.characteristicClass("0"*self.characteristicClass.digits) self.significand = \ self.significandClass("0"*self.significandClass.digits) # no rounding, so return None return None else: s = '0' if d <= self.largest_denorm: bias = self.characteristicClass.bias-1 frac_leading = 0 else: bias = self.characteristicClass.bias frac_leading = 1 # acquire precise fraction, exponent (given that exponent must be # representable in this class) f_dec, e_dec = frexp(d, self) e_dec += bias # convert to binary and round to precision of significand i = 0 max_bits = self.characteristicClass.exp_largest + \ self.significandClass.digits + 2 bfrac = npy.zeros(max_bits, int) i_stop = max_bits not_seen_one = True # make sure we use extra precision (given that might adjust for # leading 0's) while f_dec > 0 and i < max_bits: f_dec *= 2 bit = int(f_dec) if not_seen_one and bit == 1: # first time seen a 1 know that we only need up to # significand's more digits + 2 (speed optimization) i_stop = i + self.significandClass.digits + 2 not_seen_one = False bfrac[i] = bit f_dec -= bit i += 1 if i >= i_stop: break # binary string representation of fraction to full precision f = ''.join([str(b) for b in bfrac[:i]]) if frac_leading == 1: try: one_pos = f.index('1') except ValueError: # f is all 0's pass else: # adjust for leading zeros f = f[one_pos+1:] e_dec -= one_pos+1 c = decint2binstr(e_dec) else: # denormalized, inevitable lost precision in f c = '0'*self.characteristicClass.digits f = '0'*abs(e_dec) + f #print len(f), f #print 2**Decimal(e_dec - bias) * (frac_leading + binfracstr2decfrac(f)) round_dir = None # default if len(c) < self.characteristicClass.digits: c = pad(c, self.characteristicClass.digits) if len(f) < self.significandClass.digits: f = pad(f, self.significandClass.digits, to_right=True) elif len(f) > self.significandClass.digits: next_bit = int(f[self.significandClass.digits]) try: remaining_bits = f[self.significandClass.digits+1:] except IndexError: remaining_bits = '0' else: if remaining_bits == '': remaining_bits = '0' round_dir = self._round(int(s), next_bit, int(remaining_bits) > 0) f = f[:self.significandClass.digits] self.signbit = SignBit(s) self.characteristic = self.characteristicClass(c) self.significand = self.significandClass(f) return round_dir def _round(self, sign, next_bit, non_zero_remainder): r = self.round_mode if r == ROUND_DOWN: return None elif r == ROUND_UP: if next_bit == 1 or non_zero_remainder: # > 0.0b in magnitude if sign == 0: return 'next' else: return 'prev' else: return None elif r == ROUND_HALF_UP: if next_bit == 1: # >= 0.1b in magnitude, round away from 0 if sign == 0: return 'next' else: return 'prev' else: return None elif r == ROUND_HALF_DOWN: if next_bit == 1: if non_zero_remainder: # > 0.1b in magnitude, round away from 0 if sign == 0: return 'next' else: return 'prev' else: # = 0.1b, round towards 0 return None else: return None elif r == ROUND_CEILING: if sign == 0: if next_bit == 1 or non_zero_remainder: # > 0.0b in magnitude, round towards +inf return 'next' else: return None else: return None elif r == ROUND_FLOOR: if sign == 1: if next_bit == 1 or non_zero_remainder: # > 0.0b in magnitude, round towards +inf return 'prev' else: return None else: return None else: raise ValueError("Invalid rounding type") def is_denormalized(self): return self.dec_value <= self.largest_denorm def as_tuple(self): return self.tuple_rep def as_binary(self): """Lazy evaluation of bin_value in case it's never used. This is done because the calculation is slow! """ if self.bin_value == "": self.bin_value = dec2binstr(self.dec_value, self) return Binary(self.bin_value, self.__class__) def as_decimal(self): return self.dec_value def __repr__(self): if self.bin_value == "": self.bin_value = dec2binstr(self.dec_value, self) return 'Binary("%s", (%s, %s, %s))' % \ (self.bin_value, self.characteristicClass.digits, self.significandClass.digits, self.round_mode) def __str__(self): return "%s %s %s" % (str(self.signbit), str(self.characteristic), str(self.significand)) def next(self): """Return the successive representable float value in the more positive direction. """ if self.signbit.dec_value == 1: method = 'prev' else: method = 'next' return self._step(method) def prev(self): """Return the successive representable float value in the more negative direction. """ if self.signbit.dec_value == 0: method = 'prev' else: method = 'next' return self._step(method) def _step(self, method): """ Internal method for stepping to successive representable values, either in the increasing or decreasing direction, according to the method passed. """ # default value, unless switches new_signbit = self.signbit try: new_signif = getattr(self.significand, method)() except BinaryNegativeValue: # CARRY try: new_char = getattr(self.characteristic, method)() except BinaryNegativeValue: # CHANGE SIGN new_char = self.characteristic new_signbit = SignBit(str(1-self.signbit.dec_value)) # have to reset new_signif in this case new_signif = self.significandClass(pad('1', self.significand.digits)) except BinaryOverflow: raise ValueError("No more representable values") else: new_signif = self.significandClass('1'*self.significand.digits) except BinaryOverflow: # CARRY # representation of one in the same number of significant digits new_signif = self.significandClass('0'*self.significand.digits) try: new_char = getattr(self.characteristic, method)() except BinaryOverflow: raise ValueError("No more representable values") else: new_char = self.characteristic return self.__class__(" ".join([str(new_signbit), str(new_char), str(new_signif)])) def _op_check(self, other): try: # may be another ContextClass with matching precision other_sig_digits = other.significand.digits other_char_digits = other.characteristic.digits except AttributeError: # may be a Binary object with matching precision try: other_sig_digits = other.context.significandClass.digits other_char_digits = other.context.characteristicClass.digits except AttributeError: raise TypeError("Invalid Context object") else: ox = other.dec c = other.context else: ox = other.dec_value c = other if other_sig_digits != self.significand.digits or \ other_char_digits != self.characteristic.digits: raise ValueError("Mismatched precision") elif c.round_mode != self.round_mode: raise ValueError("Mismatched rounding modes") return ox def __eq__(self, other): ox = self._op_check(other) return self.dec_value == ox def __ne__(self, other): ox = self._op_check(other) return self.dec_value != ox def __le__(self, other): ox = self._op_check(other) return self.dec_value <= ox def __lt__(self, other): ox = self._op_check(other) return self.dec_value < ox def __ge__(self, other): ox = self._op_check(other) return self.dec_value >= ox def __gt__(self, other): ox = self._op_check(other) return self.dec_value > ox def __neg__(self): return self.__class__(str(1-self.signbit.dec_value) + str(self)[1:]) def __abs__(self): return self.__class__('0' + str(self)[1:]) def __add__(self, other): ox = self._op_check(other) return self.__class__(self.dec_value + ox) __radd__ = __add__ def __sub__(self, other): ox = self._op_check(other) return self.__class__(self.dec_value - ox) def __rsub__(self, other): ox = self._op_check(other) return self.__class__(ox - self.dec_value) def __mul__(self, other): ox = self._op_check(other) return self.__class__(self.dec_value * ox) __rmul__ = __mul__ def __div__(self, other): ox = self._op_check(other) return self.__class__(self.dec_value / ox) def __rdiv__(self, other): ox = self._op_check(other) return self.__class__(ox / self.dec_value) __rtruediv__ = __rdiv__ def __pow__(self, other): ox = self._op_check(other) return self.__class__(self.dec_value ** ox) def __rpow__(self, other): ox = self._op_check(other) return self.__class__(ox ** self.dec_value) def sqrt(self): return self.__class__(self.dec_value.sqrt()) def __nonzero__(self): return self.dec_value != 0 def max(self, other): """Respects NaN and Inf""" ox = self._op_check(other) r = self.dec_value.max(ox) if r == self.dec_value: return self else: return other def min(self, other): """Respects NaN and Inf""" ox = self._op_check(other) r = self.dec_value.min(ox) if r == self.dec_value: return self else: return other def __copy__(self): if type(self) == ContextClass: return self # I'm immutable; therefore I am my own clone return self.__class__(str(self)) def __deepcopy__(self, memo): if type(self) == ContextClass: return self # My components are also immutable return self.__class__(str(self)) class context_registry(object): """Registry saves re-creating temporary copies of the same context classes during eval() calls, etc., by keeping a registry of all created contexts in the present session. """ def __init__(self): self.contexts = {} def __call__(self, char_digits, sig_digits, rounding=ROUND_HALF_UP): try: return self.contexts[(char_digits, sig_digits, rounding)] except KeyError: return self.make_context(char_digits, sig_digits, rounding) def make_context(self, char_digits, sig_digits, rounding): """Class factory for binary float arithmetic using the given number of digits for the characteristic (exponent) and significand (mantissa, or fraction). rounding type defaults to rounding up (away from 0). """ if rounding not in _round_code: raise ValueError("Invalid rounding type specified") class CharacteristicClass(BinaryCharacteristic): digits = char_digits largest = 2**char_digits-1 bias = 2**(char_digits-1)-1 exp_largest = 2**(char_digits-1) exp_lowest = -2**(char_digits-1) + 1 class SignificandClass(BinarySignificand): digits = sig_digits largest = 2**sig_digits-1 class context(ContextClass): characteristicClass = CharacteristicClass significandClass = SignificandClass largest_denorm = (Decimal(2) ** Decimal(-2**(char_digits-1)+2) ) * \ (1 - Decimal(2)**(-sig_digits)) largest_norm = (1 - Decimal("0.5")**(sig_digits+1)) * \ 2 ** (2**(char_digits - 1)) digits = 1 + char_digits + sig_digits round_mode = rounding Etop = 2**(char_digits-1) Etiny = -2**(char_digits-1) + 1 context.__name__ = "Float_%d_%d_%s" % (char_digits, sig_digits, _round_code[rounding]) self.contexts[(char_digits, sig_digits, rounding)] = context return context define_context = context_registry() single = define_context(8, 23) # IEEE 754 32 bit float double = define_context(11, 52) # IEEE 754 64 bit float quadruple = define_context(15, 112) # IEEE 754 128 bit float half = define_context(5, 10) # IEEE 754 16 bit float test = define_context(4, 6, ROUND_DOWN) # for learning purposes class Binary(object): """Constructor for binary floating point representation from a binary string or Decimal class representation of a real number: e.g. -1101.100011 or Decimal("-13.546875000"). The value may also be an instance of an existing context. If a context is given as a Context class, the return value will be an instance of that class (a IEEE 754 representation of that binary value). If the value given was from a different context it will be coerced. The context may also be provided as the tuple: (characteristic_digits, significand_digits, rounding mode string) although this is intended primarily for internal use or for eval(repr(x)) for a Context object x. Binary() is the same as Binary('0') to be functionally compatible with the Decimal class. """ def __init__(self, x='0', context=None): if isinstance(context, tuple): self.context = define_context(context[0], context[1], rounding=context[2]) else: self.context = context # placeholder for representation of x self.rep = None if isinstance(x, Binary): self.dec = x.dec self.bin = x.bin if context is None: self.context = x.context # in case x.context is not None (otherwise gets # overwritten by x.dec == x.rep anyway) self.rep = x.rep elif isinstance(x, ContextClass): if context is None: # optimization -- don't recreate self.rep later # from this context, keep it now. self.context = x.__class__ self.rep = x self.dec = x.as_decimal() self.bin = str(x.as_binary()) else: x_dec = x.as_decimal() if abs(x_dec) > self.context.largest_norm: if x_dec < 0: self.bin = "-Inf" seld.dec = Decimal("-Inf") else: self.bin = "Inf" self.dec = Decimal("Inf") self.rep = self.context(self.dec) elif isinstance(x, Decimal): self.dec = x if self.context is None: raise ValueError("Cannot create arbitrary precision binary " "value without a representation context") if x._isnan() or x._isinfinity() != 0: if x._isnan(): self.bin = "NaN" else: if x < 0: self.bin = "-Inf" else: self.bin = "Inf" elif abs(x) > self.context.largest_norm: if x < 0: self.dec = Decimal("-Inf") self.bin = "-Inf" else: self.dec = Decimal("Inf") self.bin = "Inf" else: self.bin = dec2binstr(x, self.context) else: # string bstr = x.lower() if bstr in ['-inf', 'inf', 'nan']: self.bin = x self.dec = Decimal(x) else: self.bin = x self.dec = binvalstr2dec(bstr) if self.context is None: self.rep = self.dec else: if self.rep is None: try: self.rep = self.context(self.dec) except BinaryOverflow: if self.dec < 0: self.dec = Decimal("-Inf") self.bin = "-Inf" else: self.dec = Decimal("Inf") self.bin = "Inf" self.rep = self.context(self.dec) self.dec = self.rep.dec_value if self.rep.bin_value == "": # lazy evaluation hasn't been performed yet self.bin = dec2binstr(self.dec, self.rep) # might as well update the representation self.rep.bin_value = self.bin else: self.bin = self.rep.bin_value def __hash__(self): return hash(self.rep) def __str__(self): return self.bin def __repr__(self): if self.context: return 'Binary("%s", (%d, %d, %s))' % (self.bin, self.context.characteristicClass.digits, self.context.significandClass.digits, self.context.round_mode) else: return 'Binary("%s")' % self.bin def as_binary(self): return self def as_decimal(self): return self.dec def _op_check(self, other): if isinstance(other, (int, long, npy.int32, npy.int64)): ox, c = Decimal(other), None elif isinstance(other, Binary): ox, c = other.dec, other.context elif isinstance(other, Decimal): ox, c = other, None elif isinstance(other, ContextClass): # ContextClass is strict about comparing only with others # of the same representation, so ensure self is if self.context == other.__class__: ox, c = other.as_decimal(), other.__class__ else: raise TypeError("Invalid object for comparison") else: raise TypeError("Invalid object for comparison") if self.context: s_digits = self.context.digits else: s_digits = 0 if c: c_digits = c.digits else: c_digits = 0 if s_digits > c_digits: ctx = self.context else: if s_digits > 0 and s_digits == c_digits: # contexts have the same precision, but what about # rounding? if self.context.round_mode == c.round_mode: ctx = c else: raise ValueError("Clash of rounding modes for " "equal-precision comparison") else: ctx = c return ox, ctx def __eq__(self, other): ox, c = self._op_check(other) return self.dec == ox def __ne__(self, other): ox, c = self._op_check(other) return self.dec != ox def __le__(self, other): ox, c = self._op_check(other) return self.dec <= ox def __lt__(self, other): ox, c = self._op_check(other) return self.dec < ox def __ge__(self, other): ox, c = self._op_check(other) return self.dec >= ox def __gt__(self, other): ox, c = self._op_check(other) return self.dec > ox def __neg__(self): return self.__class__(-self.rep) def __abs__(self): return self.__class__(abs(self.rep)) def __add__(self, other): ox, ctx = self._op_check(other) return self.__class__(self.dec + ox, ctx) __radd__ = __add__ def __sub__(self, other): ox, ctx = self._op_check(other) return self.__class__(self.dec - ox, ctx) def __rsub__(self, other): ox, ctx = self._op_check(other) return self.__class__(ox - self.dec, ctx) def __mul__(self, other): ox, ctx = self._op_check(other) return self.__class__(self.dec * ox, ctx) __rmul__ = __mul__ def __div__(self, other): ox, ctx = self._op_check(other) return self.__class__(self.dec / ox, ctx) def __rdiv__(self, other): ox, ctx = self._op_check(other) return self.__class__(ox / self.dec, ctx) __rtruediv__ = __rdiv__ def __pow__(self, other): ox, ctx = self._op_check(other) return self.__class__(self.dec ** ox, ctx) def __rpow__(self, other): ox, ctx = self._op_check(other) return self.__class__(ox ** self.dec, ctx) def __nonzero__(self): return self.dec != 0 def sqrt(self): return self.__class__(self.dec.sqrt(), self.context) def max(self, other): """Respects NaN and Inf""" ox, ctx = self._op_check(other) r = self.dec.max(ox) if r == self.dec: return self else: return other def min(self, other): """Respects NaN and Inf""" ox, ctx = self._op_check(other) r = self.dec.min(ox) if r == self.dec: return self else: return other def __reduce__(self): return (self.__class__, (repr(self),)) def __copy__(self): if type(self) == Binary: return self # I'm immutable; therefore I am my own clone return self.__class__(str(self)) def __deepcopy__(self, memo): if type(self) == Binary: return self # My components are also immutable return self.__class__(str(self)) def binvalstr2dec(x): """Convert signed real numbers in binary string form to decimal value (no special values Inf, NaN), including values in scientific notation. """ if not isbinstr(x): raise ValueError("Invalid string representation of binary" " float: %s" % x) if x[0] == '-': x = x[1:] sign = -1 else: sign = 1 if 'e' in x: x, estr = x.split('e') e = int(estr) elif 'E' in x: x, estr = x.split('E') e = int(estr) else: e = 0 if '.' in x: try: whole, frac = x.split('.') except ValueError: raise ValueError("Invalid string representation of binary" " float") else: if frac == "": frac = '0' if whole == "": whole = '0' else: whole = x frac = '0' try: dec_whole = Decimal(int(whole, base=2)) * Decimal(2)**e except ValueError: dec_whole = Decimal(0) dec_frac = binfracstr2decfrac(frac) * Decimal(2)**e return sign*(dec_whole+dec_frac) def isbinstr(arg): # supports unary + / - at front, and checks for usage of exponentials # (using 'E' or 'e') s = arg.lower() try: if s[0] in ['+','-']: s_rest = s[1:] else: s_rest = s except IndexError: return False if '0' not in s_rest and '1' not in s_rest: return False pts = s.count('.') exps = s.count('e') pm = s_rest.count('+') + s_rest.count('-') if pts > 1 or exps > 1 or pm > 1: return False if exps == 1: exp_pos = s.find('e') pre_exp = s[:exp_pos] # must be numbers before and after the 'e' if not npy.sometrue([n in ('0','1') for n in pre_exp]): return False if s[-1]=='e': # no chars after 'e'! return False if not npy.sometrue([n in ('0','1','2','3','4','5','6','7','8','9') \ for n in s[exp_pos:]]): return False # check that any additional +/- occurs directly after 'e' if pm == 1: pm_pos = max([s_rest.find('+'), s_rest.find('-')]) if s_rest[pm_pos-1] != 'e': return False e_rest = s_rest[pm_pos+1:] # safe due to previous check s_rest = s_rest[:pm_pos+1] else: e_rest = s[exp_pos+1:] s_rest = s[:exp_pos+1] # only remaining chars in s after e and possible +/- are numbers if '.' in e_rest: return False # cannot use additional +/- if not using exponent if pm == 1 and exps == 0: return False return npy.alltrue([n in ('0', '1', '.', 'e', '+', '-') for n in s_rest]) def binstr2dec(bstr): """Convert binary string representation of an integer to a decimal integer. """ return int(bstr, base=2) def decint2binstr(n): """Convert decimal integer to binary string. """ if n < 0: return '-' + decint2binstr(-n) s = '' while n != 0: s = str(n % 2) + s n >>= 1 return s or '0' def binfracstr2decfrac(bstr): """Convert non-negative binary string fraction (without radix) to decimal fraction. e.g. to convert ".1101", binfracstr2decfrac('1101') -> Decimal("0.8125") """ assert bstr[0] != '-', "Only pass non-negative values" dec_value = 0 half = Decimal("0.5") for place, bit in enumerate(bstr): if int(bit) == 1: dec_value += half**(place+1) return dec_value def frexp(d, context): """Implementation of 'frexp' for arbitrary precision decimals. Result is a pair F, E, where 0 < F < 1 is a Decimal object, and E is a signed integer such that d = F * 2**E context specifies the maximum absolute value of the exponent to ensure termination of the calculation. e.g. for double precision pass the 'double' Context class which defines a characteristic of 11 bits, with maximum exponent size of 1024. """ e_largest = context.characteristicClass.exp_largest if d < 0: res = frexp(-d, e_largest) return -res[0], res[1] elif d == 0: return Decimal("0"), 0 elif d >= 1: w_dec = int(d) e_dec = 0 while w_dec > 0 and abs(e_dec) <= e_largest: d /= 2 w_dec = int(d) e_dec += 1 return d, e_dec else: # 0 < d < 1 w_dec = 0 e_dec = 0 while w_dec == 0 and abs(e_dec) <= e_largest: w_dec = int(d*2) if w_dec > 0: break else: d *= 2 e_dec -= 1 return d, e_dec def dec2binstr(x, context=None): if x < 0: signstr = '-' valstr = -x elif x == 0: return '0.0' else: signstr = '' valstr = x # convert to binary fraction representation e, f = decfrac2binrep(valstr, context) return signstr + '0.' + f + 'E' + str(int(e, base=2)) def decfrac2binrep(x, context): """Convert positive decimal float to the nearest representable " " binary string representation (natural format, where characteristic may be negative), using an exponent no bigger than permitted in the given context. """ assert x > 0, "Provide only positive Decimal float value" assert isinstance(x, Decimal), "Provide only positive Decimal float value" fraction, exponent = frexp(x, context) max_bits = context.characteristicClass.exp_largest + \ context.significandClass.digits bfrac = npy.zeros(max_bits, int) i = 0 i_stop = max_bits not_seen_one = True while fraction > 0 and i < max_bits: fraction *= 2 bit = int(fraction) if not_seen_one and bit == 1: # speed optimization i_stop = i + context.significandClass.digits + 2 bfrac[i] = bit fraction -= bit i += 1 if i >= i_stop: break # negative exponents OK for this usage return decint2binstr(exponent), "".join([str(bit) for \ bit in bfrac[:i]]) def pad(value, digits, to_right=False): """Only use for positive binary numbers given as strings. Pads to the left by default, or to the right using to_right flag. Inputs: value -- string of bits digits -- number of bits in representation to_right -- Boolean, direction of padding Output: string of bits of length 'digits' Raises exception if value is larger than digits in length. Example: pad('0010', 6) -> '000010' pad('0010', 6, True) -> '001000' """ len_val = len(value) assert len_val <= digits rem_digits = digits - len_val if to_right: return value + "0"*rem_digits else: return "0"*rem_digits + value def bin_sign(x): """Binary representation of sign: x < 0 is represented as 1, 0 otherwise. """ s=npy.sign(x) if s >= 0: return 0 else: return 1 ## exceptions class BinaryException(ArithmeticError): def __init__(self, value=None): self.value = value self.code = None def __str__(self): return repr(self.value) def __repr__(self): return repr(self.value) class BinaryOverflow(BinaryException): pass class BinaryUnderflow(BinaryException): pass class BinaryNegativeValue(BinaryException): pass class BinaryRemainderValue(BinaryException): pass