Skip to content

Commit 42e0315

Browse files
committed
DOC+RF: more comments / rewrite of scaling checks
I found the scaling checks difficult to follow, so I added some comments and refactored the scaling checks for the array_to_file routine.
1 parent cf7c130 commit 42e0315

File tree

4 files changed

+45
-19
lines changed

4 files changed

+45
-19
lines changed

nibabel/casting.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ class CastingError(Exception):
1414
pass
1515

1616

17-
#: Test for VC truncation when casting floats to uint64
17+
# Test for VC truncation when casting floats to uint64
18+
# Christoph Gohlke says this is so for MSVC <= 2010 because VC is using x87
19+
# instructions; see:
20+
# https://github.com/scipy/scipy/blob/99bb8411f6391d921cb3f4e56619291e91ddf43b/scipy/ndimage/tests/test_datatypes.py#L51
1821
_test_val = 2**63 + 2**11 # Should be exactly representable in float64
1922
TRUNC_UINT64 = np.float64(_test_val).astype(np.uint64) != _test_val
2023

nibabel/tests/test_arraywriters.py

+14-3
Original file line numberDiff line numberDiff line change
@@ -138,24 +138,35 @@ def test_no_scaling():
138138
with suppress_warnings():
139139
back_arr = round_trip(aw)
140140
exp_back = arr.copy()
141+
# If converting to floating point type, casting is direct.
142+
# Otherwise we will need to do float-(u)int casting at some point.
141143
if out_dtype in IUINT_TYPES:
142144
if in_dtype in CFLOAT_TYPES:
145+
# Working precision is (at least) float
143146
with suppress_warnings():
144147
exp_back = exp_back.astype(float)
148+
# Float to iu conversion will always round, clip
145149
with np.errstate(invalid='ignore'):
146150
exp_back = np.round(exp_back)
147151
if hasattr(aw, 'slope') and in_dtype in FLOAT_TYPES:
148152
# Finite scaling sets infs to min / max
149153
exp_back = np.clip(exp_back, 0, 1)
150154
else:
151-
exp_back = np.clip(exp_back, *shared_range(float, out_dtype))
152-
else: # iu input type
155+
# Clip to shared range of working precision
156+
exp_back = np.clip(exp_back,
157+
*shared_range(float, out_dtype))
158+
else: # iu input and output type
159+
# No scaling, never gets converted to float.
160+
# Does get clipped to range of output type
153161
mn_out, mx_out = _dt_min_max(out_dtype)
154162
if (mn_in, mx_in) != (mn_out, mx_out):
163+
# Use smaller of input, output range to avoid np.clip
164+
# upcasting the array because of large clip limits.
155165
exp_back = np.clip(exp_back,
156166
max(mn_in, mn_out),
157167
min(mx_in, mx_out))
158-
elif in_dtype in COMPLEX_TYPES: # always cast to real from cplx
168+
elif in_dtype in COMPLEX_TYPES:
169+
# always cast to real from complex
159170
with suppress_warnings():
160171
exp_back = exp_back.astype(float)
161172
exp_back = exp_back.astype(out_dtype)

nibabel/tests/test_spm99analyze.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -343,25 +343,31 @@ 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 converting to floating point type, casting is direct.
347+
# Otherwise we will need to do float-(u)int casting at some point
346348
if out_dtype in IUINT_TYPES:
347349
if in_dtype in FLOAT_TYPES:
348350
# Working precision is (at least) float
349351
exp_back = exp_back.astype(float)
352+
# Float to iu conversion will always round, clip
350353
with np.errstate(invalid='ignore'):
351354
exp_back = np.round(exp_back)
352355
if in_dtype in FLOAT_TYPES:
353356
# Clip to shared range of working precision
354357
exp_back = np.clip(exp_back,
355358
*shared_range(float, out_dtype))
356-
else: # iu input type
357-
# Clipping only to integer range
359+
else: # iu input and output type
360+
# No scaling, never gets converted to float.
361+
# Does get clipped to range of output type
358362
mn_out, mx_out = _dt_min_max(out_dtype)
359363
if (mn_in, mx_in) != (mn_out, mx_out):
364+
# Use smaller of input, output range to avoid np.clip
365+
# upcasting the array because of large clip limits.
360366
exp_back = np.clip(exp_back,
361367
max(mn_in, mn_out),
362368
min(mx_in, mx_out))
363-
# exp_back = exp_back.astype(out_dtype)
364369
if out_dtype in COMPLEX_TYPES:
370+
# always cast to real from complex
365371
exp_back = exp_back.astype(out_dtype)
366372
else:
367373
# Cast to working precision

nibabel/tests/test_utils.py

+18-12
Original file line numberDiff line numberDiff line change
@@ -536,13 +536,12 @@ 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)
542539
mn_out, mx_out = _dt_min_max(out_dtype)
540+
# 0 when scaled to output will also be the output value for NaN
543541
nan_fill = -intercept / divslope
544542
if out_dtype in IUINT_TYPES:
545543
nan_fill = np.round(nan_fill)
544+
# nan2zero will check whether 0 in scaled to a valid value in output
546545
if (in_dtype in CFLOAT_TYPES and not mn_out <= nan_fill <= mx_out):
547546
assert_raises(ValueError,
548547
array_to_file,
@@ -552,29 +551,36 @@ def test_a2f_scaled_unscaled():
552551
divslope=divslope,
553552
intercept=intercept)
554553
continue
555-
with suppress_warnings(): # cast to real
554+
with suppress_warnings():
556555
back_arr = write_return(arr, fobj,
557556
out_dtype=out_dtype,
558557
divslope=divslope,
559558
intercept=intercept)
560559
exp_back = arr.copy()
561-
if iu_via_float:
562-
if out_dtype in IUINT_TYPES:
560+
if (in_dtype in IUINT_TYPES and
561+
out_dtype in IUINT_TYPES and
562+
(intercept, divslope) == (0, 1)):
563+
# Direct iu to iu casting.
564+
# Need to clip if ranges not the same.
565+
# Use smaller of input, output range to avoid np.clip upcasting
566+
# the array because of large clip limits.
567+
if (mn_in, mx_in) != (mn_out, mx_out):
568+
exp_back = np.clip(exp_back,
569+
max(mn_in, mn_out),
570+
min(mx_in, mx_out))
571+
else: # Need to deal with nans, casting to float, clipping
572+
if in_dtype in CFLOAT_TYPES and out_dtype in IUINT_TYPES:
563573
exp_back[np.isnan(exp_back)] = 0
564574
if in_dtype not in COMPLEX_TYPES:
565575
exp_back = exp_back.astype(float)
566576
if intercept != 0:
567577
exp_back -= intercept
568578
if divslope != 1:
569579
exp_back /= divslope
570-
if out_dtype in IUINT_TYPES:
580+
if (exp_back.dtype.type in CFLOAT_TYPES and
581+
out_dtype in IUINT_TYPES):
571582
exp_back = np.round(exp_back).astype(float)
572583
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))
578584
exp_back = exp_back.astype(out_dtype)
579585
# Allow for small differences in large numbers
580586
assert_allclose_safely(back_arr, exp_back)

0 commit comments

Comments
 (0)