Skip to content

Commit f1e1008

Browse files
authored
Merge pull request #844 from effigies/fix/arraylike_dtype
FIX: Accept dtype parameter to ArrayProxy.__array__
2 parents fcc5448 + 3351d16 commit f1e1008

File tree

8 files changed

+89
-111
lines changed

8 files changed

+89
-111
lines changed

Changelog

+7-4
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,13 @@ References like "pr/298" refer to github pull request numbers.
3030

3131
New features
3232
------------
33-
* ArrayProxy method ``get_scaled()`` scales data with a dtype of a
34-
specified precision, promoting as necessary to avoid overflow. This
35-
is to used in ``img.get_fdata()`` to control memory usage. (pr/833)
36-
(CM, reviewed by Ross Markello)
33+
* ArrayProxy ``__array__()`` now accepts a ``dtype`` parameter, allowing
34+
``numpy.array(dataobj, dtype=...)`` calls, as well as casting directly
35+
with a dtype (for example, ``numpy.float32(dataobj)``) to control the
36+
output type. Scale factors (slope, intercept) are applied, but may be
37+
cast to narrower types, to control memory usage. This is now the basis
38+
of ``img.get_fdata()``, which will scale data in single precision if
39+
the output type is ``float32``. (pr/844) (CM, reviewed by ...)
3740
* GiftiImage method ``agg_data()`` to return usable data arrays (pr/793)
3841
(Hao-Ting Wang, reviewed by CM)
3942
* Accept ``os.PathLike`` objects in place of filenames (pr/610) (Cameron

nibabel/arrayproxy.py

+15-19
Original file line numberDiff line numberDiff line change
@@ -378,35 +378,31 @@ def get_unscaled(self):
378378
"""
379379
return self._get_unscaled(slicer=())
380380

381-
def get_scaled(self, dtype=None):
382-
""" Read data from file and apply scaling
381+
def __array__(self, dtype=None):
382+
""" Read data from file and apply scaling, casting to ``dtype``
383383
384-
The dtype of the returned array is the narrowest dtype that can
385-
represent the data without overflow, and is at least as wide as
386-
the dtype parameter.
384+
If ``dtype`` is unspecified, the dtype of the returned array is the
385+
narrowest dtype that can represent the data without overflow.
386+
Generally, it is the wider of the dtypes of the slopes or intercepts.
387387
388-
If dtype is unspecified, it is the wider of the dtypes of the slope
389-
or intercept. This will generally be determined by the parameter
390-
size in the image header, and so should be consistent for a given
391-
image format, but may vary across formats. Notably, these factors
392-
are single-precision (32-bit) floats for NIfTI-1 and double-precision
393-
(64-bit) floats for NIfTI-2.
388+
The types of the scale factors will generally be determined by the
389+
parameter size in the image header, and so should be consistent for a
390+
given image format, but may vary across formats.
394391
395392
Parameters
396393
----------
397-
dtype : numpy dtype specifier
398-
A numpy dtype specifier specifying the narrowest acceptable
399-
dtype.
394+
dtype : numpy dtype specifier, optional
395+
A numpy dtype specifier specifying the type of the returned array.
400396
401397
Returns
402398
-------
403399
array
404-
Scaled of image data of data type `dtype`.
400+
Scaled image data with type `dtype`.
405401
"""
406-
return self._get_scaled(dtype=dtype, slicer=())
407-
408-
def __array__(self):
409-
return self._get_scaled(dtype=None, slicer=())
402+
arr = self._get_scaled(dtype=dtype, slicer=())
403+
if dtype is not None:
404+
arr = arr.astype(dtype, copy=False)
405+
return arr
410406

411407
def __getitem__(self, slicer):
412408
return self._get_scaled(dtype=None, slicer=slicer)

nibabel/dataobj_images.py

+3-8
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
import numpy as np
1212

13-
from .arrayproxy import is_proxy
1413
from .filebasedimages import FileBasedImage
1514
from .keywordonly import kw_only_meth
1615
from .deprecated import deprecate_with_version
@@ -351,14 +350,10 @@ def get_fdata(self, caching='fill', dtype=np.float64):
351350
if self._fdata_cache is not None:
352351
if self._fdata_cache.dtype.type == dtype.type:
353352
return self._fdata_cache
354-
dataobj = self._dataobj
355-
# Attempt to confine data array to dtype during scaling
356-
# On overflow, may still upcast
357-
if is_proxy(dataobj):
358-
dataobj = dataobj.get_scaled(dtype=dtype)
359353
# Always return requested data type
360-
# For array proxies, will only copy on overflow
361-
data = np.asanyarray(dataobj, dtype=dtype)
354+
# For array proxies, will attempt to confine data array to dtype
355+
# during scaling
356+
data = np.asanyarray(self._dataobj, dtype=dtype)
362357
if caching == 'fill':
363358
self._fdata_cache = data
364359
return data

nibabel/ecat.py

+14-27
Original file line numberDiff line numberDiff line change
@@ -689,46 +689,33 @@ def ndim(self):
689689
def is_proxy(self):
690690
return True
691691

692-
def __array__(self):
692+
def __array__(self, dtype=None):
693693
''' Read of data from file
694694
695695
This reads ALL FRAMES into one array, can be memory expensive.
696696
697697
If you want to read only some slices, use the slicing syntax
698698
(``__getitem__``) below, or ``subheader.data_from_fileobj(frame)``
699-
'''
700-
data = np.empty(self.shape)
701-
frame_mapping = get_frame_order(self._subheader._mlist)
702-
for i in sorted(frame_mapping):
703-
data[:, :, :, i] = self._subheader.data_from_fileobj(
704-
frame_mapping[i][0])
705-
return data
706-
707-
def get_scaled(self, dtype=None):
708-
""" Read data from file and apply scaling
709-
710-
The dtype of the returned array is the narrowest dtype that can
711-
represent the data without overflow, and is at least as wide as
712-
the dtype parameter.
713-
714-
If dtype is unspecified, it is automatically determined.
715699
716700
Parameters
717701
----------
718-
dtype : numpy dtype specifier
719-
A numpy dtype specifier specifying the narrowest acceptable
720-
dtype.
702+
dtype : numpy dtype specifier, optional
703+
A numpy dtype specifier specifying the type of the returned array.
721704
722705
Returns
723706
-------
724707
array
725-
Scaled of image data of data type `dtype`.
726-
"""
727-
data = self.__array__()
728-
if dtype is None:
729-
return data
730-
final_type = np.promote_types(data.dtype, dtype)
731-
return data.astype(final_type, copy=False)
708+
Scaled image data with type `dtype`.
709+
'''
710+
# dtype=None is interpreted as float64
711+
data = np.empty(self.shape)
712+
frame_mapping = get_frame_order(self._subheader._mlist)
713+
for i in sorted(frame_mapping):
714+
data[:, :, :, i] = self._subheader.data_from_fileobj(
715+
frame_mapping[i][0])
716+
if dtype is not None:
717+
data = data.astype(dtype, copy=False)
718+
return data
732719

733720
def __getitem__(self, sliceobj):
734721
""" Return slice `sliceobj` from ECAT data, optimizing if possible

nibabel/minc1.py

+11-24
Original file line numberDiff line numberDiff line change
@@ -261,42 +261,29 @@ def ndim(self):
261261
def is_proxy(self):
262262
return True
263263

264-
def _get_scaled(self, dtype, slicer):
265-
data = self.minc_file.get_scaled_data(slicer)
266-
if dtype is None:
267-
return data
268-
final_type = np.promote_types(data.dtype, dtype)
269-
return data.astype(final_type, copy=False)
270-
271-
def get_scaled(self, dtype=None):
272-
""" Read data from file and apply scaling
264+
def __array__(self, dtype=None):
265+
""" Read data from file and apply scaling, casting to ``dtype``
273266
274-
The dtype of the returned array is the narrowest dtype that can
275-
represent the data without overflow, and is at least as wide as
276-
the dtype parameter.
277-
278-
If dtype is unspecified, it is automatically determined.
267+
If ``dtype`` is unspecified, the dtype is automatically determined.
279268
280269
Parameters
281270
----------
282-
dtype : numpy dtype specifier
283-
A numpy dtype specifier specifying the narrowest acceptable
284-
dtype.
271+
dtype : numpy dtype specifier, optional
272+
A numpy dtype specifier specifying the type of the returned array.
285273
286274
Returns
287275
-------
288276
array
289-
Scaled of image data of data type `dtype`.
277+
Scaled image data with type `dtype`.
290278
"""
291-
return self._get_scaled(dtype=dtype, slicer=())
292-
293-
def __array__(self):
294-
''' Read of data from file '''
295-
return self._get_scaled(dtype=None, slicer=())
279+
arr = self.minc_file.get_scaled_data(sliceobj=())
280+
if dtype is not None:
281+
arr = arr.astype(dtype, copy=False)
282+
return arr
296283

297284
def __getitem__(self, sliceobj):
298285
""" Read slice `sliceobj` of data from file """
299-
return self._get_scaled(dtype=None, slicer=sliceobj)
286+
return self.minc_file.get_scaled_data(sliceobj)
300287

301288

302289
class MincHeader(SpatialHeader):

nibabel/parrec.py

+12-16
Original file line numberDiff line numberDiff line change
@@ -676,31 +676,27 @@ def get_unscaled(self):
676676
"""
677677
return self._get_unscaled(slicer=())
678678

679-
def get_scaled(self, dtype=None):
680-
""" Read data from file and apply scaling
679+
def __array__(self, dtype=None):
680+
""" Read data from file and apply scaling, casting to ``dtype``
681681
682-
The dtype of the returned array is the narrowest dtype that can
683-
represent the data without overflow, and is at least as wide as
684-
the dtype parameter.
685-
686-
If dtype is unspecified, it is the wider of the dtypes of the slopes
687-
or intercepts
682+
If ``dtype`` is unspecified, the dtype of the returned array is the
683+
narrowest dtype that can represent the data without overflow.
684+
Generally, it is the wider of the dtypes of the slopes or intercepts.
688685
689686
Parameters
690687
----------
691-
dtype : numpy dtype specifier
692-
A numpy dtype specifier specifying the narrowest acceptable
693-
dtype.
688+
dtype : numpy dtype specifier, optional
689+
A numpy dtype specifier specifying the type of the returned array.
694690
695691
Returns
696692
-------
697693
array
698-
Scaled of image data of data type `dtype`.
694+
Scaled image data with type `dtype`.
699695
"""
700-
return self._get_scaled(dtype=dtype, slicer=())
701-
702-
def __array__(self):
703-
return self._get_scaled(dtype=None, slicer=())
696+
arr = self._get_scaled(dtype=dtype, slicer=())
697+
if dtype is not None:
698+
arr = arr.astype(dtype, copy=False)
699+
return arr
704700

705701
def __getitem__(self, slicer):
706702
return self._get_scaled(dtype=None, slicer=slicer)

nibabel/tests/test_api_validators.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def meth(self):
1919
for imaker, params in self.obj_params():
2020
validator(self, imaker, params)
2121
meth.__name__ = 'test_' + name[len('validate_'):]
22-
meth.__doc__ = 'autogenerated test from ' + name
22+
meth.__doc__ = 'autogenerated test from {}.{}'.format(klass.__name__, name)
2323
return meth
2424
for name in dir(klass):
2525
if not name.startswith('validate_'):

nibabel/tests/test_proxy_api.py

+26-12
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757

5858
from numpy.testing import assert_almost_equal, assert_array_equal, assert_allclose
5959

60-
from ..testing import data_path as DATA_PATH, assert_dt_equal
60+
from ..testing import data_path as DATA_PATH, assert_dt_equal, clear_and_catch_warnings
6161

6262
from ..tmpdirs import InTemporaryDirectory
6363

@@ -132,24 +132,38 @@ def validate_asarray(self, pmaker, params):
132132
# Shape matches expected shape
133133
assert_equal(out.shape, params['shape'])
134134

135-
def validate_get_scaled(self, pmaker, params):
135+
def validate_array_interface_with_dtype(self, pmaker, params):
136136
# Check proxy returns expected array from asarray
137137
prox, fio, hdr = pmaker()
138-
out = prox.get_scaled()
139-
assert_array_equal(out, params['arr_out'])
140-
assert_dt_equal(out.dtype, params['dtype_out'])
141-
# Shape matches expected shape
142-
assert_equal(out.shape, params['shape'])
138+
orig = np.array(prox, dtype=None)
139+
assert_array_equal(orig, params['arr_out'])
140+
assert_dt_equal(orig.dtype, params['dtype_out'])
141+
142+
context = None
143+
if np.issubdtype(orig.dtype, np.complexfloating):
144+
context = clear_and_catch_warnings()
145+
context.__enter__()
146+
warnings.simplefilter('ignore', np.ComplexWarning)
143147

144148
for dtype in np.sctypes['float'] + np.sctypes['int'] + np.sctypes['uint']:
145-
out = prox.get_scaled(dtype=dtype)
149+
# Directly coerce with a dtype
150+
direct = dtype(prox)
146151
# Half-precision is imprecise. Obviously. It's a bad idea, but don't break
147152
# the test over it.
148153
rtol = 1e-03 if dtype == np.float16 else 1e-05
149-
assert_allclose(out, params['arr_out'].astype(out.dtype), rtol=rtol, atol=1e-08)
150-
assert_greater_equal(out.dtype, np.dtype(dtype))
151-
# Shape matches expected shape
152-
assert_equal(out.shape, params['shape'])
154+
assert_allclose(direct, orig.astype(dtype), rtol=rtol, atol=1e-08)
155+
assert_dt_equal(direct.dtype, np.dtype(dtype))
156+
assert_equal(direct.shape, params['shape'])
157+
# All three methods should produce equivalent results
158+
for arrmethod in (np.array, np.asarray, np.asanyarray):
159+
out = arrmethod(prox, dtype=dtype)
160+
assert_array_equal(out, direct)
161+
assert_dt_equal(out.dtype, np.dtype(dtype))
162+
# Shape matches expected shape
163+
assert_equal(out.shape, params['shape'])
164+
165+
if context is not None:
166+
context.__exit__()
153167

154168
def validate_header_isolated(self, pmaker, params):
155169
# Confirm altering input header has no effect

0 commit comments

Comments
 (0)