Skip to content

Commit ff6dadd

Browse files
committed
move ulp and almosteq to new module unpythonic.numutil
They are still available in the top-level namespace, as usual.
1 parent d191fbe commit ff6dadd

File tree

7 files changed

+116
-95
lines changed

7 files changed

+116
-95
lines changed

unpythonic/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from .llist import * # noqa: F401, F403
3636
from .mathseq import * # noqa: F401, F403
3737
from .misc import * # noqa: F401, F403
38+
from .numutil import * # noqa: F401, F403
3839
from .seq import * # noqa: F401, F403
3940
from .singleton import * # noqa: F401, F403
4041
from .slicing import * # noqa: F401, F403

unpythonic/mathseq.py

Lines changed: 1 addition & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020

2121
__all__ = ["s", "imathify", "gmathify",
2222
"m", "mg", # old names, pre-0.14.3, will go away in 0.15.0
23-
"almosteq",
2423
"sadd", "ssub", "sabs", "spos", "sneg", "sinvert", "smul", "spow",
2524
"struediv", "sfloordiv", "smod", "sdivmod",
2625
"sround", "strunc", "sfloor", "sceil",
@@ -45,13 +44,13 @@
4544

4645
from .it import take, rev, window
4746
from .gmemo import imemoize, gmemoize
47+
from .numutil import almosteq
4848

4949
class _NoSuchType:
5050
pass
5151

5252
# stuff to support float, mpf and SymPy expressions transparently
5353
#
54-
from sys import float_info
5554
from math import log as math_log, copysign, trunc, floor, ceil
5655
try:
5756
from mpmath import mpf, almosteq as mpf_almosteq
@@ -98,44 +97,6 @@ def sign(x):
9897
sign = _numsign
9998
_symExpr = _NoSuchType
10099

101-
# TODO: Overhaul `almosteq` in v0.15.0, should work like mpf for consistency.
102-
# TODO: Also move it to `unpythonic.misc`, where `ulp` already is. Or make a `numutil`.
103-
def almosteq(a, b, tol=1e-8):
104-
"""Almost-equality that supports several formats.
105-
106-
The tolerance ``tol`` is used for the builtin ``float`` and ``mpmath.mpf``.
107-
108-
For ``mpmath.mpf``, we just delegate to ``mpmath.almosteq``, with the given
109-
``tol``. For ``float``, we use the strategy suggested in:
110-
111-
https://floating-point-gui.de/errors/comparison/
112-
113-
Anything else, for example SymPy expressions, strings, and containers
114-
(regardless of content), is tested for exact equality.
115-
116-
**CAUTION**: Although placed in ``unpythonic.mathseq``, this function
117-
**does not** support iterables; rather, it is a low-level tool that is
118-
exposed in the public API in the hope it may be useful elsewhere.
119-
"""
120-
if a == b: # infinities and such, plus any non-float type
121-
return True
122-
123-
if isinstance(a, mpf) and isinstance(b, mpf):
124-
return mpf_almosteq(a, b, tol)
125-
# compare as native float if only one is an mpf
126-
elif isinstance(a, mpf) and isinstance(b, (float, int)):
127-
a = float(a)
128-
elif isinstance(a, (float, int)) and isinstance(b, mpf):
129-
b = float(b)
130-
131-
if not all(isinstance(x, (float, int)) for x in (a, b)):
132-
return False # non-float type, already determined that a != b
133-
min_normal = float_info.min
134-
max_float = float_info.max
135-
d = abs(a - b)
136-
if a == 0 or b == 0 or d < min_normal:
137-
return d < tol * min_normal
138-
return d / min(abs(a) + abs(b), max_float) < tol
139100

140101
def s(*spec):
141102
"""Create a lazy mathematical sequence.

unpythonic/misc.py

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,14 @@
55
"pack", "namelambda", "timer",
66
"getattrrec", "setattrrec",
77
"Popper", "CountingIterator",
8-
"ulp",
98
"slurp", "async_raise", "callsite_filename", "safeissubclass"]
109

1110
from copy import copy
1211
from functools import partial
1312
from itertools import count
1413
import inspect
15-
from math import floor, log2
1614
from queue import Empty
17-
from sys import float_info, version_info
15+
from sys import version_info
1816
import threading
1917
from time import monotonic
2018
from types import CodeType, FunctionType, LambdaType, TracebackType
@@ -654,21 +652,6 @@ def __next__(self):
654652
self.count += 1
655653
return x
656654

657-
# TODO: move to a new module unpythonic.numutil in v0.15.0.
658-
def ulp(x): # Unit in the Last Place
659-
"""Given a float x, return the unit in the last place (ULP).
660-
661-
This is the numerical value of the least-significant bit, as a float.
662-
For x = 1.0, the ULP is the machine epsilon (by definition of machine epsilon).
663-
664-
See:
665-
https://en.wikipedia.org/wiki/Unit_in_the_last_place
666-
"""
667-
eps = float_info.epsilon
668-
# m_min = abs. value represented by a mantissa of 1.0, with the same exponent as x has
669-
m_min = 2**floor(log2(abs(x)))
670-
return m_min * eps
671-
672655
def slurp(queue):
673656
"""Slurp all items currently on a queue.Queue into a list.
674657

unpythonic/numutil.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# -*- coding: utf-8 -*-
2+
"""Low-level utilities for numerics."""
3+
4+
__all__ = ["almosteq", "ulp"]
5+
6+
from math import floor, log2
7+
import sys
8+
9+
class _NoSuchType:
10+
pass
11+
12+
try:
13+
from mpmath import mpf, almosteq as mpf_almosteq
14+
except ImportError: # pragma: no cover, optional at runtime, but installed at development time.
15+
# Can't use a gensym here since `mpf` must be a unique *type*.
16+
mpf = _NoSuchType
17+
mpf_almosteq = None
18+
19+
20+
# TODO: Overhaul `almosteq` in v0.15.0, should work like mpf for consistency.
21+
def almosteq(a, b, tol=1e-8):
22+
"""Almost-equality that supports several formats.
23+
24+
The tolerance ``tol`` is used for the builtin ``float`` and ``mpmath.mpf``.
25+
26+
For ``mpmath.mpf``, we just delegate to ``mpmath.almosteq``, with the given
27+
``tol``. For ``float``, we use the strategy suggested in:
28+
29+
https://floating-point-gui.de/errors/comparison/
30+
31+
Anything else, for example SymPy expressions, strings, and containers
32+
(regardless of content), is tested for exact equality.
33+
"""
34+
if a == b: # infinities and such, plus any non-float type
35+
return True
36+
37+
if isinstance(a, mpf) and isinstance(b, mpf):
38+
return mpf_almosteq(a, b, tol)
39+
# compare as native float if only one is an mpf
40+
elif isinstance(a, mpf) and isinstance(b, (float, int)):
41+
a = float(a)
42+
elif isinstance(a, (float, int)) and isinstance(b, mpf):
43+
b = float(b)
44+
45+
if not all(isinstance(x, (float, int)) for x in (a, b)):
46+
return False # non-float type, already determined that a != b
47+
min_normal = sys.float_info.min
48+
max_float = sys.float_info.max
49+
d = abs(a - b)
50+
if a == 0 or b == 0 or d < min_normal:
51+
return d < tol * min_normal
52+
return d / min(abs(a) + abs(b), max_float) < tol
53+
54+
55+
def ulp(x): # Unit in the Last Place
56+
"""Given a float x, return the unit in the last place (ULP).
57+
58+
This is the numerical value of the least-significant bit, as a float.
59+
For x = 1.0, the ULP is the machine epsilon (by definition of machine epsilon).
60+
61+
See:
62+
https://en.wikipedia.org/wiki/Unit_in_the_last_place
63+
"""
64+
eps = sys.float_info.epsilon
65+
# m_min = abs. value represented by a mantissa of 1.0, with the same exponent as x has
66+
m_min = 2**floor(log2(abs(x)))
67+
return m_min * eps

unpythonic/tests/test_mathseq.py

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@
55

66
from operator import mul
77
from math import exp, trunc, floor, ceil
8-
from sys import float_info
98

109
from ..mathseq import (s, imathify, gmathify,
1110
sadd, smul, spow, cauchyprod,
1211
primes, fibonacci,
13-
sign, log, almosteq)
12+
sign, log)
1413
from ..it import take, last
1514
from ..fold import scanl
1615
from ..gmemo import imemoize
@@ -46,31 +45,6 @@ def runtests():
4645
x = symbols("x", positive=True)
4746
test[log(symbolicExp(x)) == x]
4847

49-
with testset("almosteq"):
50-
# For anything but floating-point inputs, it's exact equality.
51-
test[almosteq("abc", "abc")]
52-
test[not almosteq("ab", "abc")]
53-
54-
test[almosteq(1.0, 1.0 + ulp(1.0))]
55-
56-
# TODO: counterintuitively, need a large tolerance here, because when one operand is zero,
57-
# TODO: the final tolerance is actually tol*min_normal.
58-
min_normal = float_info.min
59-
test[almosteq(min_normal / 2, 0, tol=1.0)]
60-
61-
too_large = 2**int(1e6)
62-
test_raises[OverflowError, float(too_large), "UPDATE THIS, need a float overflow here."]
63-
test[almosteq(too_large, too_large + 1)] # works, because 1/too_large is very small.
64-
65-
try:
66-
from mpmath import mpf
67-
except ImportError: # pragma: no cover
68-
error["mpmath not installed in this Python, cannot test arbitrary precision input for mathseq."]
69-
else:
70-
test[almosteq(mpf(1.0), mpf(1.0 + ulp(1.0)))]
71-
test[almosteq(1.0, mpf(1.0 + ulp(1.0)))]
72-
test[almosteq(mpf(1.0), 1.0 + ulp(1.0))]
73-
7448
# explicitly listed elements, same as a genexpr using tuple input (but supports infix math)
7549
with testset("s, convenience"):
7650
test[tuple(s(1)) == (1,)]

unpythonic/tests/test_misc.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,14 @@
66
from operator import add
77
from functools import partial
88
from collections import deque
9-
from sys import float_info
109
from queue import Queue
1110
from time import sleep
1211
import threading
1312
import sys
1413

1514
from ..misc import (call, callwith, raisef, tryf, equip_with_traceback,
1615
pack, namelambda, timer,
17-
getattrrec, setattrrec, Popper, CountingIterator, ulp, slurp,
16+
getattrrec, setattrrec, Popper, CountingIterator, slurp,
1817
async_raise, callsite_filename, safeissubclass)
1918
from ..fun import withself
2019
from ..env import env
@@ -270,14 +269,6 @@ def __init__(self, x):
270269
test[it.count == k]
271270
test[it.count == 5]
272271

273-
# Unit in the Last Place, float utility
274-
# https://en.wikipedia.org/wiki/Unit_in_the_last_place
275-
with testset("ulp (unit in the last place; float utility)"):
276-
test[ulp(1.0) == float_info.epsilon]
277-
# test also at some base-2 exponent switch points
278-
test[ulp(2.0) == 2 * float_info.epsilon]
279-
test[ulp(0.5) == 0.5 * float_info.epsilon]
280-
281272
with testset("slurp (drain a queue into a list)"):
282273
q = Queue()
283274
for k in range(10):

unpythonic/tests/test_numutil.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from ..syntax import macros, test, test_raises, error # noqa: F401
4+
from ..test.fixtures import session, testset
5+
6+
import sys
7+
8+
from ..numutil import almosteq, ulp
9+
10+
def runtests():
11+
with testset("ulp (unit in the last place; float utility)"):
12+
test[ulp(1.0) == sys.float_info.epsilon]
13+
# test also at some base-2 exponent switch points
14+
test[ulp(2.0) == 2 * sys.float_info.epsilon]
15+
test[ulp(0.5) == 0.5 * sys.float_info.epsilon]
16+
17+
with testset("almosteq"):
18+
# For anything but floating-point inputs, it's exact equality.
19+
test[almosteq("abc", "abc")]
20+
test[not almosteq("ab", "abc")]
21+
22+
test[almosteq(1.0, 1.0 + ulp(1.0))]
23+
24+
# TODO: counterintuitively, need a large tolerance here, because when one operand is zero,
25+
# TODO: the final tolerance is actually tol*min_normal.
26+
min_normal = sys.float_info.min
27+
test[almosteq(min_normal / 2, 0, tol=1.0)]
28+
29+
too_large = 2**int(1e6)
30+
test_raises[OverflowError, float(too_large), "UPDATE THIS, need a float overflow here."]
31+
test[almosteq(too_large, too_large + 1)] # works, because 1/too_large is very small.
32+
33+
try:
34+
from mpmath import mpf
35+
except ImportError: # pragma: no cover
36+
error["mpmath not installed in this Python, cannot test arbitrary precision input for mathseq."]
37+
else:
38+
test[almosteq(mpf(1.0), mpf(1.0 + ulp(1.0)))]
39+
test[almosteq(1.0, mpf(1.0 + ulp(1.0)))]
40+
test[almosteq(mpf(1.0), 1.0 + ulp(1.0))]
41+
42+
if __name__ == '__main__': # pragma: no cover
43+
with session(__file__):
44+
runtests()

0 commit comments

Comments
 (0)