-
-
Notifications
You must be signed in to change notification settings - Fork 49
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
Add fast method to lookup/estimate WCS limits in world coordinates #474
Conversation
Hello @dhomeier! Thanks for updating this PR.
Comment last updated at 2021-10-27 16:15:21 UTC |
Doh, taking the extrema on |
3202d16
to
56b6124
Compare
The different output in the py38-devdeps test probably highlights further issues with the >>> from ndcube.tests import test_ndcollection
>>> cube = test_ndcollection.cube1
>>> cube._extra_coords = ndcube.ExtraCoords.from_lookup_tables(('time', 'hello', 'bye'), (0, 1, 2), (list(range(2)) * u.pix, list(range(3)) * u.pix, list(range(4)) * u.pix))
>>> cube.axis_world_coords()[1]
<SpectralCoord [1.02e-09, 1.04e-09, 1.06e-09, 1.08e-09, 1.10e-09] m>
>>> cube.axis_world_coords(wcs=cube.extra_coords)
(<Quantity [0., 1., 2., 3.] pix>,)
>>> cube.axis_world_coords(wcs=cube.extra_coords.wcs)
(<Quantity [ 0., 1., 2., 3., nan] pix>,) whereas the same with different units produces >>> cube._extra_coords = ndcube.ExtraCoords.from_lookup_tables(('time', 'hello', 'bye'), (0, 1, 2), (list(range(2)) * u.second, list(range(3)) * u.pix, list(range(4)) * u.micron))
>>> cube.axis_world_coords(wcs=cube.extra_coords)
(<Quantity [ 0., 1., nan, nan, nan] s>, <Quantity [0., 1., 2.] pix>, <Quantity [0., 1., 2., 3.] micron>)
>>> cube.axis_world_coords(wcs=cube.extra_coords.wcs)
(<Quantity [ 0., 1., nan, nan] s>, <Quantity [0., 1., 2.] pix>, <Quantity [ 0., 1., 2., 3., nan] micron>) Maybe the option to use a different wcs is better just removed from |
56b6124
to
40eee20
Compare
40eee20
to
e73ac71
Compare
Still holds even for consistent output from that function. Testing the length of the returned output on devdeps has coords = ndcube_3d_l_ra_dec.axis_world_coords(wcs=ndcube_3d_l_ra_dec.extra_coords)
> assert len(coords) == 1
E assert 3 == 1 which passes on all other platforms and with my local astropy 5.0dev1016 installation with WCSLIB 7.7; no idea what might cause the different behaviour above. |
As @Cadair pointed out, the difference is the dev version of gwcs being installed on devdeps; the different behaviour can be isolated to the fact that for from ndcube.conftest import data_nd
data = data_nd((2, 3, 4))
cube = ndcube.NDCube(data, wcs_3d_l_lt_ln, mask=data>0, uncertainty=data)
coord0 = time.Time(['2000-01-01T12:00:00', '2000-01-02T12:00:00'], format='isot')
cube._extra_coords = ndcube.ExtraCoords.from_lookup_tables(('time', 'hello', 'bye'), (0, 1, 2), (coord0, list(range(3))*u.pix, list(range(4))*u.m))
cube._extra_coords._ndcube = cube
cube.axis_world_coords(wcs=cube.extra_coords) (<Time object: scale='utc' format='isot' value=['2000-01-01T12:00:00.000' '2000-01-02T12:00:00.000']>, <Quantity [0., 1., 2.] pix>, <Quantity [0., 1., 2., 3.] m>) whereas cube._extra_coords = ndcube.ExtraCoords.from_lookup_tables(('time', 'hello', 'bye'), (0, 1, 2), (list(range(2))*u.pix, list(range(3))*u.pix, list(range(4))*u.pix))
cube._extra_coords._ndcube = cube
cube.axis_world_coords(wcs=cube.extra_coords) (<Quantity [0., 1., 2., 3.] pix>,) so apparently for repeated occurrences of identical physical types gwcs 0.16.1 only keeps the last instance, while the dev version more consistently returns (<Quantity [ 0., 1.] pix>, <Quantity [0., 1., 2.] pix>, <Quantity [0., 1., 2., 3.] pix>) I have not traced back where, how or why that happens, but arguably the dev version is working more correctly here, so we should perhaps refrain from using the |
21abce4
to
87c97c9
Compare
87c97c9
to
1efb0b8
Compare
I am now testing for the new functionality and have it xfail with gwcs 0.16, so marking this ready for a first review. |
@@ -507,6 +538,132 @@ def axis_world_coords_values(self, *axes, pixel_corners=False, wcs=None): | |||
CoordValues = namedtuple("CoordValues", identifiers) | |||
return CoordValues(*axes_coords[::-1]) | |||
|
|||
@utils.cube.sanitize_wcs | |||
def axis_world_coords_limits(self, *axes, pixel_corners=False, wcs=None, max_size=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.
My initial thought is I wonder if it would be better if this starts off as private API? I am not sure of it's utility to the general users?
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.
Yes, at the start I was relatively indifferent whether it should be public or private, just thought some general uses might spring from it as it develops. The speed differences probably won't matter there, but the output can be much more compact than axis_world_coords
.
""" | ||
# Cannot use naxis and array_shape to construct bounding box for | ||
# extra_coords or combined_wcs, so for now force using the full wcs for those. | ||
if isinstance(wcs, (ExtraCoords, HighLevelWCSWrapper)) or wcs.array_shape is 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.
There are potentially many situations where we would be passed a HighLevelWCSWrapper
, not just those which you list in the comment (mainly a wrapped SlicedLowLevelWCS). A more nuanced check would be to extract the low level object first and then dispatch on the type of that instead.
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.
Not sure how to get an overview of what could potentially be passed there.
pass | ||
# axes_coords = [None] * wcs.world_n_dim | ||
# Convert to world coordinates | ||
axes_coords = list(wcs.pixel_to_world_values(*bbox)) |
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 will have to get @DanRyanIrish to check me on this, but I think there is a subset of cases where this _bounding_box_to_points
method generates invalid inputs to pixel_to_world
.
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 from the suggestion in #413 (comment), but the link there must have pointed to an older revision.
upper = np.array(wcs.array_shape[::-1]) - 1 | ||
bbox = np.array(self._bounding_box_to_points(lower, upper, wcs)).T | ||
# If wcs has a FITS-type Wcsprm, try to include CRPIX axes | ||
try: |
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 am not sure I am following exactly what this block is doing.
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 one below?
It takes the world coordinate at crpix[i] in the respective dimension, and creates a grid over the other dependent axes (will probably be usually only one) through that point.
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.
For example in
ndcube/ndcube/tests/test_ndcube.py
Lines 468 to 476 in 1efb0b8
coords = ndcube_3d_l_ra_pol.axis_world_coords_limits(max_size=10000) | |
assert u.allclose(coords[0], [1.02e-09, 1.20e-09] * u.m) | |
assert u.allclose(coords[1].ra, [26.1484, 333.4424] * u.deg) | |
assert u.allclose(coords[1].dec, [61.8572, 89.98945] * u.deg) | |
coords = ndcube_3d_l_ra_pol.axis_world_coords_limits(max_size=10) | |
assert u.allclose(coords[0], [1.02e-09, 1.20e-09] * u.m) | |
assert u.allclose(coords[1].ra, [26.1484, 333.4424] * u.deg) | |
assert u.allclose(coords[1].dec, [61.8572, 80.429] * u.deg) |
which is working on
wcs_3d_l_ra_pol
with CRPIX2=200
at CRVAL2=90.5
and CRPIX3
at CRVAL3=80.4
, it is spanning a line through all declinations at RA=90.5 deg and all Right Ascensions at Dec=80.4 deg each.This is covering the point closest to the pole, or very nearly, while the last call, which is skipping this block as it requires more than
max_size=10
points, only returns the world coordinates at the bbox corners.It's of course arguable which is really more useful as limit values...
object_names = np.array([wao_comp[0] for wao_comp in wcs.world_axis_object_components]) | ||
unique_obj_names = utils.misc.unique_sorted(object_names) | ||
world_axes_for_obj = [np.where(object_names == name)[0] for name in unique_obj_names] | ||
|
||
# Create a mapping from world index in the WCS to object index in axes_coords | ||
world_index_to_object_index = {} | ||
for object_index, world_axes in enumerate(world_axes_for_obj): | ||
for world_index in world_axes: | ||
world_index_to_object_index[world_index] = object_index | ||
|
||
world_indices = utils.wcs.calculate_world_indices_from_axes(wcs, axes) | ||
object_indices = utils.misc.unique_sorted( | ||
[world_index_to_object_index[world_index] for world_index in world_indices] | ||
) | ||
|
||
return tuple(axes_coords[i] for i in object_indices) |
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.
Is this identical to the logic at the end of axis_world_coords
? if so should we move it into a shared method?
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.
Much of the code is; I was just hesitant to overload axis_world_coords
(like implementing the whole thing as axis_world_coords(limits=True)
).
This pull request has been automatically marked as stale because it has not had any activity for the past five months. It will be closed if no further activity occurs. If the ideas in this pull request are still worth implementing, please make sure you open an issue to keep track of that idea! |
This pull request has been automatically closed since there was no activity for a month since it was marked as stale. If the ideas in this pull request are still worth implementing, please make sure you open an issue to keep track of that idea! |
Rebasing just to check the tests are still passing; not sure where we last stood re. design decisions. |
Adding a method
~ndcube.NDCube.axis_world_coords_limits
to return the extension (minimum/maximum in all axes) in world coordinates as laid out in #413 (comment). This returns the appropriate limits asSkyCoord
,SpectralCoord
objects in the same format asaxis_world_coords
, to be built into the__str__
representation:The function is largely a clone of
axis_world_coords
, but adding amax_size
parameter above which the call to the full_generate_world_coords
evaluation is replaced by estimating the limits from only a subset of pixel coordinates.My timings indicate that below WCS sizes of some 10000 the former has negligible impact, from some 100000 it approaches 0.1 seconds, so that's probably the limit that
__str__
or__repr__
should set when calling it.TODO etc.:
2d
is still missing); not sure if/which of the split/spliced test cases need to be covered in addition. Something forextra_coords
andcombined_wcs
should probably be added, although that is not the core purpose of this function.NDCubeBase
property. While at that, it's probably worth discussing if the entire(axes_coords, )
for the limits should simply be cached in a property as well (only to be updated wheneveraxis_world_coords_limits
with higher precision/max_size
), so__str__
would only need to read that back.min()
andmax()
will just return the pixel closest to the pole forSkyCoord.dec.max()
(which lies somewhere along acrpix[i]
axis in this example), and whatever pixels fall closest to the meridian on both sides forSkyCoord.ra
minmax. Both are of course not representative of the WCS extension on the sky in any way, but I don't have a good idea how to find a better representation in this case. Maybe falling back to the pixel corners would be an option; shifting RA to a range [-180 deg, 180 deg] would also be helpful.