Skip to content

Commit b52623a

Browse files
committed
NF - add ulp function (cf. matlab eps function)
Function to return gap between value and nearest representable value of same type. Use ``ulp`` instead of ``eps` because the wikipedia article pointed out the different meanings possible for eps: http://en.wikipedia.org/wiki/Machine_epsilon#Variant_definitions
1 parent ae75e20 commit b52623a

File tree

3 files changed

+130
-37
lines changed

3 files changed

+130
-37
lines changed

nibabel/casting.py

Lines changed: 76 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,10 @@ def type_info(np_type):
177177
info : dict
178178
with fields ``min`` (minimum value), ``max`` (maximum value), ``nexp``
179179
(exponent width), ``nmant`` (significand precision not including
180-
implicit first digit) ``width`` (width in bytes). ``nexp``, ``nmant``
181-
are None for integer types. Both ``min`` and ``max`` are of type
182-
`np_type`.
180+
implicit first digit), ``minexp`` (minimum exponent), ``maxexp``
181+
(maximum exponent), ``width`` (width in bytes). (``nexp``, ``nmant``,
182+
``minexp``, ``maxexp``) are None for integer types. Both ``min`` and
183+
``max`` are of type `np_type`.
183184
184185
Raises
185186
------
@@ -188,9 +189,10 @@ def type_info(np_type):
188189
Notes
189190
-----
190191
You might be thinking that ``np.finfo`` does this job, and it does, except
191-
for PPC long doubles (http://projects.scipy.org/numpy/ticket/2077). This
192-
routine protects against errors in ``np.finfo`` by only accepting values
193-
that we know are likely to be correct.
192+
for PPC long doubles (http://projects.scipy.org/numpy/ticket/2077) and
193+
float96 on Windows compiled with Mingw. This routine protects against such
194+
errors in ``np.finfo`` by only accepting values that we know are likely to
195+
be correct.
194196
"""
195197
dt = np.dtype(np_type)
196198
np_type = dt.type
@@ -200,13 +202,18 @@ def type_info(np_type):
200202
except ValueError:
201203
pass
202204
else:
203-
return dict(min=np_type(info.min), max=np_type(info.max),
204-
nmant=None, nexp=None, width=width)
205+
return dict(min=np_type(info.min), max=np_type(info.max), minexp=None,
206+
maxexp=None, nmant=None, nexp=None, width=width)
205207
info = np.finfo(dt)
206208
# Trust the standard IEEE types
207209
nmant, nexp = info.nmant, info.nexp
208-
ret = dict(min=np_type(info.min), max=np_type(info.max), nmant=nmant,
209-
nexp=nexp, width=width)
210+
ret = dict(min=np_type(info.min),
211+
max=np_type(info.max),
212+
nmant=nmant,
213+
nexp=nexp,
214+
minexp=info.minexp,
215+
maxexp=info.maxexp,
216+
width=width)
210217
if np_type in (_float16, np.float32, np.float64,
211218
np.complex64, np.complex128):
212219
return ret
@@ -223,14 +230,12 @@ def type_info(np_type):
223230
pass # these are OK
224231
elif vals in ((52, 15, 12), # windows float96
225232
(52, 15, 16)): # windows float128?
226-
# On windows 32 bit at least, float96 appears to be a float64 padded to
227-
# 96 bits. The nexp == 15 is the same as for intel 80 but nexp in fact
228-
# appears to be 11 as for float64
229-
return dict(min=np_type(info_64.min), max=np_type(info_64.max),
230-
nmant=info_64.nmant, nexp=info_64.nexp, width=width)
233+
# On windows 32 bit at least, float96 is Intel 80 storage but operating
234+
# at float64 precision. The finfo values give nexp == 15 (as for intel
235+
# 80) but in calculations nexp in fact appears to be 11 as for float64
236+
return type_info(np.float64).update(dict(width=width))
231237
elif vals == (1, 1, 16) and processor() == 'powerpc': # broken PPC
232-
return dict(min=np_type(info_64.min), max=np_type(info_64.max),
233-
nmant=106, nexp=11, width=width)
238+
ret = type_info(np.float64).update(dict(nmant=106, width=width))
234239
else: # don't recognize the type
235240
raise FloatingError('We had not expected type %s' % np_type)
236241
return ret
@@ -438,21 +443,32 @@ def floor_log2(x):
438443
439444
Returns
440445
-------
441-
L : int
442-
floor of base 2 log of `x`
446+
L : None or int
447+
floor of base 2 log of `x`. None if `x` == 0.
443448
444449
Examples
445450
--------
446451
>>> floor_log2(2**9+1)
447452
9
448453
>>> floor_log2(-2**9+1)
449454
8
455+
>>> floor_log2(0.5)
456+
-1
457+
>>> floor_log2(0) is None
458+
True
450459
"""
451460
ip = 0
452461
rem = abs(x)
453-
while rem>=2:
454-
ip += 1
455-
rem //= 2
462+
if rem > 1:
463+
while rem>=2:
464+
ip += 1
465+
rem //= 2
466+
return ip
467+
elif rem == 0:
468+
return None
469+
while rem < 1:
470+
ip -= 1
471+
rem *= 2
456472
return ip
457473

458474

@@ -523,3 +539,41 @@ def able_int_type(values):
523539
if mn >= info.min and mx <= info.max:
524540
return ityp
525541
return None
542+
543+
544+
def ulp(val=np.float64(1.0)):
545+
""" Return gap between `val` and nearest representable number of same type
546+
547+
This is the value of a unit in the last place (ULP), and is similar in
548+
meaning to the MATLAB eps function.
549+
550+
Parameters
551+
----------
552+
val : scalar, optional
553+
scalar value of any numpy type. Default is 1.0 (float64)
554+
555+
Returns
556+
-------
557+
ulp_val : scalar
558+
gap between `val` and nearest representable number of same type
559+
560+
Notes
561+
-----
562+
The wikipedia article on machine epsilon points out that the term *epsilon*
563+
can be used in the sense of a unit in the last place (ULP), or as the
564+
maximum relative rounding error. The MATLAB ``eps`` function uses the ULP
565+
meaning, but this function is ``ulp`` rather than ``eps`` to avoid confusion
566+
between different meanings of *eps*.
567+
"""
568+
val = np.array(val)
569+
if not np.isfinite(val):
570+
return np.nan
571+
if val.dtype.kind in 'iu':
572+
return 1
573+
aval = np.abs(val)
574+
info = type_info(val.dtype)
575+
fl2 = floor_log2(aval)
576+
if fl2 is None or fl2 < info['minexp']: # subnormal
577+
fl2 = info['minexp']
578+
# 'nmant' value does not include implicit first bit
579+
return 2**(fl2 - info['nmant'])

nibabel/tests/test_casting.py

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import numpy as np
55

66
from ..casting import (float_to_int, shared_range, CastingError, int_to_float,
7-
as_int, int_abs, best_float)
7+
as_int, int_abs, floor_log2, able_int_type, best_float,
8+
ulp)
89

910
from numpy.testing import (assert_array_almost_equal, assert_array_equal)
1011

@@ -138,6 +139,18 @@ def test_int_abs():
138139
assert_array_equal(int_abs(in_arr), [e_mn, mx])
139140

140141

142+
def test_floor_log2():
143+
assert_equal(floor_log2(2**9+1), 9)
144+
assert_equal(floor_log2(-2**9+1), 8)
145+
assert_equal(floor_log2(2), 1)
146+
assert_equal(floor_log2(1), 0)
147+
assert_equal(floor_log2(0.5), -1)
148+
assert_equal(floor_log2(0.75), -1)
149+
assert_equal(floor_log2(0.25), -2)
150+
assert_equal(floor_log2(0.24), -3)
151+
assert_equal(floor_log2(0), None)
152+
153+
141154
def test_able_int_type():
142155
# The integer type cabable of containing values
143156
for vals, exp_out in (
@@ -194,18 +207,33 @@ def test_best_float():
194207
assert_equal(best, np.longdouble)
195208

196209

197-
def test_eps():
198-
assert_equal(eps(), np.finfo(np.float64).eps)
199-
assert_equal(eps(1.0), np.finfo(np.float64).eps)
200-
assert_equal(eps(np.float32(1.0)), np.finfo(np.float32).eps)
201-
assert_equal(eps(np.float32(1.999)), np.finfo(np.float32).eps)
210+
def test_ulp():
211+
assert_equal(ulp(), np.finfo(np.float64).eps)
212+
assert_equal(ulp(1.0), np.finfo(np.float64).eps)
213+
assert_equal(ulp(np.float32(1.0)), np.finfo(np.float32).eps)
214+
assert_equal(ulp(np.float32(1.999)), np.finfo(np.float32).eps)
202215
# Integers always return 1
203-
assert_equal(eps(1), 1)
204-
assert_equal(eps(2**63-1), 1)
216+
assert_equal(ulp(1), 1)
217+
assert_equal(ulp(2**63-1), 1)
205218
# negative / positive same
206-
assert_equal(eps(-1), 1)
207-
assert_equal(eps(7.999), eps(4.0))
208-
assert_equal(eps(-7.999), eps(4.0))
209-
assert_equal(eps(np.float64(2**54-2)), 2)
210-
assert_equal(eps(np.float64(2**54)), 4)
211-
assert_equal(eps(np.float64(2**54)), 4)
219+
assert_equal(ulp(-1), 1)
220+
assert_equal(ulp(7.999), ulp(4.0))
221+
assert_equal(ulp(-7.999), ulp(4.0))
222+
assert_equal(ulp(np.float64(2**54-2)), 2)
223+
assert_equal(ulp(np.float64(2**54)), 4)
224+
assert_equal(ulp(np.float64(2**54)), 4)
225+
# Infs, NaNs return nan
226+
assert_true(np.isnan(ulp(np.inf)))
227+
assert_true(np.isnan(ulp(-np.inf)))
228+
assert_true(np.isnan(ulp(np.nan)))
229+
# 0 gives subnormal smallest
230+
subn64 = np.float64(2**(-1022-52))
231+
subn32 = np.float32(2**(-126-23))
232+
assert_equal(ulp(0.0), subn64)
233+
assert_equal(ulp(np.float64(0)), subn64)
234+
assert_equal(ulp(np.float32(0)), subn32)
235+
# as do multiples of subnormal smallest
236+
assert_equal(ulp(subn64 * np.float64(2**52)), subn64)
237+
assert_equal(ulp(subn64 * np.float64(2**53)), subn64*2)
238+
assert_equal(ulp(subn32 * np.float32(2**23)), subn32)
239+
assert_equal(ulp(subn32 * np.float32(2**24)), subn32*2)

nibabel/tests/test_floating.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ def test_type_info():
2828
for dtt in np.sctypes['int'] + np.sctypes['uint']:
2929
info = np.iinfo(dtt)
3030
infod = type_info(dtt)
31-
assert_equal(dict(min=info.min, max=info.max, nexp=None, nmant=None,
31+
assert_equal(dict(min=info.min, max=info.max,
32+
nexp=None, nmant=None,
33+
minexp=None, maxexp=None,
3234
width=np.dtype(dtt).itemsize), infod)
3335
assert_equal(infod['min'].dtype.type, dtt)
3436
assert_equal(infod['max'].dtype.type, dtt)
@@ -37,6 +39,7 @@ def test_type_info():
3739
infod = type_info(dtt)
3840
assert_equal(dict(min=info.min, max=info.max,
3941
nexp=info.nexp, nmant=info.nmant,
42+
minexp=info.minexp, maxexp=info.maxexp,
4043
width=np.dtype(dtt).itemsize),
4144
infod)
4245
assert_equal(infod['min'].dtype.type, dtt)
@@ -54,12 +57,20 @@ def test_type_info():
5457
(112, 15, 16), # real float128
5558
(106, 11, 16)): # PPC head, tail doubles, expected values
5659
assert_equal(dict(min=info.min, max=info.max,
60+
minexp=info.minexp, maxexp=info.maxexp,
5761
nexp=info.nexp, nmant=info.nmant, width=width),
5862
infod)
5963
elif vals == (1, 1, 16): # bust info for PPC head / tail longdoubles
6064
assert_equal(dict(min=dbl_info.min, max=dbl_info.max,
65+
minexp=-1022, maxexp=1024,
6166
nexp=11, nmant=106, width=16),
6267
infod)
68+
elif vals == (52, 15, 12):
69+
exp_res = type_info(np.float64)
70+
exp_res['width'] = width
71+
assert_equal(exp_res, infod)
72+
else:
73+
raise ValueError("Unexpected float type to test")
6374

6475

6576
def test_nmant():

0 commit comments

Comments
 (0)