Improve contour performance in WCSAxes by 10-1000x - #7568
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. |
larrybradley
left a comment
There was a problem hiding this comment.
Thanks, @astrofrog. I only had a few minor comments (e.g. typos).
|
|
||
| return super().imshow(X, *args, **kwargs) | ||
|
|
||
| def contour(self, *args, **kwargs): |
There was a problem hiding this comment.
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.
Would it be worth sticking functools.wraps on this so you have the function signature and stuff?
There was a problem hiding this comment.
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.
@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.
@astrofrog I wasn't actually thinking for the docstring, I was thinking for the function signature.
There was a problem hiding this comment.
@Cadair ah, that won't be very interesting though:
Signature: ax.contour(*args, data=None, **kwargs)
(that's the matplotlib one!)
| # 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 |
|
|
||
| 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 |
| pos_segments = [] | ||
|
|
||
| for collection in cset.collections: | ||
|
|
There was a problem hiding this comment.
nit: unnecessary blank line?
| # 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.
I think using enumerate here would be slightly faster, e.g. something like
for iseg, ivert in vert:
all_paths[ilevel][iseg].vertices = ivert
|
@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
contourandcontourf, 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 callSkyCoordonce.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