Skip to content

extract_array can give results of different size - #3333

Closed
hamogu wants to merge 19 commits into
astropy:masterfrom
hamogu:fix_3333
Closed

hamogu wants to merge 19 commits into
astropy:masterfrom
hamogu:fix_3333

Conversation

@hamogu

@hamogu hamogu commented Jan 26, 2015

Copy link
Copy Markdown
Member
>>> import numpy as np
>>> from astropy.nddata.utils import extract_array
>>> large_array = np.arange(30).reshape(5,6)
>>> extract_array(large_array, (3, 4), (0, 0))
array([[0, 1],
       [6, 7]])
>>> extract_array(large_array, (3, 4), (3, 3))
array([[13, 14, 15, 16],
       [19, 20, 21, 22],
       [25, 26, 27, 28]])

It would be useful for me if we had either:

  • an option to return where the position I put in falls on the new image ((0,0) in the first example and (1,2) in the second example) or
  • an option to always return a (3,4) array, but fill missing values in some way (e.g. nan or return a mask of invalid entries) or
  • both (via a keyword). What do people think?

This was originally astropy/imageutils#21 but got not resolved before merging with astropy, so I open it here again to raise visibility.

My use specific use case is PSF photometry (astropy/photutils#159) where I would like to extract sub-arrays or a large image and perform the PSF photometry on those subarrays. Unfortunately, with the current behaviour, I cannot easily relate the pixel coordinates of the large array and the subarray. The extract_array routine has all the code to check if I am extracting something in the middle of an array or on the edges (that's how it decides how large the return array is), but there is no way to propagate this information back to the caller.

I think this is a problem in almost any application of extract_array, my PSF photometry is just one concrete example.

CC: @astrofrog @larrybradley

@astrofrog

Copy link
Copy Markdown
Member

cc @adonath @mhvk

@mhvk

mhvk commented Jan 22, 2015

Copy link
Copy Markdown
Contributor

Cleanest may be to always return a MaskedArray, then the user can decide to do what they want to do if any elements are masked. Indeed, if the small array falls completely outside the large array, one could return a fully masked array. If we make this an option, then I'd suggest that for the normal case the code raises an exception if the requested region does not completely fall within the large array.

@embray embray added Affects-dev PRs and issues that do not impact an existing Astropy release nddata labels Jan 22, 2015
@larrybradley

Copy link
Copy Markdown
Member

I think extract_array should always return an array of the requested shape, so
+1 to @mhvk's suggestion for returning a MaskedArray and raising an exception if there is no overlap.

@astrofrog

Copy link
Copy Markdown
Member

+1

@mhvk

mhvk commented Jan 22, 2015

Copy link
Copy Markdown
Contributor

My suggestion was a bit more complicated, allowing one to ask for either masked or regular array, but always returning a masked array and raising an exception if fully outside is probably easier and thus better.

@hamogu

hamogu commented Jan 25, 2015

Copy link
Copy Markdown
Member Author

This turns out to be trickier than I thought, but I'm working on it.

@hamogu

hamogu commented Jan 26, 2015

Copy link
Copy Markdown
Member Author

OK, I pushed code. I suspect that this will not make it into 1.0 and thus I have not written anything into the CHANGES.rst yet.

@cdeil

cdeil commented Jan 26, 2015

Copy link
Copy Markdown
Member

travis-ci failed because of this Sphinx issue:
https://travis-ci.org/astropy/astropy/jobs/48295532#L4253

I'm not convinced this change is the right approach. Maybe it would be better to have functions that compute nd slices and then the user can decide what to do (e.g. change in place, or copy with or without padding) ... maybe using other helper functions.

I don't have a concrete suggestion at the moment, but my suggestion would be to think about this and discuss some more, unless most people think it's important to make this change before the 1.0 release.

@adonath You're using extract_array in gammapy/gammapy#234 ... what do you think?

@hamogu

hamogu commented Jan 26, 2015

Copy link
Copy Markdown
Member Author

@cdeil The function to compute ndslices is overlap_arrays. If a user wants just a slice, he/she can call that. All that extract_array does is to return large_array[slices] (padded with masked entries in my branch to ensure that the return array has the size requested).

@hamogu

hamogu commented Jan 26, 2015

Copy link
Copy Markdown
Member Author

I should point out that in the course of this PR, I found that the current implementation no only returns arrays or different sizes (which I consider a bug, but others may consider that a feature) it also gives clearly wrong results when called with invalid (negative) inputs (examples with current master, are fixed with the modified implementation in this PR):

>>> from astropy.nddata.utils import overlap_slices, extract_array
>>> l = np.arange(5)
>>> extract_array(l, (2,), (-5,))
array([0, 1])     # correct answer would be empty or (per discussion above) an exception
>>> extract_array(l, (2,), (-3,))
array([0, 1, 2, 3])   # incorrect and much longer than requested
>>> # It works again is the requested position is close to valid
>>> extract_array(l, (2,), (-2,))
array([], dtype=int32)

The problem is independent of the number of dimensions, so I chose 1d arrays in these example, but it is a problem for 2 d arrays, too:

>>> extract_array(la, (2,3), (-5,-4))
array([[0, 1, 2, 3]])

@cdeil

cdeil commented Jan 26, 2015

Copy link
Copy Markdown
Member

Ah, OK ... I didn't know overlap_slices was part of the public API.

Still ... aren't there use cases where one does want to get smaller cutouts at the edged (and not have those padded or masked)? I think that's what we need in gammapy/gammapy#234 .

Would it be bad to add an option to let the user control the behaviour at the large array edges where the small array doesn't completely fit?

@hamogu

hamogu commented Jan 26, 2015

Copy link
Copy Markdown
Member Author

When you use the current version of extract_array you get a cutout of unknown size. Let's say you have some X-ray (or gammy ray) event with a peak in pixel (x, y) and not you want to extract a 5*5 pixel island around it.
Let's say you determine the centroid of that and find it's at (0.5, 2.2) of the small array. How do you know what coordinate that is in the big array? Your code might try (x+0.5, y+2.2), but that is wrong at the edges.
Instead the code in gammypy would have to do:

#event on the left side of array:
if x < 5/2:
    x_event = x - (5 - small_array.shape[0]) / 2 + 0.5
else:
    x_event = x + 0.5
# event on left side of array:
if y < 5/2:
    y_event = y - (5 - small_array.shape[1]) / 2 + 2.2
else:
    y_event = y + 2.2

(Note that the example above does not include the correct rounding for even/odd array sizes and that in practice 5, 0.5 and 2.2 would be variable names.) Does that not look a little more confusing than it should be?

I'm not opposed to offering a keyword to switch that behaviour (that's what I suggested at the top of this thread) but I've been convinced that it would be easier to support one and only one way to do it.

@adonath

adonath commented Jan 26, 2015

Copy link
Copy Markdown
Member

@cdeil For gammapy/gammapy#234 it doesn't make a difference because the boundary handling is done outside the extract_array function. In general, I guess, that's what most users will do anyway, because it depends a lot on the application what you want to do with the extracted 'incomplete' arrays at the boundaries.

@hamogu Sorry for not commenting earlier. I'm not in favor of returning a MaskedArray, because it could change the type of the array. When you extract a smaller array out of a larger one you would expect it to have exactly the same type as the larger one.

I would keep the default behaviour as it is. That leaves is to the user, what to do with those 'incomplete' extracted arrays, because it think there are surely cases were you just don't care and discard them, as it was done in the photutils psf photometry prototype . But I would add an optional boundary parameter for convenience. It could even be the same options as in astropy.convolution.convolve i.e. 'none', 'fill', 'wrap' and 'extend' or possibly 'reflect'. But that might be overkill... and padding with NaN is sufficient. If the smaller array is completely outside the boundaries an error is raised.

Comment thread astropy/nddata/utils.py Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why the addition of np.floor? For positive numbers it does the same as int, while I'd think that negative positions would need special treatment.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is required to avoid errors with negative positions.
If we want to allow extraction of partially overlapping arrays with a central position to the left of the larger array (such as extract an array of size 5 with a center at -1, which still overlaps for two positions such as shown below)

----0123456789
  sssss

The extracted array s would be ---01.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Subject to the same caveat as my other comments: I thought "pos" would be the "position of the center" not the "index of the center". If that is indeed meant to be the index, then negative indices can be disallowed and there is no need for np.floor.

@astrofrog

Copy link
Copy Markdown
Member

Regarding the discussion re: maskedarray one can also return an array with NaN values outside the boundary. That could actually be a keyword argument if needed.

@mhvk

mhvk commented Jan 26, 2015

Copy link
Copy Markdown
Contributor

@hamogu - on the negative indices: yes, this is indeed inconsistent. In principle, there is no reason not to be able to give a negative offset -- meaning, as usual, counting from the far end of the array -- but the addition of the size should not be allowed to become positive. Still, the effort would likely not be worth the pain, so it may be easiest to just disallow negative indices.

@hamogu

hamogu commented Jan 26, 2015

Copy link
Copy Markdown
Member Author

What does a negative position mean?

I interpreted that as the position in a geometical sense (The array as indexes 0,1,2,3, ... ,so "-1" would be on to the left of "0"), but I now realize that this can also be interpreted as the "index of the position" (negative numbers simply mean it's counted from the right of the array.)

Which one was the intention of the original authors of this code?

If they intended the meaning "array indices" then I take everything back that I said and close this PR. In that case, I just suggest the simple addition of "Index of the Position of the small array's center, with respect to the large array." to the docstring.

@mhvk

mhvk commented Jan 26, 2015

Copy link
Copy Markdown
Contributor

@hamogu - for what it is worth, I think your interpretation is much more logical than mine: let's treat pos as a position, not an index, i.e., something that might be returned by a conversion from RA, Dec.

@adonath's point about keeping the array type the same is well-taken (especially since MaskedArray deals so poorly with subclasses). At the same time, I do like the idea that one always gets an array with the right shape back. Filling with nan would seem the best compromise.

@larrybradley

Copy link
Copy Markdown
Member

I'd second @astrofrog's suggestion that the fill value be a keyword parameter (e.g. with default=np.nan). The user could then later create a MaskedArray for the boundary cases if needed.

@adonath

adonath commented Jan 26, 2015

Copy link
Copy Markdown
Member

@hamogu My intention was position measured in pixel coordinates, rather than index, I just didn't care about negative positions. But as you suggested it should be handled in some way, because I think e.g. wcs_world2pix also returns negative pixel coordinates, when the world position is outside the image.

@adonath

adonath commented Jan 26, 2015

Copy link
Copy Markdown
Member

Just to underline my comment above: I don't think extract_array should always return the given shape, because the information that the array was extracted from the boundary region can be useful for how the array data is processed later. E.g. as I've written above, you might just want to discard this cases or apply a renormalisation. My suggestion would be the same as @astrofrog, but additonally having the boundary handling optional with a parameter boundary='fill' and 'none'. When the option is set to'none' the behaviour could be either as it is now (returning 'incomplete' arrays) or maybe better raising some SmallerArrayIsNotFullyContained error. boundary='fill' could be the default...

@larrybradley

Copy link
Copy Markdown
Member

Just thinking out loud here, but perhaps a better approach would be to make this a class (e.g. DataCutout?) that returns both a cutout array (which would be truncated at the edges) and a bunch of positional attributes (e.g. center, bounding box, slice tuple, etc.) relative to both the large and small images/data?

@hamogu

hamogu commented Jan 27, 2015

Copy link
Copy Markdown
Member Author

I see this more as a low-level function that could be used to build a classes like @larrybradley suggested on top of it. If we are talking e.g. about an image, that class could also handle changes to the WCS etc.
On the other hand, in some classes (e.g. photometry) I probably want to use this operation in an inner loop and I really only want one return value (and as other comments point out above, even the padding with nans might already be too much for some use cases), so making this into a class might be more complicated than necessary.

@hamogu

hamogu commented Feb 1, 2015

Copy link
Copy Markdown
Member Author

The Travis failure us just a few sphinx warnings which I will fix when I add the changelog in the end.
Before that, I want to make sure that we agree that this is the way it should be implemented.

@cdeil cdeil mentioned this pull request Mar 25, 2015
@hamogu

hamogu commented Mar 28, 2015

Copy link
Copy Markdown
Member Author

@larrybradley: For odd arrays we use the middle element as the center already, which, I believe, is exactly what you suggest. (I looked at both the scipy documentation and the scipy source code and it is not clear to me what happens for even sized arrays).

@hamogu

hamogu commented Mar 28, 2015

Copy link
Copy Markdown
Member Author

@astrofrog : If you want to know exactly what the coordinates of the input position are in terms of your new array, use mode='partial'. The position you put in will be exactly the center of the extracted array, with two complications:

  • if you put in an float position instead of an int, then it might not be at the center of the pixel, but it's in the same pixel (e.g. if you put in 3.2, the center of the new array will have the same coordinates as 3.0 in the original array)
  • for even sized arrays it's rounded up, i.e. the "center" of a (2,2) array is the pixel (1,1), thus pixel (1,1) will have the same coordinates as the position you put in.

However, I understand that this is not necessarily sufficient. A user will want an option that returns the coordinates of position in the new array for even and odd shaped arrays and for any mode. Unfortunately, this is somewhat non-trivial in the presence of edges (and precisely this is the reason why I originally implemented the mode that we now call "partial", where it's easy). I think I will have to redesign overlap_slices to calculate these numbers. Sounds simple, but is a little tedious to get it right for even shaped arrays and rounding, so it won't happen today and maybe not even this weekend.

I'm sorry that now it's me to hold this up. I know that by now there are at least two other PRs that wait for this. Please bear with me.

@larrybradley

Copy link
Copy Markdown
Member

@hamogu Yes, that's right. I was just trying to clarify that the generic filter origin=0 means the "middle", not index=0 (origin was probably not the best name).

BTW, for even-sized arrays, scipy appears to use (shape / 2) as the "center" index (as you have done here I think).

@hamogu

hamogu commented Apr 9, 2015

Copy link
Copy Markdown
Member Author

Travis is still running, but this is a heads up that I added a keyword to return the position in the coordinate system of the new array. Please review.

Comment thread CHANGES.rst Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please move to the nddata section

Comment thread astropy/nddata/utils.py Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This isn't actually restricted to integers, right? How about just tuple or number here, along with the minor edit a few lines down?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The shape of an array is restricted to an int. (No array has float dimensions), but he position is not. I changed this.

@mwcraig

mwcraig commented Apr 13, 2015

Copy link
Copy Markdown
Member

@hamogu -- only a couple minor comments, thanks for adding the option to return position! If you don't have time to make these changes please speak up. They are not critical to merging this.

@astrofrog -- position is now returned, haven't tried it extensively myself, but the test suite is pretty thorough. Unless you have concerns beyond my line comments, this looks good to merge; those line comments are not critical, can be fixed later.

also fix a couple of line breaks to make lines < 80 characters

[skip CI]
@hamogu

hamogu commented Apr 21, 2015

Copy link
Copy Markdown
Member Author

Any more comments on my longest ever running PR?

@pllim

pllim commented Apr 21, 2015

Copy link
Copy Markdown
Member

But it was created only in this year!

@mwcraig

mwcraig commented Apr 22, 2015

Copy link
Copy Markdown
Member

This might not get merged until the python in astronomy workshop is done.

Perhaps we could open a PR for a new unit? 1 hans = 1 comment/sloc or 1 hans = 1 day-since-pr-opened/sloc and any PR that reaches 0.5 hans is automatically merged? 😁

@wkerzendorf

Copy link
Copy Markdown
Member

@hamogu: @mwcraig and myself just looked through the comments and agree that this is good to merge. However, it can't be automatically merged. Can you rebase and ping us? Thanks for all your effort!

@hamogu

hamogu commented Apr 26, 2015

Copy link
Copy Markdown
Member Author

I get a merge conflict in every single of 19 commits. That's why I don't like PR's that are open this long (and if I remember right, this is about the forth time that I rebase).
I will close this PR and start from scratch from a fresh master checkout and just copy and paste the final changes in a text editor. That also cleans up the commit history, because this entire PR will only be a single commit.
(I understand that technically I could do that on the same branch, but I want to keep this branch in my repro in case anything goes wrong. )

@hamogu hamogu closed this Apr 26, 2015
hamogu added a commit to hamogu/astropy that referenced this pull request Apr 26, 2015
@hamogu

hamogu commented Apr 26, 2015

Copy link
Copy Markdown
Member Author

@mwcraig @wkerzendorf : Please see #3727 which is this PR, with copy & paste of the changes into a new branch based of the current master.

@embray embray removed Affects-dev PRs and issues that do not impact an existing Astropy release Ready-for-final-review labels May 4, 2015
@embray

embray commented May 4, 2015

Copy link
Copy Markdown
Member

I get a merge conflict in every single of 19 commits.

Something about that doesn't sound right. In the future I could probably try rebasing for you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants