Skip to content

Commit 9637ed8

Browse files
committed
Merge pull request #91 from matthew-brett/array-writers-ppc
Array writers ppc After merging the array writers branch recently, I discovered that PPC min and max values for longdouble were wrong. This branch introduces a type_info function to return the min, max etc for a particular numpy type, and special-cases the broken PPC longdouble. I've used the type_info function throughout for consistency (instead of np.finfo).
2 parents b960228 + 5f48a1b commit 9637ed8

File tree

9 files changed

+178
-116
lines changed

9 files changed

+178
-116
lines changed

nibabel/arraywriters.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def __init__(self, array, out_dtype=None)
2525

2626
import numpy as np
2727

28-
from .casting import int_to_float, as_int, int_abs
28+
from .casting import int_to_float, as_int, int_abs, type_info
2929
from .volumeutils import finite_range, array_to_file
3030

3131

@@ -329,13 +329,13 @@ def _range_scale(self):
329329
""" Calculate scaling based on data range and output type """
330330
mn, mx = self.finite_range() # These can be floats or integers
331331
out_dtype = self._out_dtype
332+
info = type_info(out_dtype)
333+
t_mn_mx = info['min'], info['max']
332334
if out_dtype.kind == 'f':
333-
t_mn_mx = np.finfo(out_dtype).min, np.finfo(out_dtype).max
334335
# But we want maximum precision for the calculations. Casting will
335336
# not lose precision because min/max are of fp type.
336337
t_min, t_max = np.array(t_mn_mx, dtype = np.longdouble)
337338
else: # (u)int
338-
t_mn_mx = np.iinfo(out_dtype).min, np.iinfo(out_dtype).max
339339
t_min, t_max = [int_to_float(v, np.longdouble) for v in t_mn_mx]
340340
if self._out_dtype.kind == 'u':
341341
if mn < 0 and mx > 0:
@@ -484,7 +484,8 @@ def _range_scale(self):
484484
mn2mx = int_to_float(as_int(mx) - as_int(mn), np.longdouble)
485485
if out_dtype.kind == 'f':
486486
# Type range, these are also floats
487-
t_mn_mx = np.finfo(out_dtype).min, np.finfo(out_dtype).max
487+
info = type_info(out_dtype)
488+
t_mn_mx = info['min'], info['max']
488489
else:
489490
t_mn_mx = np.iinfo(out_dtype).min, np.iinfo(out_dtype).max
490491
t_mn_mx= [int_to_float(v, np.longdouble) for v in t_mn_mx]

nibabel/casting.py

Lines changed: 61 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
""" Utilties for casting floats to integers
22
"""
33

4+
from platform import processor
5+
46
import numpy as np
57

68

@@ -152,56 +154,75 @@ def shared_range(flt_type, int_type):
152154
except AttributeError: # float16 not present in np < 1.6
153155
_float16 = None
154156

155-
# The number of significand digits in IEEE floating point formats, not including
156-
# the implicit leading 0. See http://en.wikipedia.org/wiki/IEEE_754-2008
157-
_flt_nmant = {
158-
_float16: 10,
159-
np.float32: 23,
160-
np.float64: 52,
161-
}
162-
163157

164158
class FloatingError(Exception):
165159
pass
166160

167161

168-
def flt2nmant(flt_type):
169-
""" Number of significand bits in float type `flt_type`
162+
def type_info(np_type):
163+
""" Return dict with min, max, nexp, nmant, width for numpy type `np_type`
164+
165+
Type can be integer in which case nexp and nmant are None.
170166
171167
Parameters
172168
----------
173-
flt_type : object
174-
Numpy floating point type, such as np.float32
169+
np_type : numpy type specifier
170+
Any specifier for a numpy dtype
175171
176172
Returns
177173
-------
178-
nmant : int
179-
Number of digits in the signficand
174+
info : dict
175+
with fields ``min`` (minimum value), ``max`` (maximum value), ``nexp``
176+
(exponent width), ``nmant`` (significand precision not including
177+
implicit first digit) ``width`` (width in bytes). ``nexp``, ``nmant``
178+
are None for integer types. Both ``min`` and ``max`` are of type
179+
`np_type`.
180+
181+
Raises
182+
------
183+
FloatingError : for floating point types we don't recognize
184+
185+
Notes
186+
-----
187+
You might be thinking that ``np.finfo`` does this job, and it does, except
188+
for PPC long doubles (http://projects.scipy.org/numpy/ticket/2077). This
189+
routine protects against errors in ``np.finfo`` by only accepting values
190+
that we know are likely to be correct.
180191
"""
181-
try:
182-
return _flt_nmant[flt_type]
183-
except KeyError:
192+
dt = np.dtype(np_type)
193+
np_type = dt.type
194+
width = dt.itemsize
195+
try: # integer type
196+
info = np.iinfo(dt)
197+
except ValueError:
184198
pass
185-
fi = np.finfo(flt_type)
186-
nmant, nexp = fi.nmant, fi.nexp
187-
# Assuming the np.float type is always IEEE 64 bit
188-
if flt_type is np.float and (nmant, nexp) == (52, 11):
189-
return 52
190-
# Now we should be testing long doubles
191-
assert flt_type is np.longdouble
192-
if (nmant, nexp) == (63, 15): # 80-bit intel type
193-
return 63 # Not including explicit first digit
194-
# We test the declared nmant by stepping up and down. These tests assume a
195-
# binary format
196-
i_end_contig = 2**(nmant+1) # int
197-
f_end_contig = flt_type(i_end_contig)
198-
# We need as_int here because long doubles do not necessarily convert
199-
# correctly to ints with int() - see
200-
# http://projects.scipy.org/numpy/ticket/1395
201-
if as_int(f_end_contig-1) == (i_end_contig-1): # still representable
202-
if as_int(f_end_contig+1) == i_end_contig: # Rounding down
203-
return nmant
204-
raise FloatingError('Cannot be confident of nmant value for %s' % flt_type)
199+
else:
200+
return dict(min=np_type(info.min), max=np_type(info.max),
201+
nmant=None, nexp=None, width=width)
202+
info = np.finfo(dt)
203+
# Trust the standard IEEE types
204+
nmant, nexp = info.nmant, info.nexp
205+
ret = dict(min=np_type(info.min), max=np_type(info.max), nmant=nmant,
206+
nexp=nexp, width=width)
207+
if np_type in (_float16, np.float32, np.float64,
208+
np.complex64, np.complex128):
209+
return ret
210+
if dt.kind == 'c':
211+
assert np_type is np.longcomplex
212+
vals = (nmant, nexp, width / 2)
213+
else:
214+
assert np_type is np.longdouble
215+
vals = (nmant, nexp, width)
216+
if vals in ((112, 15, 16), # binary128
217+
(63, 15, 12), (63, 15, 16)): # Intel extended 80
218+
pass # these are OK
219+
elif vals == (1, 1, 16) and processor() == 'powerpc': # broken PPC
220+
dbl_info = np.finfo(np.float64)
221+
return dict(min=np_type(dbl_info.min), max=np_type(dbl_info.max),
222+
nmant=106, nexp=11, width=width)
223+
else: # don't recognize the type
224+
raise FloatingError('We had not expected this type')
225+
return ret
205226

206227

207228
def as_int(x, check=True):
@@ -342,14 +363,14 @@ def floor_exact(val, flt_type):
342363
faval = int_to_float(aval, flt_type)
343364
except OverflowError:
344365
faval = np.inf
366+
info = type_info(flt_type)
345367
if faval == np.inf:
346-
return sign * np.finfo(flt_type).max
368+
return sign * info['max']
347369
if as_int(faval) <= aval: # as_int deals with longdouble safely
348370
# Float casting has made the value go down or stay the same
349371
return sign * faval
350372
# Float casting made the value go up
351-
nmant = flt2nmant(flt_type)
352-
biggest_gap = 2**(floor_log2(aval) - nmant)
373+
biggest_gap = 2**(floor_log2(aval) - info['nmant'])
353374
assert biggest_gap > 1
354375
faval -= flt_type(biggest_gap)
355376
return sign * faval

nibabel/tests/test_arraywriters.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def __init__(self, array, out_dtype=None, order='F')
3737
WriterError, ScalingError, ArrayWriter,
3838
make_array_writer, get_slope_inter)
3939

40-
from ..casting import int_abs
40+
from ..casting import int_abs, type_info
4141

4242
from ..volumeutils import array_from_file, apply_read_scaling
4343

@@ -354,8 +354,8 @@ def test_io_scaling():
354354
(np.float32, np.int16, None)):
355355
out_dtype = np.dtype(out_type)
356356
arr = np.zeros((3,), dtype=in_type)
357-
info = np.finfo(in_type) if arr.dtype.kind == 'f' else np.iinfo(in_type)
358-
arr[0], arr[1] = info.min, info.max
357+
info = type_info(in_type)
358+
arr[0], arr[1] = info['min'], info['max']
359359
aw = SlopeInterArrayWriter(arr, out_dtype, calc_scale=False)
360360
if not err is None:
361361
assert_raises(err, aw.calc_scale)
@@ -429,17 +429,16 @@ def test_writers_roundtrip():
429429

430430

431431
def test_to_float():
432+
start, stop = 0, 100
432433
for in_type in NUMERIC_TYPES:
433-
if in_type in IUINT_TYPES:
434-
info = np.iinfo(in_type)
435-
mn, mx, start, stop, step = info.min, info.max, 0, 100, 1
436-
else:
437-
info = np.finfo(in_type)
438-
mn, mx, start, stop, step = info.min, info.max, 0, 100, 0.5
434+
step = 1 if in_type in IUINT_TYPES else 0.5
435+
info = type_info(in_type)
436+
mn, mx = info['min'], info['max']
439437
arr = np.arange(start, stop, step, dtype=in_type)
440438
arr[0] = mn
441439
arr[-1] = mx
442440
for out_type in CFLOAT_TYPES:
441+
out_info = type_info(out_type)
443442
for klass in (SlopeInterArrayWriter, SlopeArrayWriter,
444443
ArrayWriter):
445444
if in_type in COMPLEX_TYPES and out_type in FLOAT_TYPES:
@@ -451,8 +450,7 @@ def test_to_float():
451450
arr_back = round_trip(aw)
452451
assert_array_equal(arr.astype(out_type), arr_back)
453452
# Check too-big values overflowed correctly
454-
out_min = np.finfo(out_type).min
455-
out_max = np.finfo(out_type).max
453+
out_min, out_max = out_info['min'], out_info['max']
456454
assert_true(np.all(arr_back[arr > out_max] == np.inf))
457455
assert_true(np.all(arr_back[arr < out_min] == -np.inf))
458456

@@ -498,8 +496,8 @@ def test_writer_maker():
498496
def test_float_int_min_max():
499497
# Conversion between float and int
500498
for in_dt in FLOAT_TYPES:
501-
finf = np.finfo(in_dt)
502-
arr = np.array([finf.min, finf.max], dtype=in_dt)
499+
finf = type_info(in_dt)
500+
arr = np.array([finf['min'], finf['max']], dtype=in_dt)
503501
for out_dt in IUINT_TYPES:
504502
try:
505503
aw = SlopeInterArrayWriter(arr, out_dt)

0 commit comments

Comments
 (0)