Skip to content

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

Merged
merged 16 commits into from
Apr 6, 2017
Merged

ENH: ArrayProxy reshape #521

merged 16 commits into from
Apr 6, 2017

Conversation

effigies
Copy link
Member

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.)

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.003%) to 96.154% when pulling 8b41f55 on effigies:ap_reshape into ca977ab on nipy:master.

@codecov-io
Copy link

codecov-io commented Mar 25, 2017

Codecov Report

Merging #521 into master will increase coverage by 0.03%.
The diff coverage is 100%.

Impacted file tree graph

@@            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
Impacted Files Coverage Δ
nibabel/cifti2/parse_cifti2.py 83.76% <ø> (+0.8%) ⬆️
nibabel/arrayproxy.py 100% <100%> (ø) ⬆️
nibabel/tests/test_proxy_api.py 98.63% <100%> (ø) ⬆️
nibabel/cifti2/tests/test_cifti2io.py 98.29% <100%> (ø) ⬆️
nibabel/tests/test_arrayproxy.py 100% <100%> (ø) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 8de91a1...37405c3. Read the comment docs.

@demianw
Copy link
Contributor

demianw commented Mar 25, 2017

This seems to solve the issue!
Just that, to close #520
we need to also include this patch

--- a/nibabel/cifti2/parse_cifti2.py
+++ b/nibabel/cifti2/parse_cifti2.py
@@ -147,7 +147,6 @@ class _Cifti2AsNiftiImage(Nifti2Image):
         if self.cifti_img is None:
             raise ValueError('Nifti2 header does not contain a CIFTI2 '
                              'extension')
-        self.cifti_img.data = self.get_data()


 class Cifti2Parser(xml.XmlParser):

@matthew-brett
Copy link
Member

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

    # alternative signature 0 (current signature)
    def __init__(self, file_like, header, mmap=True):
    # alternative signature 1 (new signature)
    def __init__(self, file_like, shape, storage_dtype, offset=0, slope_inter=None, mmap=True)

Then we could use the second signature for the reshaped arrayproxy. The mmap argument is already keyword only, so this won't mess up calls with the current signature.

Then - it would be good to have more tests for new constructor and the reshape.

@@ -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()
Copy link
Member

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.

Copy link
Member Author

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

@effigies
Copy link
Member Author

@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.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.003%) to 96.154% when pulling c6192ad on effigies:ap_reshape into ca977ab on nipy:master.

@effigies
Copy link
Member Author

@matthew-brett It looks like the FutureWarning has been on ArrayProxy.header since the 2.0 release, so I think it's reasonable to fast-track deprecation to 3.0.

How about this approach:

  • Add (and document) signature 1
  • Add .from_header classmethod
  • Update signature 0 docs to point to .from_header, raise a FutureWarning if signature 0 is used
    • 3.0 (along with .header removal)
      • Move signature 0 to DeprecationWarning
      • Remove from docstring
    • 4.0
      • Remove signature 0 support

@effigies
Copy link
Member Author

No tests yet. Want to make sure I haven't broken old tests first.

@matthew-brett
Copy link
Member

I like the deprecation scheme...

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,
Copy link
Member

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?

Copy link
Member Author

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?

Copy link
Member

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)...

@effigies
Copy link
Member Author

@matthew-brett Ready for review.

Copy link
Member

@matthew-brett matthew-brett left a 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:]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice.

""" 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:
Copy link
Member

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.

Copy link
Member

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.

Copy link
Member Author

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')
Copy link
Member

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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One-liner docstring?

Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

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,
Copy link
Member

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,
Copy link
Member

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?

Copy link
Member Author

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.

@matthew-brett
Copy link
Member

We might also want to set self._header = None for the tuple input argument case, and test that the header attribute correctly returns None.

@effigies
Copy link
Member Author

I do set the default _header attribute to None: https://github.com/nipy/nibabel/pull/521/files#diff-644c66bb38fc2e28c018a29d87e96d36R69

Added a test.

Copy link
Member

@matthew-brett matthew-brett left a 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
Copy link
Member

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?

def reshape(self, shape):
''' Return an ArrayProxy with a new shape, without modifying data

``array_proxy.reshape(shape)`` is equivalent to
Copy link
Member

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.

# Calculate new shape if not fully specified
shape_arr = np.asarray(shape)
unknowns = shape_arr == -1
if len(unknowns) > 1:
Copy link
Member

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.

Copy link
Member

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))
Copy link
Member

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.

@effigies
Copy link
Member Author

Thanks for the review. Kind of rushed that last push without testing first.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.005%) to 96.163% when pulling 8ad9c32 on effigies:ap_reshape into 8de91a1 on nipy:master.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.01%) to 96.172% when pulling 37405c3 on effigies:ap_reshape into 8de91a1 on nipy:master.

@matthew-brett
Copy link
Member

Excellent - thanks. Good to go from my side. Any other comments?

@effigies
Copy link
Member Author

I'm all set. Thanks for the review. I'll let you merge.

@matthew-brett
Copy link
Member

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?

@effigies
Copy link
Member Author

Given that the way to reshape has been with reshape_dataobj, I was intending to leave it be. If we do add reshape in whatever form to the other proxies, I think we'd need to switch that to a try/catch block, rather than a hasattr check, though.

@matthew-brett matthew-brett merged commit 2584b23 into nipy:master Apr 6, 2017
@matthew-brett
Copy link
Member

Thanks - let's put this in now and reflect at leisure about the other classes.

@effigies effigies deleted the ap_reshape branch April 6, 2017 17:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Problem loading large CIFTI2 files in lastest updates
5 participants