-
Notifications
You must be signed in to change notification settings - Fork 260
ENH: ArrayProxy reshape #521
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Codecov Report
@@ Coverage Diff @@
## master #521 +/- ##
==========================================
+ Coverage 94.14% 94.17% +0.03%
==========================================
Files 175 175
Lines 23890 23930 +40
Branches 2568 2574 +6
==========================================
+ Hits 22492 22537 +45
+ Misses 918 916 -2
+ Partials 480 477 -3
Continue to review full report at Codecov.
|
This seems to solve the issue!
|
Thanks for taking the initiative on this. I've been thinking about this for a bit. Reshape is a good idea in general. The immediate problem is that we can expect the headers to be pretty big, and the ArrayProxy constructor copies them. More generally, I would really like to add an alternative constructor signature to ArrayProxy, as in
Then we could use the second signature for the reshaped arrayproxy. The Then - it would be good to have more tests for new constructor and the reshape. |
nibabel/cifti2/parse_cifti2.py
Outdated
@@ -147,7 +147,6 @@ def __init__(self, dataobj, affine, header=None, | |||
if self.cifti_img is None: | |||
raise ValueError('Nifti2 header does not contain a CIFTI2 ' | |||
'extension') | |||
self.cifti_img.data = self.get_data() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or could be self.cifti_img.dataobj = self.dataobj
. This doesn't load the data, but keeps it as an array proxy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure the cifti_img
attribute is doing any work at all. Even the check on the header is replicated here. I think it may be a refactoring hold-over that never got cleaned up. I'm pretty sure we could, without losing functionality, rewrite the entire class as:
class _Cifti2AsNiftiImage(Nifti2Image):
header_class = _Cifti2AsNiftiHeader
makeable = False
@demianw Great! Until this PR is finished, you can use c6192ad. @matthew-brett I'll see what I can do. I've got a lot on my plate at the moment, so this may take a little bit of time to iterate through. |
@matthew-brett It looks like the How about this approach:
|
No tests yet. Want to make sure I haven't broken old tests first. |
I like the deprecation scheme... |
nibabel/arrayproxy.py
Outdated
The header should also have a 'copy' method. This requirement will | ||
go away when the deprecated 'header' property goes away. | ||
|
||
(2) ArrayProxy(file_like, shape, storage_dtype, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now I think of it, maybe the second argument can be either:
- the header (as now)
- a tuple of length 2 - 5 containing (shape, storage_dtype, [offset [slope, [inter]]]).
Then the signature can stay the same length, at the expense of slightly greater complexity of explanation / signature. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that's a little less awkward. Do we still want to deprecate the header
path, then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I think we can leave the header path. I suppose we might rename the input argument, hoping that no-one out there is using ArrayProxy(header=something)
...
@matthew-brett Ready for review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks - a few questions / comments - but looking good.
self._header = spec.copy() | ||
elif 2 <= len(spec) <= 5: | ||
optional = (0, 1., 0.) | ||
par = spec + optional[len(spec) - 2:] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice.
nibabel/arrayproxy.py
Outdated
""" Initialize array proxy instance | ||
|
||
Parameters | ||
---------- | ||
file_like : object | ||
File-like object or filename. If file-like object, should implement | ||
at least ``read`` and ``seek``. | ||
header : object | ||
spec : object or tuple | ||
Tuple must have length 2-5, with the following fields: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggest 'values' instead of 'fields' - 'fields' (to me) implies names, which might be confusing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anal, I know, but I prefer full-stops at the end of sentences in the docstring.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Used a period here. The rest seemed to be more fragments than sentences, but I can add periods there, if they're preferred, stylistically.
|
||
@property | ||
@deprecate_with_version('ArrayProxy.header deprecated', '2.2', '3.0') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice.
@@ -162,6 +187,16 @@ def __getitem__(self, slicer): | |||
# Upcast as necessary for big slopes, intercepts | |||
return apply_read_scaling(raw_data, self._slope, self._inter) | |||
|
|||
def reshape(self, shape): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One-liner docstring?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be good to allow -1 as element in shape, for compatibility with numpy array reshape.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
nibabel/arrayproxy.py
Outdated
if np.prod(shape) != size: | ||
raise ValueError("cannot reshape array of size {:d} into shape " | ||
"{!s}".format(size, shape)) | ||
return ArrayProxy(file_like=self.file_like, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe self.__class__(file_like=self.file_like
... to allow subclassing.
makeable = False | ||
rw = True | ||
|
||
def __init__(self, dataobj, affine, header=None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this is because the cifti_img
attribute is not being used elsewhere?
Do we want to raise an error if no cifti extension found?
What is this subclass now for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The point of the subclass is to be a Nifti2Image
with this header, which has different constraints from a standard Nifti2Header
.
We might also want to set |
I do set the default Added a test. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nearly there - mainly some reshape stuff and tests.
): | ||
assert_array_equal(getattr(ap_header, method)(*args), | ||
getattr(ap_tuple, method)(*args)) | ||
# Tuple-defined ArrayProxies have no header to store |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one raises a warning no? Need to protect with warnings context manager?
nibabel/arrayproxy.py
Outdated
def reshape(self, shape): | ||
''' Return an ArrayProxy with a new shape, without modifying data | ||
|
||
``array_proxy.reshape(shape)`` is equivalent to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think these will give different results, no (I guess np.reshape will cast to an array, and it also allows the order
keyword)? Maybe omit this sentence.
nibabel/arrayproxy.py
Outdated
# Calculate new shape if not fully specified | ||
shape_arr = np.asarray(shape) | ||
unknowns = shape_arr == -1 | ||
if len(unknowns) > 1: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will be boolean array of same length as shape
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe
from operator import mul
from functools import reduce
n_unknowns = len([e for e in shape if e == -1])
if n_unknowns > 1:
raise ValueError("can only specify one unknown dimension")
elif n_unknowns == 1:
known_size = reduce(mul, shape, -1)
unknown_size = size // known_size
shape = [unknown_size if e == -1 else e for e in shape]
hdr = FunkyHeader(shape) | ||
bio = BytesIO() | ||
prox = ArrayProxy(bio, hdr) | ||
assert_true(isinstance(prox.reshape((2, 3, 4)), ArrayProxy)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need tests for -1 element, errors for incorrect size, to many -1s.
Thanks for the review. Kind of rushed that last push without testing first. |
Excellent - thanks. Good to go from my side. Any other comments? |
I'm all set. Thanks for the review. I'll let you merge. |
An extra thought - what should we do for the other ArrayProxy API objects such as MincImageArray, EcatImageArray, PARRECArrayProxy ? Leave reshape to generate an AttributeError? Raise a NotImplementedError? |
Given that the way to reshape has been with |
Thanks - let's put this in now and reflect at leisure about the other classes. |
Might as well start a PR. Are there any reasons this is a terrible idea?
Closes #520.
(Will close this if it doesn't actually resolve the issue.)