Skip to content

Commit b0a3325

Browse files
committed
BF: work round horrible MSVC uint64 casting
MSVC casting floats to uint64 casts floats > 2**63 to 2*63, at least on 32-bit Windows. Platform standard compiler.
1 parent 5f203ac commit b0a3325

File tree

5 files changed

+84
-39
lines changed

5 files changed

+84
-39
lines changed

nibabel/arraywriters.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -411,15 +411,16 @@ def _do_scaling(self):
411411
def _iu2iu(self):
412412
# (u)int to (u)int scaling
413413
mn, mx = self.finite_range()
414-
if self._out_dtype.kind == 'u':
414+
out_dt = self._out_dtype
415+
if out_dt.kind == 'u':
415416
# We're checking for a sign flip. This can only work for uint
416417
# output, because, for int output, the abs min of the type is
417418
# greater than the abs max, so the data either fits into the range
418419
# (tested for in _do_scaling), or this test can't pass. Need abs
419420
# that deals with max neg ints. abs problem only arises when all
420421
# the data is set to max neg integer value
421-
imax = np.iinfo(self._out_dtype).max
422-
if mx <= 0 and int_abs(mn) <= imax: # sign flip enough?
422+
o_min, o_max = shared_range(self.scaler_dtype, out_dt)
423+
if mx <= 0 and int_abs(mn) <= as_int(o_max): # sign flip enough?
423424
# -1.0 * arr will be in scaler_dtype precision
424425
self.slope = -1.0
425426
return
@@ -563,15 +564,19 @@ def _iu2iu(self):
563564
# range may be greater than the largest integer for this type.
564565
# as_int needed to work round numpy 1.4.1 int casting bug
565566
out_dtype = self._out_dtype
566-
t_min, t_max = np.iinfo(out_dtype).min, np.iinfo(out_dtype).max
567-
type_range = as_int(t_max) - as_int(t_min)
567+
# Options in this method are scaling using intercept only. These will
568+
# have to pass through ``self.scaler_dtype`` (because the intercept is
569+
# in this type).
570+
o_min, o_max = [as_int(v)
571+
for v in shared_range(self.scaler_dtype, out_dtype)]
572+
type_range = o_max - o_min
568573
mn2mx = mx - mn
569574
if mn2mx <= type_range: # might offset be enough?
570-
if t_min == 0: # uint output - take min to 0
575+
if o_min == 0: # uint output - take min to 0
571576
# decrease offset with floor_exact, meaning mn >= t_min after
572577
# subtraction. But we may have pushed the data over t_max,
573578
# which we check below
574-
inter = floor_exact(mn - t_min, self.scaler_dtype)
579+
inter = floor_exact(mn - o_min, self.scaler_dtype)
575580
else: # int output - take midpoint to 0
576581
# ceil below increases inter, pushing scale up to 0.5 towards
577582
# -inf, because ints have abs min == abs max + 1
@@ -581,8 +586,8 @@ def _iu2iu(self):
581586
inter = floor_exact(midpoint, self.scaler_dtype)
582587
# Need to check still in range after floor_exact-ing
583588
int_inter = as_int(inter)
584-
assert mn - int_inter >= t_min
585-
if mx - int_inter <= t_max:
589+
assert mn - int_inter >= o_min
590+
if mx - int_inter <= o_max:
586591
self.inter = inter
587592
return
588593
# Try slope options (sign flip) and then range scaling

nibabel/casting.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ class CastingError(Exception):
1414
pass
1515

1616

17+
#: Test for VC truncation when casting floats to uint64
18+
_test_val = 2**63 + 2**11 # Should be exactly representable in float64
19+
TRUNC_UINT64 = np.float64(_test_val).astype(np.uint64) != _test_val
20+
21+
1722
def float_to_int(arr, int_type, nan2zero=True, infmax=False):
1823
""" Convert floating point array `arr` to type `int_type`
1924
@@ -151,6 +156,8 @@ def shared_range(flt_type, int_type):
151156
mx = floor_exact(ii.max, flt_type)
152157
if mx == np.inf:
153158
mx = fi.max
159+
elif TRUNC_UINT64 and int_type == np.uint64:
160+
mx = min(mx, flt_type(2**63))
154161
_SHARED_RANGES[key] = (mn, mx)
155162
return mn, mx
156163

nibabel/tests/test_arraywriters.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -135,17 +135,29 @@ def test_no_scaling():
135135
kwargs = (dict(check_scaling=False) if awt == ArrayWriter
136136
else dict(calc_scale=False))
137137
aw = awt(arr, out_dtype, **kwargs)
138-
with suppress_warnings(): # cast to real from cplx
138+
with suppress_warnings():
139139
back_arr = round_trip(aw)
140-
exp_back = arr.astype(float)
140+
exp_back = arr.copy()
141141
if out_dtype in IUINT_TYPES:
142-
with np.errstate(invalid='ignore'):
143-
exp_back = np.round(exp_back)
144-
if hasattr(aw, 'slope') and in_dtype in FLOAT_TYPES:
145-
# Finite scaling sets infs to min / max
146-
exp_back = np.clip(exp_back, 0, 1)
147-
else:
148-
exp_back = np.clip(exp_back, *shared_range(float, out_dtype))
142+
if in_dtype in CFLOAT_TYPES:
143+
with suppress_warnings():
144+
exp_back = exp_back.astype(float)
145+
with np.errstate(invalid='ignore'):
146+
exp_back = np.round(exp_back)
147+
if hasattr(aw, 'slope') and in_dtype in FLOAT_TYPES:
148+
# Finite scaling sets infs to min / max
149+
exp_back = np.clip(exp_back, 0, 1)
150+
else:
151+
exp_back = np.clip(exp_back, *shared_range(float, out_dtype))
152+
else: # iu input type
153+
mn_out, mx_out = _dt_min_max(out_dtype)
154+
if (mn_in, mx_in) != (mn_out, mx_out):
155+
exp_back = np.clip(exp_back,
156+
max(mn_in, mn_out),
157+
min(mx_in, mx_out))
158+
elif in_dtype in COMPLEX_TYPES: # always cast to real from cplx
159+
with suppress_warnings():
160+
exp_back = exp_back.astype(float)
149161
exp_back = exp_back.astype(out_dtype)
150162
# Sometimes working precision is float32 - allow for small differences
151163
assert_allclose_safely(back_arr, exp_back)

nibabel/tests/test_spm99analyze.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -343,15 +343,29 @@ def test_no_scaling(self):
343343
with suppress_warnings(): # invalid mult
344344
back_arr = rt_img.get_data()
345345
exp_back = arr.copy()
346-
if in_dtype not in COMPLEX_TYPES:
347-
exp_back = arr.astype(float)
348346
if out_dtype in IUINT_TYPES:
349-
with np.errstate(invalid='ignore'):
350-
exp_back = np.round(exp_back)
351-
exp_back = np.clip(exp_back, *shared_range(float, out_dtype))
352-
exp_back = exp_back.astype(out_dtype).astype(float)
353-
else:
347+
if in_dtype in FLOAT_TYPES:
348+
# Working precision is (at least) float
349+
exp_back = exp_back.astype(float)
350+
with np.errstate(invalid='ignore'):
351+
exp_back = np.round(exp_back)
352+
if in_dtype in FLOAT_TYPES:
353+
# Clip to shared range of working precision
354+
exp_back = np.clip(exp_back,
355+
*shared_range(float, out_dtype))
356+
else: # iu input type
357+
# Clipping only to integer range
358+
mn_out, mx_out = _dt_min_max(out_dtype)
359+
if (mn_in, mx_in) != (mn_out, mx_out):
360+
exp_back = np.clip(exp_back,
361+
max(mn_in, mn_out),
362+
min(mx_in, mx_out))
363+
# exp_back = exp_back.astype(out_dtype)
364+
if out_dtype in COMPLEX_TYPES:
354365
exp_back = exp_back.astype(out_dtype)
366+
else:
367+
# Cast to working precision
368+
exp_back = exp_back.astype(float)
355369
# Allow for small differences in large numbers
356370
with suppress_warnings(): # invalid value
357371
assert_allclose_safely(back_arr,

nibabel/tests/test_utils.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,9 @@ def test_a2f_scaled_unscaled():
536536
mn_in, mx_in = _dt_min_max(in_dtype)
537537
nan_val = np.nan if in_dtype in CFLOAT_TYPES else 10
538538
arr = np.array([mn_in, -1, 0, 1, mx_in, nan_val], dtype=in_dtype)
539+
iu_via_float = (in_dtype in CFLOAT_TYPES or
540+
(intercept, divslope) != (0, 1) or
541+
out_dtype in CFLOAT_TYPES)
539542
mn_out, mx_out = _dt_min_max(out_dtype)
540543
nan_fill = -intercept / divslope
541544
if out_dtype in IUINT_TYPES:
@@ -555,20 +558,24 @@ def test_a2f_scaled_unscaled():
555558
divslope=divslope,
556559
intercept=intercept)
557560
exp_back = arr.copy()
558-
if out_dtype in IUINT_TYPES:
559-
exp_back[np.isnan(exp_back)] = 0
560-
if in_dtype not in COMPLEX_TYPES:
561-
exp_back = exp_back.astype(float)
562-
if intercept != 0:
563-
exp_back -= intercept
564-
if divslope != 1:
565-
exp_back /= divslope
566-
if out_dtype in IUINT_TYPES:
567-
exp_back = np.round(exp_back).astype(float)
568-
exp_back = np.clip(exp_back, *shared_range(float, out_dtype))
569-
exp_back = exp_back.astype(out_dtype)
570-
else:
571-
exp_back = exp_back.astype(out_dtype)
561+
if iu_via_float:
562+
if out_dtype in IUINT_TYPES:
563+
exp_back[np.isnan(exp_back)] = 0
564+
if in_dtype not in COMPLEX_TYPES:
565+
exp_back = exp_back.astype(float)
566+
if intercept != 0:
567+
exp_back -= intercept
568+
if divslope != 1:
569+
exp_back /= divslope
570+
if out_dtype in IUINT_TYPES:
571+
exp_back = np.round(exp_back).astype(float)
572+
exp_back = np.clip(exp_back, *shared_range(float, out_dtype))
573+
else: # Not via float, direct iu to iu casting
574+
if (mn_in, mx_in) != (mn_out, mx_out):
575+
exp_back = np.clip(exp_back,
576+
max(mn_in, mn_out),
577+
min(mx_in, mx_out))
578+
exp_back = exp_back.astype(out_dtype)
572579
# Allow for small differences in large numbers
573580
assert_allclose_safely(back_arr, exp_back)
574581

0 commit comments

Comments
 (0)