Skip to content

Commit f1e32ea

Browse files
committed
NF+TST - careful upcasting for very large values
Previous scaling incarnation was using longdouble for intermediate calculations. With the new arraywriters, I was managing the slope and intercept precision, but had missed the situation where large (positive or negative) floating point values get pushed out of the current type range by scaling and get clipped, causing large loss of precision for those values. I test for this situation during application of the scale and upcast the slope, inter type accordingly.
1 parent 51e8790 commit f1e32ea

File tree

2 files changed

+340
-26
lines changed

2 files changed

+340
-26
lines changed

nibabel/tests/test_utils.py

Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,20 @@
2121
can_cast,
2222
write_zeros,
2323
apply_read_scaling,
24+
working_type,
25+
best_write_scale_ftype,
26+
better_float_of,
2427
int_scinter_ftype,
2528
allopen,
2629
make_dt_codes,
2730
native_code,
2831
shape_zoom_affine,
29-
rec2dict)
32+
rec2dict,
33+
IUINT_TYPES,
34+
FLOAT_TYPES,
35+
NUMERIC_TYPES)
3036

31-
from ..casting import flt2nmant, floor_log2
37+
from ..casting import flt2nmant, FloatingError, floor_log2
3238

3339
from numpy.testing import (assert_array_almost_equal,
3440
assert_array_equal)
@@ -121,6 +127,26 @@ def test_a2f_intercept_scale():
121127
assert_array_equal(data_back, (arr-1) / 2.0)
122128

123129

130+
def test_a2f_upscale():
131+
# Test working type scales with needed range
132+
info = np.finfo(np.float32)
133+
# Test values discovered from stress testing. The largish value (2**115)
134+
# overflows to inf after the intercept is subtracted, using float32 as the
135+
# working precision. The difference between inf and this value is lost.
136+
arr = np.array([[info.min, 2**115, info.max]], dtype=np.float32)
137+
slope = np.float32(2**121)
138+
inter = info.min
139+
str_io = BytesIO()
140+
# We need to provide mn, mx for function to be able to calculate upcasting
141+
array_to_file(arr, str_io, np.uint8, intercept=inter, divslope=slope,
142+
mn = info.min, mx = info.max)
143+
raw = array_from_file(arr.shape, np.uint8, str_io)
144+
back = apply_read_scaling(raw, slope, inter)
145+
top = back - arr
146+
score = np.abs(top / arr)
147+
assert_true(np.all(score < 10))
148+
149+
124150
def test_a2f_min_max():
125151
str_io = BytesIO()
126152
for in_dt in (np.float32, np.int8):
@@ -270,6 +296,110 @@ def test_int_scinter():
270296
assert_equal(int_scinter_ftype(np.int8, -1e38, 0.0), np.float64)
271297

272298

299+
def test_working_type():
300+
# Which type do input types with slope and inter cast to in numpy?
301+
# Wrapper function because we need to use the dtype str for comparison. We
302+
# need this because of the very confusing np.int32 != np.intp (on 32 bit).
303+
def wt(*args, **kwargs):
304+
return np.dtype(working_type(*args, **kwargs)).str
305+
d1 = np.atleast_1d
306+
for in_type in NUMERIC_TYPES:
307+
in_ts = np.dtype(in_type).str
308+
assert_equal(wt(in_type), in_ts)
309+
assert_equal(wt(in_type, 1, 0), in_ts)
310+
assert_equal(wt(in_type, 1.0, 0.0), in_ts)
311+
in_val = d1(in_type(0))
312+
for slope_type in NUMERIC_TYPES:
313+
sl_val = slope_type(1) # no scaling, regardless of type
314+
assert_equal(wt(in_type, sl_val, 0.0), in_ts)
315+
sl_val = slope_type(2) # actual scaling
316+
out_val = in_val / d1(sl_val)
317+
assert_equal(wt(in_type, sl_val), out_val.dtype.str)
318+
for inter_type in NUMERIC_TYPES:
319+
i_val = inter_type(0) # no scaling, regardless of type
320+
assert_equal(wt(in_type, 1, i_val), in_ts)
321+
i_val = inter_type(1) # actual scaling
322+
out_val = in_val - d1(i_val)
323+
assert_equal(wt(in_type, 1, i_val), out_val.dtype.str)
324+
# Combine scaling and intercept
325+
out_val = (in_val - d1(i_val)) / d1(sl_val)
326+
assert_equal(wt(in_type, sl_val, i_val), out_val.dtype.str)
327+
# Confirm that type codes and dtypes work as well
328+
f32s = np.dtype(np.float32).str
329+
assert_equal(wt('f4', 1, 0), f32s)
330+
assert_equal(wt(np.dtype('f4'), 1, 0), f32s)
331+
332+
333+
def test_better_float():
334+
# Better float function
335+
def check_against(f1, f2):
336+
return f1 if FLOAT_TYPES.index(f1) >= FLOAT_TYPES.index(f2) else f2
337+
for first in FLOAT_TYPES:
338+
for other in IUINT_TYPES + np.sctypes['complex']:
339+
assert_equal(better_float_of(first, other), first)
340+
assert_equal(better_float_of(other, first), first)
341+
for other2 in IUINT_TYPES + np.sctypes['complex']:
342+
assert_equal(better_float_of(other, other2), np.float32)
343+
assert_equal(better_float_of(other, other2, np.float64),
344+
np.float64)
345+
for second in FLOAT_TYPES:
346+
assert_equal(better_float_of(first, second),
347+
check_against(first, second))
348+
# Check codes and dtypes work
349+
assert_equal(better_float_of('f4', 'f8', 'f4'), np.float64)
350+
assert_equal(better_float_of('i4', 'i8', 'f8'), np.float64)
351+
352+
353+
def have_longer_double():
354+
# True if longdouble has more precision than float64, and longdouble is
355+
# something we can rely on
356+
nmant_64 = flt2nmant(np.float64) # should be 52
357+
try:
358+
nmant_ld = flt2nmant(np.longdouble)
359+
except FloatingError:
360+
return False
361+
return nmant_ld > nmant_64
362+
363+
364+
def test_best_write_scale_ftype():
365+
# Test best write scaling type
366+
# Types return better of (default, array type) unless scale overflows.
367+
# Return float type cannot be less capable than the input array type
368+
for dtt in IUINT_TYPES + FLOAT_TYPES:
369+
arr = np.arange(10, dtype=dtt)
370+
assert_equal(best_write_scale_ftype(arr, 1, 0),
371+
better_float_of(dtt, np.float32))
372+
assert_equal(best_write_scale_ftype(arr, 1, 0, np.float64),
373+
better_float_of(dtt, np.float64))
374+
assert_equal(best_write_scale_ftype(arr, np.float32(2), 0),
375+
better_float_of(dtt, np.float32))
376+
assert_equal(best_write_scale_ftype(arr, 1, np.float32(1)),
377+
better_float_of(dtt, np.float32))
378+
# Overflowing ints with scaling results in upcast
379+
best_vals = ((np.float32, np.float64),)
380+
if have_longer_double():
381+
best_vals += ((np.float64, np.longdouble),)
382+
for lower_t, higher_t in best_vals:
383+
# Information on this float
384+
t_max = np.finfo(lower_t).max
385+
nmant = flt2nmant(lower_t) # number of significand digits
386+
big_delta = lower_t(2**(floor_log2(t_max) - nmant)) # delta below max
387+
# Even large values that don't overflow don't change output
388+
arr = np.array([0, t_max], dtype=lower_t)
389+
assert_equal(best_write_scale_ftype(arr, 1, 0), lower_t)
390+
# Scaling > 1 reduces output values, so no upcast needed
391+
assert_equal(best_write_scale_ftype(arr, lower_t(1.01), 0), lower_t)
392+
# Scaling < 1 increases values, so upcast may be needed (and is here)
393+
assert_equal(best_write_scale_ftype(arr, lower_t(0.99), 0), higher_t)
394+
# Large minus offset on large array can cause upcast
395+
assert_equal(best_write_scale_ftype(arr, 1, -big_delta/2.01), lower_t)
396+
assert_equal(best_write_scale_ftype(arr, 1, -big_delta/2.0), higher_t)
397+
# With infs already in input, default type returns
398+
arr[0] = np.inf
399+
assert_equal(best_write_scale_ftype(arr, lower_t(0.5), 0), lower_t)
400+
arr[0] = -np.inf
401+
assert_equal(best_write_scale_ftype(arr, lower_t(0.5), 0), lower_t)
402+
273403

274404
def test_can_cast():
275405
tests = ((np.float32, np.float32, True, True, True),

0 commit comments

Comments
 (0)