Увеличьте значение с плавающей запятой Python на минимально возможную величину - PullRequest
59 голосов
/ 19 мая 2011

Я использую значения с плавающей точкой в ​​качестве словарных ключей.

Иногда очень иногда (и, возможно, никогда, но не обязательно никогда), будут конфликты.Я хотел бы разрешить их, увеличив значение с плавающей запятой на минимально возможное значение.Как я могу это сделать?

В C я бы вертел биты мантиссы, чтобы достичь этого, но я предполагаю, что это невозможно в python.

Ответы [ 14 ]

86 голосов
/ 28 мая 2011

Увеличьте значение с плавающей запятой Python на наименьшую возможную сумму

Вы не сумасшедший, и вы должны быть в состоянии это сделать.К сожалению, это текущий недостаток математической библиотеки Python как в Python 2.X, так и в Python3000.В Python должно быть math.nextafter(x,y), но его нет.Было бы тривиально добавить, так как большинство компиляторов Си имеют функции.

Функции nextafter (x, y) возвращают следующее дискретно другое представимое значение с плавающей точкой, следующее за x в направлении y.Функции nextafter () гарантированно работают на платформе или возвращают разумное значение, чтобы указать, что следующее значение невозможно.

Функции nextafter() являются частью стандартов POSIX и ISO C99 и _nextafter () в Visual C .Стандартные математические библиотеки, совместимые с C99, Visual C, C ++, Boost и Java все реализуют рекомендуемые IEEE функции или методы nextafter ().(Я честно не знаю, есть ли в .NET nextafter (). Microsoft не особо заботится о C99 или POSIX.)

Поскольку Python, похоже, движется в направлении поддержки большинства математических функций и поведений C99 дляматематический модуль, исключение nextafter() любопытно.К счастью, есть простые обходные пути.

Ни одна из функций перестановки битов здесь полностью или правильно не обрабатывает граничные случаи, такие как значения, проходящие через 0,0, отрицательные 0,0, субнормальные, бесконечности, отрицательные значения, переполнения или недостаточные значения и т. Д.. Вот эталонная реализация nextafter () в C , чтобы дать представление о том, как выполнить правильное переключение битов, если это ваше направление.

Есть два надежных обходных пути для получения nextafter() или других исключенных математических функций POSIX в Python:

Использование Numpy:

>>> import numpy
>>> numpy.nextafter(0,1)
4.9406564584124654e-324
>>> numpy.nextafter(.1, 1)
0.10000000000000002
>>> numpy.nextafter(1e6, -1)
999999.99999999988
>>> numpy.nextafter(-.1, 1)
-0.099999999999999992

Ссылка непосредственно на системную математическую DLL:

import ctypes
import sys
from sys import platform as _platform

if _platform == "linux" or _platform == "linux2":
    _libm = ctypes.cdll.LoadLibrary('libm.so.6')
    _funcname = 'nextafter'
elif _platform == "darwin":
    _libm = ctypes.cdll.LoadLibrary('libSystem.dylib')
    _funcname = 'nextafter'
elif _platform == "win32":
    _libm = ctypes.cdll.LoadLibrary('msvcrt.dll')
    _funcname = '_nextafter'
else:
    # these are the ones I have access to...
    # fill in library and function name for your system math dll
    print "Platform", repr(_platform), "is not supported"
    sys.exit(0)

_nextafter = getattr(_libm, _funcname)
_nextafter.restype = ctypes.c_double
_nextafter.argtypes = [ctypes.c_double, ctypes.c_double]

def nextafter(x, y):
    "Returns the next floating-point number after x in the direction of y."
    return _nextafter(x, y)

assert nextafter(0, 1) - nextafter(0, 1) == 0
assert 0.0 + nextafter(0, 1) > 0.0

А если вы действительно действительно хотите чисто Python-решение:

# handles edge cases correctly on MY computer 
# not extensively QA'd...
import math
# 'double' means IEEE 754 double precision -- c 'double'
epsilon  = math.ldexp(1.0, -53) # smallest double that 0.5+epsilon != 0.5
maxDouble = float(2**1024 - 2**971)  # From the IEEE 754 standard
minDouble  = math.ldexp(1.0, -1022) # min positive normalized double
smallEpsilon  = math.ldexp(1.0, -1074) # smallest increment for doubles < minFloat
infinity = math.ldexp(1.0, 1023) * 2

def nextafter(x,y):    
    """returns the next IEEE double after x in the direction of y if possible"""
    if y==x:
       return y         #if x==y, no increment

    # handle NaN
    if x!=x or y!=y:
        return x + y       

    if x >= infinity:
        return infinity

    if x <= -infinity:
        return -infinity

    if -minDouble < x < minDouble:
        if y > x:
            return x + smallEpsilon
        else:
            return x - smallEpsilon  

    m, e = math.frexp(x)        
    if y > x:
        m += epsilon
    else:
        m -= epsilon

    return math.ldexp(m,e)

Или, используйте Марк Дикинсон превосходное решение

Очевидно, что решение Numpy является самым простым.

9 голосов
/ 19 мая 2011

Во-первых, эта «реакция на столкновение» - довольно плохая идея.

Если они сталкиваются, значения в словаре должны были быть списками элементов с общим ключом, а не отдельными элементами.

Ваш алгоритм «зондирования хеша» должен будет проходить более одного «крошечного приращения» для разрешения коллизий.

А последовательные хэш-зонды, как известно, неэффективны.

Читать это: http://en.wikipedia.org/wiki/Quadratic_probing

Во-вторых, используйте math.frexp и sys.float_info.epsilon, чтобы поиграть с мантиссой и показателем степени отдельно.

>>> m, e = math.frexp(4.0)
>>> (m+sys.float_info.epsilon)*2**e
4.0000000000000018
6 голосов
/ 19 мая 2011

Я не рекомендую полагать, что значения с плавающей точкой (или метки времени) будут уникальными, если это вообще возможно. Используйте итератор подсчета, последовательность базы данных или другой сервис для выдачи уникальных идентификаторов.

6 голосов
/ 19 мая 2011
import sys
>>> sys.float_info.epsilon
2.220446049250313e-16
6 голосов
/ 19 мая 2011

Для увеличения значения просто используйте кортеж для сталкивающейся клавиши.Если вам нужно сохранить их в порядке, каждый ключ должен быть кортежем, а не просто дубликатами.

4 голосов
/ 27 мая 2011

Лучший ответ (сейчас я просто делаю это ради забавы ...), мотивированный переворотом. Обработка переноса и переполнения между частями числа отрицательных значений несколько сложна.

import struct

def floatToieee754Bits(f):
    return struct.unpack('<Q', struct.pack('<d', f))[0]

def ieee754BitsToFloat(i):
    return struct.unpack('<d', struct.pack('<Q', i))[0]

def incrementFloat(f):
    i = floatToieee754Bits(f)
    if f >= 0:
        return ieee754BitsToFloat(i+1)
    else:
        raise Exception('f not >= 0: unsolved problem!')
4 голосов
/ 26 мая 2011

Забыв о почему мы бы хотели на минуту увеличить значение с плавающей запятой, я должен был бы сказать, что я думаю, что собственный ответ Autopulated, вероятно, правильный.

Но для проблемной областиЯ разделяю опасения большинства респондентов относительно идеи использования чисел с плавающей запятой в качестве словарных ключей.Если возражение против использования десятичной дроби (как предлагается в основных комментариях) состоит в том, что это «тяжеловесное» решение, я предлагаю компромисс «сделай сам»: выясните, каково практическое разрешение на метках времени, выберите количество цифрчтобы адекватно его охватить, умножьте все временные метки на необходимую величину, чтобы вы могли использовать целые числа в качестве ключей.Если вы можете позволить себе дополнительную цифру или две сверх точности таймера, то вы можете быть еще более уверены, что не будет или будет меньше коллизий, и что, если есть коллизии, вы можете просто добавить 1 (вместо некоторого ригамарола, чтобы найтиследующее значение с плавающей запятой).

3 голосов
/ 26 мая 2011

Для сталкивающегося ключа k , добавьте: k / 2 50


Интересная проблема. Сумма, которую нужно добавить, очевидно, зависит от величины встречного значения, так что нормализованное прибавление повлияет только на младшие значащие биты.

Нет необходимости определять наименьшее значение, которое может быть добавлено. Все, что вам нужно сделать, это приблизить его. Формат FPU обеспечивает 52 бита мантиссы плюс скрытый бит для 53 бит точности. Ни одна физическая константа не известна нигде вблизи этого уровня точности. Ни один датчик не может измерить что-либо рядом с ним. Так что у вас нет особых проблем.

В большинстве случаев для ключа k вы можете добавить k / 2 53 , из-за этой 52-битной дроби плюс скрытый бит.

Но не обязательно рисковать, вызывая баги библиотек или исследуя проблемы округления, стреляя в последний бит или что-то рядом с ним.

Итак, я бы сказал, для коллизионного ключа k , просто добавьте k / 2 50 и назовите его в день.


1. Возможно, несколько раз, пока он больше не столкнется, по крайней мере, чтобы помешать авторам дьявольских юнит-тестов.

3 голосов
/ 20 мая 2011

Вместо изменения вашей временной метки с плавающей запятой используйте кортеж для каждого ключа, так как Марк Рэнсом предлагает , где кортеж (x,y) состоит из x=your_unmodified_time_stamp и y=(extremely unlikely to be a same value twice).

Итак:

  1. x просто является неизмененной отметкой времени и может иметь одно и то же значение много раз;
  2. y, которую вы можете использовать:
    1. случайное целое число из большого диапазона,
    2. последовательное целое число (0,1,2 и т. Д.),
    3. UUID .

Хотя 2.1 (random int из большого диапазона) прекрасно работает для Ethernet, я бы использовал 2.2 (сериализатор) или 2.3 (UUID).Легко, быстро, пуленепробиваемо.Для 2.2 и 2.3 вам даже не нужно обнаружение коллизий (вы можете захотеть использовать его для 2.1, как это делает Ethernet).

Преимущество 2.2 состоит в том, что вы также можете определять и сортировать элементы данных, которыеиметь одинаковую метку времени с плавающей точкой.

Затем просто извлеките x из кортежа для любых операций типа сортировки, и сам кортеж является свободным от коллизий ключом для хеша / словаря.

Редактировать

Я думаю, пример кода поможет:

#!/usr/bin/env python

import time
import sys
import random

#generator for ints from 0 to maxinteger on system:
serializer=(sn for sn in xrange(0,sys.maxint))

#a list with guranteed collisions:
times=[]
for c in range(0,35):
   t=time.clock()
   for i in range(0,random.choice(range(0,4))):
      times.append(t)

print len(set(times)), "unique items in a list of",len(times)      

#dictionary of tuples; no possibilities of collisions:
di={}   
for time in times:
    sn=serializer.next()
    di[(time,sn)]='Element {}'.format(sn)

#for tuples of multiple numbers, Python sorts
# as you expect: first by t[0] then t[1], until t[n]
for key in sorted(di.keys()):
    print "{:>15}:{}".format(key, di[key]) 

Вывод:

26 unique items in a list of 55
  (0.042289, 0):Element 0
  (0.042289, 1):Element 1
  (0.042289, 2):Element 2
  (0.042305, 3):Element 3
  (0.042305, 4):Element 4
  (0.042317, 5):Element 5
  # and so on until Element n...
2 голосов
/ 26 мая 2011

Вот она часть этого.Это грязно и медленно, но, может быть, вам так нравится.В нем пропущено несколько угловых случаев, но, возможно, это приблизит кого-то еще.

Идея состоит в том, чтобы получить шестнадцатеричную строку числа с плавающей точкой.Это дает вам строку с битами мантиссы и экспоненты, чтобы вертеться.Тидлинг - это боль, так как вы должны делать все это вручную и продолжать конвертировать в / из строк.В любом случае, вы добавляете (вычитаете) 1 к (из) последней цифре для положительных (отрицательных) чисел.Убедитесь, что вы переносите на экспоненту, если вы переполнены.Отрицательные числа немного сложнее, чтобы вы не теряли ничего.

def increment(f):
    h = f.hex()
    # decide if we need to increment up or down
    if f > 0:
        sign = '+'
        inc = 1
    else:
        sign = '-'
        inc = -1
    # pull the string apart
    h = h.split('0x')[-1]
    h,e = h.split('p')
    h = ''.join(h.split('.'))
    h2 = shift(h, inc)
    # increase the exponent if we added a digit
    h2 = '%s0x%s.%sp%s' % (sign, h2[0], h2[1:], e)
    return float.fromhex(h2)

def shift(s, num):
    if not s:
        return ''
    right = s[-1]
    right = int(right, 16) + num
    if right > 15:
        num = right // 16
        right = right%16
    elif right < 0:
        right = 0
        num = -1
    else:
        num = 0
    # drop the leading 0x
    right = hex(right)[2:]
    return shift(s[:-1], num) + right

a = 1.4e4
print increment(a) - a
a = -1.4e4
print increment(a) - a

a = 1.4
print increment(a) - a
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...