Skip to content

Commit f2b665f

Browse files
committed
Merge pull request #134 from matthew-brett/better-longdouble-checks
Better longdouble checks Make checks for longdouble implementations more specific. Also consider POWER7 when checking for double pair implementation. Work round some odd float128 / complex256 behavior on PPC. Disable use of float128 nifti data type unless we have a real binary128 data type.
2 parents a9f37e7 + 0398e8e commit f2b665f

File tree

6 files changed

+161
-17
lines changed

6 files changed

+161
-17
lines changed

nibabel/casting.py

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,14 @@ class FloatingError(Exception):
168168
pass
169169

170170

171+
def on_powerpc():
172+
""" True if we are running on a Power PC platform
173+
174+
Has to deal with older Macs and IBM POWER7 series among others
175+
"""
176+
return processor() == 'powerpc' or machine().startswith('ppc')
177+
178+
171179
def type_info(np_type):
172180
""" Return dict with min, max, nexp, nmant, width for numpy type `np_type`
173181
@@ -244,13 +252,96 @@ def type_info(np_type):
244252
# at float64 precision. The finfo values give nexp == 15 (as for intel
245253
# 80) but in calculations nexp in fact appears to be 11 as for float64
246254
ret.update(dict(width=width))
247-
elif vals == (1, 1, 16) and processor() == 'powerpc': # broken PPC
248-
ret.update(dict(nmant=106, width=width))
249-
else: # don't recognize the type
255+
# Oh dear, we don't recognize the type information. Try some known types
256+
# and then give up. At this stage we're expecting exotic longdouble or their
257+
# complex equivalent.
258+
if not np_type in (np.longdouble, np.longcomplex) or width not in (16, 32):
250259
raise FloatingError('We had not expected type %s' % np_type)
260+
if (vals == (1, 1, 16) and on_powerpc() and
261+
_check_maxexp(np.longdouble, 1024)):
262+
# double pair on PPC. The _check_nmant routine does not work for this
263+
# type, hence the powerpc platform check instead
264+
ret.update(dict(nmant = 106, width=width))
265+
elif (_check_nmant(np.longdouble, 52) and
266+
_check_maxexp(np.longdouble, 11)):
267+
# Got float64 despite everything
268+
pass
269+
elif (_check_nmant(np.longdouble, 112) and
270+
_check_maxexp(np.longdouble, 16384)):
271+
# binary 128, but with some busted type information. np.longcomplex
272+
# seems to break here too, so we need to use np.longdouble and
273+
# complexify
274+
two = np.longdouble(2)
275+
# See: http://matthew-brett.github.com/pydagogue/floating_point.html
276+
max_val = (two ** 113 - 1) / (two ** 112) * two ** 16383
277+
if np_type is np.longcomplex:
278+
max_val += 0j
279+
ret = dict(min = -max_val,
280+
max= max_val,
281+
nmant = 112,
282+
nexp = 15,
283+
minexp = -16382,
284+
maxexp = 16384,
285+
width = width)
286+
else: # don't recognize the type
287+
raise FloatingError('We had not expected long double type %s '
288+
'with info %s' % (np_type, info))
251289
return ret
252290

253291

292+
def _check_nmant(np_type, nmant):
293+
""" True if fp type `np_type` seems to have `nmant` significand digits
294+
295+
Note 'digits' does not include implicit digits. And in fact if there are no
296+
implicit digits, the `nmant` number is one less than the actual digits.
297+
Assumes base 2 representation.
298+
299+
Parameters
300+
----------
301+
np_type : numpy type specifier
302+
Any specifier for a numpy dtype
303+
nmant : int
304+
Number of digits to test against
305+
306+
Returns
307+
-------
308+
tf : bool
309+
True if `nmant` is the correct number of significand digits, false
310+
otherwise
311+
"""
312+
np_type = np.dtype(np_type).type
313+
max_contig = np_type(2 ** (nmant + 1)) # maximum of contiguous integers
314+
tests = max_contig + np.array([-2, -1, 0, 1, 2], dtype=np_type)
315+
return np.all(tests - max_contig == [-2, -1, 0, 0, 2])
316+
317+
318+
def _check_maxexp(np_type, maxexp):
319+
""" True if fp type `np_type` seems to have `maxexp` maximum exponent
320+
321+
We're testing "maxexp" as returned by numpy. This value is set to one
322+
greater than the maximum power of 2 that `np_type` can represent.
323+
324+
Assumes base 2 representation. Very crude check
325+
326+
Parameters
327+
----------
328+
np_type : numpy type specifier
329+
Any specifier for a numpy dtype
330+
maxexp : int
331+
Maximum exponent to test against
332+
333+
Returns
334+
-------
335+
tf : bool
336+
True if `maxexp` is the correct maximum exponent, False otherwise.
337+
"""
338+
dt = np.dtype(np_type)
339+
np_type = dt.type
340+
two = np_type(2).reshape((1,)) # to avoid upcasting
341+
return (np.isfinite(two ** (maxexp - 1)) and
342+
not np.isfinite(two ** maxexp))
343+
344+
254345
def as_int(x, check=True):
255346
""" Return python integer representation of number
256347
@@ -548,6 +639,13 @@ def best_float():
548639
return np.float64
549640

550641

642+
def have_binary128():
643+
""" True if we have a binary128 IEEE longdouble
644+
"""
645+
ti = type_info(np.longdouble)
646+
return (ti['nmant'], ti['maxexp']) == (112, 16384)
647+
648+
551649
def ok_floats():
552650
""" Return floating point types sorted by precision
553651

nibabel/nifti1.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from .quaternions import fillpositive, quat2mat, mat2quat
2121
from . import analyze # module import
2222
from .spm99analyze import SpmAnalyzeHeader
23+
from .casting import have_binary128
2324

2425
# Needed for quaternion calculation
2526
FLOAT32_EPS_3 = -np.finfo(np.float32).eps * 3
@@ -76,13 +77,12 @@
7677
header_dtype = np.dtype(header_dtd)
7778

7879
# datatypes not in analyze format, with codes
79-
try:
80-
_float128t = np.float128
81-
except AttributeError:
80+
if have_binary128():
81+
# Only enable 128 bit floats if we really have IEEE binary 128 longdoubles
82+
_float128t = np.longdouble
83+
_complex256t = np.longcomplex
84+
else:
8285
_float128t = np.void
83-
try:
84-
_complex256t = np.complex256
85-
except AttributeError:
8686
_complex256t = np.void
8787

8888
_dtdefs = ( # code, label, dtype definition, niistring

nibabel/tests/test_arraywriters.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,11 @@ def test_arraywriters():
8787
# Byteswapped is OK
8888
bs_arr = arr.byteswap().newbyteorder('S')
8989
bs_aw = klass(bs_arr)
90-
assert_array_equal(bs_arr, round_trip(bs_aw))
90+
# assert against original array because POWER7 was running into
91+
# trouble using the byteswapped array (bs_arr)
92+
assert_array_equal(arr, round_trip(bs_aw))
9193
bs_aw2 = klass(bs_arr, arr.dtype)
92-
assert_array_equal(bs_arr, round_trip(bs_aw2))
94+
assert_array_equal(arr, round_trip(bs_aw2))
9395
# 2D array
9496
arr2 = np.reshape(arr, (2, 5))
9597
a2w = klass(arr2)
@@ -508,6 +510,10 @@ def test_float_int_min_max():
508510
for in_dt in FLOAT_TYPES:
509511
finf = type_info(in_dt)
510512
arr = np.array([finf['min'], finf['max']], dtype=in_dt)
513+
# Bug in numpy 1.6.2 on PPC leading to infs - abort
514+
if not np.all(np.isfinite(arr)):
515+
print 'Hit PPC max -> inf bug; skip in_type %s' % in_dt
516+
continue
511517
for out_dt in IUINT_TYPES:
512518
try:
513519
aw = SlopeInterArrayWriter(arr, out_dt)

nibabel/tests/test_floating.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
""" Test floating point deconstructions and floor methods
22
"""
3-
from platform import processor
4-
53
import numpy as np
64

75
from ..casting import (floor_exact, ceil_exact, as_int, FloatingError,
8-
int_to_float, floor_log2, type_info)
6+
int_to_float, floor_log2, type_info, _check_nmant,
7+
_check_maxexp, ok_floats, on_powerpc, have_binary128)
98

109
from nose import SkipTest
11-
from nose.tools import assert_equal, assert_raises
10+
from nose.tools import assert_equal, assert_raises, assert_true, assert_false
1211

1312
IEEE_floats = [np.float32, np.float64]
1413
try:
@@ -80,6 +79,25 @@ def test_nmant():
8079
assert_equal(type_info(np.longdouble)['nmant'], 63)
8180

8281

82+
def test_check_nmant_nexp():
83+
# Routine for checking number of sigificand digits and exponent
84+
for t in IEEE_floats:
85+
nmant = np.finfo(t).nmant
86+
maxexp = np.finfo(t).maxexp
87+
assert_true(_check_nmant(t, nmant))
88+
assert_false(_check_nmant(t, nmant - 1))
89+
assert_false(_check_nmant(t, nmant + 1))
90+
assert_true(_check_maxexp(t, maxexp))
91+
assert_false(_check_maxexp(t, maxexp - 1))
92+
assert_false(_check_maxexp(t, maxexp + 1))
93+
# Check against type_info
94+
for t in ok_floats():
95+
ti = type_info(t)
96+
if ti['nmant'] != 106: # This check does not work for PPC double pair
97+
assert_true(_check_nmant(t, ti['nmant']))
98+
assert_true(_check_maxexp(t, ti['maxexp']))
99+
100+
83101
def test_as_int():
84102
# Integer representation of number
85103
assert_equal(as_int(2.0), 2)
@@ -214,7 +232,7 @@ def test_floor_exact():
214232
assert_equal(func(-iv+1, t), -iv+1)
215233
# The nmant value for longdouble on PPC appears to be conservative, so
216234
# that the tests for behavior above the nmant range fail
217-
if t is np.longdouble and processor() == 'powerpc':
235+
if t is np.longdouble and on_powerpc():
218236
continue
219237
# Confirm to ourselves that 2**(nmant+1) can't be exactly represented
220238
iv = 2**(nmant+1)
@@ -240,3 +258,12 @@ def test_floor_exact():
240258
assert_equal(int_flex(-iv-gap-j, t), -iv-2*gap)
241259
assert_equal(int_ceex(-iv-j, t), -iv)
242260
assert_equal(int_ceex(-iv-gap-j, t), -iv-gap)
261+
262+
263+
def test_usable_binary128():
264+
# Check for usable binary128
265+
yes = have_binary128()
266+
abf = np.longdouble(2) ** 16383
267+
assert_equal(yes,
268+
abf.dtype.itemsize == 16 and
269+
np.isfinite(abf))

nibabel/tests/test_nifti1.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import numpy as np
1616

17-
from ..casting import type_info
17+
from ..casting import type_info, have_binary128
1818
from ..tmpdirs import InTemporaryDirectory
1919
from ..spatialimages import HeaderDataError
2020
from ..affines import from_matvec
@@ -309,6 +309,14 @@ def test_binblock_is_file(self):
309309
hdr.write_to(str_io)
310310
assert_equal(str_io.getvalue(), hdr.binaryblock + ZEROB * 4)
311311

312+
def test_float128(self):
313+
hdr = self.header_class()
314+
if have_binary128():
315+
hdr.set_data_dtype(np.longdouble)
316+
assert_equal(hdr.get_data_dtype().type, np.longdouble)
317+
else:
318+
assert_raises(HeaderDataError, hdr.set_data_dtype, np.longdouble)
319+
312320

313321
class TestNifti1Image(tana.TestAnalyzeImage):
314322
# Run analyze-flavor spatialimage tests

nibabel/tests/test_scaling.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,11 @@ def check_int_a2f(in_type, out_type):
224224
this_min, this_max = info['min'], info['max']
225225
if not in_type in np.sctypes['complex']:
226226
data = np.array([this_min, this_max], in_type)
227+
# Bug in numpy 1.6.2 on PPC leading to infs - abort
228+
if not np.all(np.isfinite(data)):
229+
if DEBUG:
230+
print 'Hit PPC max -> inf bug; skip in_type %s' % in_type
231+
return
227232
else: # Funny behavior with complex256
228233
data = np.zeros((2,), in_type)
229234
data[0] = this_min + 0j

0 commit comments

Comments
 (0)