Skip to content

Commit ac9c3ec

Browse files
committed
Merge pull request #115 from matthew-brett/int_uint_futz
Improving int to int scaling and adding roundtrip tests After discussion on the mailing list, use various tricks to make int to int scaling more accurate. Add functions useful for estimating floating point error. Avoid using useless windows 32 longdouble because it gives no higher precision. Make roundtrip tests with fairly rational bounds on error. Check and fix on windows 32 and 64 bit, PPC.
2 parents d6098b9 + 76031a0 commit ac9c3ec

File tree

9 files changed

+733
-106
lines changed

9 files changed

+733
-106
lines changed

nibabel/arraywriters.py

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ 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, type_info
28+
from .casting import (int_to_float, as_int, int_abs, type_info, floor_exact,
29+
best_float)
2930
from .volumeutils import finite_range, array_to_file
3031

3132

@@ -303,7 +304,11 @@ def _do_scaling(self):
303304
# (u)int to (u)int
304305
info = np.iinfo(out_dtype)
305306
out_max, out_min = info.max, info.min
306-
if mx <= out_max and mn >= out_min: # already in range
307+
# If left as int64, uint64, comparisons will default to floats, and
308+
# these are inexact for > 2**53 - so convert to int
309+
if (as_int(mx) <= as_int(out_max) and
310+
as_int(mn) >= as_int(out_min)):
311+
# already in range
307312
return
308313
# (u)int to (u)int scaling
309314
self._iu2iu()
@@ -331,12 +336,13 @@ def _range_scale(self):
331336
out_dtype = self._out_dtype
332337
info = type_info(out_dtype)
333338
t_mn_mx = info['min'], info['max']
339+
big_float = best_float()
334340
if out_dtype.kind == 'f':
335341
# But we want maximum precision for the calculations. Casting will
336342
# not lose precision because min/max are of fp type.
337-
t_min, t_max = np.array(t_mn_mx, dtype = np.longdouble)
343+
t_min, t_max = np.array(t_mn_mx, dtype = big_float)
338344
else: # (u)int
339-
t_min, t_max = [int_to_float(v, np.longdouble) for v in t_mn_mx]
345+
t_min, t_max = [int_to_float(v, big_float) for v in t_mn_mx]
340346
if self._out_dtype.kind == 'u':
341347
if mn < 0 and mx > 0:
342348
raise WriterError('Cannot scale negative and positive '
@@ -458,9 +464,25 @@ def _iu2iu(self):
458464
t_min, t_max = np.iinfo(out_dtype).min, np.iinfo(out_dtype).max
459465
type_range = as_int(t_max) - as_int(t_min)
460466
mn2mx = mx - mn
461-
if mn2mx <= type_range: # offset enough?
462-
self.inter = mn - t_min
463-
return
467+
if mn2mx <= type_range: # might offset be enough?
468+
if t_min == 0: # uint output - take min to 0
469+
# decrease offset with floor_exact, meaning mn >= t_min after
470+
# subtraction. But we may have pushed the data over t_max,
471+
# which we check below
472+
inter = floor_exact(mn - t_min, self.scaler_dtype)
473+
else: # int output - take midpoint to 0
474+
# ceil below increases inter, pushing scale up to 0.5 towards
475+
# -inf, because ints have abs min == abs max + 1
476+
midpoint = mn + as_int(np.ceil(mn2mx / 2.0))
477+
# Floor exact decreases inter, so pulling scaled values more
478+
# positive. This may make mx - inter > t_max
479+
inter = floor_exact(midpoint, self.scaler_dtype)
480+
# Need to check still in range after floor_exact-ing
481+
int_inter = as_int(inter)
482+
assert mn - int_inter >= t_min
483+
if mx - int_inter <= t_max:
484+
self.inter = inter
485+
return
464486
# Try slope options (sign flip) and then range scaling
465487
super(SlopeInterArrayWriter, self)._iu2iu()
466488

@@ -472,27 +494,28 @@ def _range_scale(self):
472494
self.inter = mn
473495
return
474496
# Straight mx-mn can overflow.
497+
big_float = best_float() # usually longdouble except in win 32
475498
if mn.dtype.kind == 'f': # Already floats
476499
# float64 and below cast correctly to longdouble. Longdouble needs
477500
# no casting
478-
mn2mx = np.diff(np.array([mn, mx], dtype=np.longdouble))
501+
mn2mx = np.diff(np.array([mn, mx], dtype=big_float))
479502
else: # max possible (u)int range is 2**64-1 (int64, uint64)
480503
# int_to_float covers this range. On windows longdouble is the same
481504
# as double so mn2mx will be 2**64 - thus overestimating slope
482505
# slightly. Casting to int needed to allow mx-mn to be larger than
483506
# the largest (u)int value
484-
mn2mx = int_to_float(as_int(mx) - as_int(mn), np.longdouble)
507+
mn2mx = int_to_float(as_int(mx) - as_int(mn), big_float)
485508
if out_dtype.kind == 'f':
486509
# Type range, these are also floats
487510
info = type_info(out_dtype)
488511
t_mn_mx = info['min'], info['max']
489512
else:
490513
t_mn_mx = np.iinfo(out_dtype).min, np.iinfo(out_dtype).max
491-
t_mn_mx= [int_to_float(v, np.longdouble) for v in t_mn_mx]
514+
t_mn_mx= [int_to_float(v, big_float) for v in t_mn_mx]
492515
# We want maximum precision for the calculations. Casting will
493516
# not lose precision because min/max are of fp type.
494517
assert [v.dtype.kind for v in t_mn_mx] == ['f', 'f']
495-
scaled_mn2mx = np.diff(np.array(t_mn_mx, dtype = np.longdouble))
518+
scaled_mn2mx = np.diff(np.array(t_mn_mx, dtype = big_float))
496519
slope = mn2mx / scaled_mn2mx
497520
self.inter = mn - t_mn_mx[0] * slope
498521
self.slope = slope

0 commit comments

Comments
 (0)