-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Improve contour performance in WCSAxes by 10-1000x #7568
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
The default implementation of contour in Matplotlib calls the transform (when a transform is given) for each individual contour line. Since our transforms rely on WCS and SkyCoord, each transform can have non-negligible overhead. Therefore, we now instead deal with the transformation ourselves by stacking all the vertices of the paths and transforming them all in one go.
Hi there @astrofrog 👋 - thanks for the pull request! I'm just a friendly 🤖 that checks for issues related to the changelog and making sure that this pull request is milestoned and labeled correctly. This is mainly intended for the maintainers, so if you are not a maintainer you can ignore this, and a maintainer will let you know if any action is required on your part 😃. Everything looks good from my point of view! 👍 If there are any issues with this message, please report them here. |
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, @astrofrog. I only had a few minor comments (e.g. typos).
@@ -196,6 +196,46 @@ def imshow(self, X, *args, **kwargs): | |||
|
|||
return super().imshow(X, *args, **kwargs) | |||
|
|||
def contour(self, *args, **kwargs): |
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.
Will the matplotlib docstrings for contour
and contourf
cause any sphinx warnings? I know it's not desirable to copy the docstrings, but I recently noticed that when building the astropy docs locally that the WCSAxes
set_xlabel
set_ylabel
docstrings (inherited from matplotlib
) cause sphinx warnings (~matplotlib.text.Text
and "text"
links are broken), e.g. http://docs.astropy.org/en/latest/api/astropy.visualization.wcsaxes.WCSAxes.html#astropy.visualization.wcsaxes.WCSAxes.set_xlabel. I'm not sure why the ~matplotlib.text.Text
target isn't found.
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 it be worth sticking functools.wraps
on this so you have the function signature and stuff?
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.
Good point - I've written some custom docstrings that explicitly refer to contour and contourf via intersphinx for information about valid arguments.
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.
@Cadair - Actually if we didn't set any docstring I think it just inherits it? But I'd rather go with the approach done here now so we don't get random doc failures when Matplotlib change their 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.
@astrofrog I wasn't actually thinking for the docstring, I was thinking for the function signature.
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.
@Cadair ah, that won't be very interesting though:
Signature: ax.contour(*args, data=None, **kwargs)
(that's the matplotlib one!)
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 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.
🤦♀️
# In Matplotlib, when calling contour() with a transform, each | ||
# individual path in the contour map is transformed separately. However, | ||
# this is much too slow for us since each call to the transforms results | ||
# in an Astropy coordinate transformation, which has a non-negligeable |
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.
negligible
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.
Fixed
|
||
Using transforms with the native Matplotlib contour/contourf can be slow if | ||
the transforms have a non-negligible overhead (which is the case for | ||
WCS/Skycoord transforms) since the transform is called for each individual |
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.
SkyCoord
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.
Fixed
pos_segments = [] | ||
|
||
for collection in cset.collections: | ||
|
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.
nit: unnecessary blank line?
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.
Removed
# Now re-populate the segments in the line collections | ||
for ilevel, vert in enumerate(vertices): | ||
vert = np.split(vert, pos_segments[ilevel]) | ||
for iseg in range(len(vert)): |
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 using enumerate
here would be slightly faster, e.g. something like
for iseg, ivert in vert:
all_paths[ilevel][iseg].vertices = ivert
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.
Fixed
@astrofrog When will the new contour benchmarks be run? I don't see them yet. I assume this PR shouldn't be merged until the benchmarks have been run at least once with the old code? |
@larrybradley - thanks for the review! I'll check today why the benchmarks haven't run yet. |
@astrofrog Let me know when the benchmarks have been run and I'll merge this. |
@larrybradley - they have now run: http://www.astropy.org/astropy-benchmarks/#visualization.wcsaxes.time_contour_with_transform Thanks! |
Background
WCSAxes has a mechanism for overplotting contours from an image in a different coordinate system from the original image. This essentially boils down to:
However, the way Matplotlib deals with this internally is to compute the contours on
contour_data
, then to represent each line in the contour map as a MatplotlibPath
(note - each line, not each level - there might be multiple lines per level), then to transform each path individually using the specified transform. In the case where coordinate conversions are needed, the transform includes calls toSkyCoord
, which we know is a little slow, especially if called once for each line in the contour map. For some contour maps I've made, there are sometimes thousands of individual lines.New approach
The approach implemented here is to overload
contour
andcontourf
, and internally call them without transform, but to then modify the resulting ContourSet manually. We do this by extracting all the vertices of the lines, stacking them into a single array, transforming them, and splitting them again. This is far more efficient because we then only effectively callSkyCoord
once.Performance
The following functions test the new approach, the old approach, and also test the performance without contours:
The results are as follows:
If we subtract the base time for the plot excluding the contour, the old approach took 14+ seconds, whereas now it takes ~70ms, a speedup of around 200x.
The exact speedup will depend a lot on the number of lines in the contour map. The worst case scenario (for the new implementation) would be if there was just a single contour line. If I take the example above and set
levels=[5.8e-4]
, I get a single contour line. In that case, the timings look like:Basically the differences are in the noise. So there should not really be any cases where this results in worse performance.
I've added a couple of benchmark to astropy-benchmarks: astropy/astropy-benchmarks#58