-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
ENH: optimize Collection non-affine transform to call transform once #11465
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
ENH: optimize Collection non-affine transform to call transform once #11465
Conversation
Hah, need to be more careful - borks a lot of other things. Still, should be an easy fix... |
acf4ac4
to
c31d901
Compare
Timing: there is a small hit for this PR for non-affine transforms that are not too slow. i.e. if I remove the With time.sleep: 34 s before, 1.09 s after I'd argue the small 20% hit just for non-affine transforms is worth it to make slow transforms work much faster. |
Ähm.. no. 99.999% of all transforms being used in matplotlib are expected to be sufficiently quick. So you say you would rather let those standard cases become slower???? |
This code path is only for non-affine transforms. Affine transforms aren't affected... |
The standard polar plot contains a non-affine transform. Does this proposed solution make the creation of contours on polar plots slower? |
That probably depends on the nature of the contours.... |
import time
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.transforms import Transform
from scipy.ndimage import gaussian_filter
np.random.seed(19680801)
image = np.random.random((1024, 1024))
image = gaussian_filter(image, 2)
ax = plt.subplot(1, 1, 1, projection='polar')
ax.contour(image)
plt.savefig('contours.png')
plt.close() Old: 11.1 s |
What about a less pathological case like
|
Old: 168 ms EDIT: But thats not surprising because all the contours are a single line segment in that example. The slowdown / speed up only happens if there are multiple line segments per contour level like in the random examples. |
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.
👍 looks like a nice speedup
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.
👍 on the functionality and trying to optimize this path.
I think the current implementation has some behavior changes that aren't quite right, though.
@@ -189,7 +189,7 @@ def get_datalim(self, transData): | |||
paths = self.get_paths() | |||
|
|||
if not transform.is_affine: | |||
paths = [transform.transform_path_non_affine(p) for p in paths] | |||
paths = self._non_affine_transform_paths(paths, transform) |
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.
We've gone here from calling transform_path_non_affine
to (effectively) transform_non_affine
. The former returns a new Path
, which based on my reading would seem to offer possibly interpolate points along the path in the transformed coordinate system. The latter will not do this. Now, does anyone know if this is important?
Is it possible to instead combine the individual Paths into one big path and use transform_path_non_affine
?
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.
Oh, hummm. OK< I just looked at the default transform_path_non_affine
but that can be overridden, so I think you're right that we need to call 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.
I looked at the default as well, and the line I'm concerned we're skipping is:
return Path._fast_from_codes_and_verts(x, path.codes,
{'interpolation_steps': path._interpolation_steps,
'should_simplify': path.should_simplify})
Your current implementation calls self.transform_non_affine(vertices)
, just like the default transform_path_non_affine
, but it reuses the existing Path
instances.
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.
Ok, I may be over paranoid. It looks like _fast_from_codes_and_verts
just sets the interpolation steps, it doesn't do any actual interpolation at that time.
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.
matplotlib/lib/matplotlib/projections/geo.py
Lines 260 to 265 in b078643
def transform_path_non_affine(self, path): | |
vertices = path.vertices | |
ipath = path.interpolated(self._resolution) | |
return Path(self.transform(ipath.vertices), ipath.codes) | |
transform_path_non_affine.__doc__ = \ | |
Transform.transform_path_non_affine.__doc__ |
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.
So, its possible to come up w/ an algorithm that does this if transform_path_non_affine
uses only path.interpolated
to get the higher-res path. But, I don't see any reason that an arbitrary user-supplied transform would be restricted to doing that method of interpolation.
Given that ambiguity, I think its impossible to come up with a foolproof algorithm for this. I suppose the special case of no interpolation could be checked for and would work.
I'll point out that @astrofrog's fix is obviously counting on the non-affine path transform not doing any interpolation - i.e. its doing linear dot connecting after the tranform. Certainly that'd be a disaster for a geographic non-affine transform (i.e. you want a straight line in lat/lon to be curved in the projection), but maybe is fine for astropy?
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.
@jklymak - ah I see - in our case we write the transforms we are using and we don't make use of interpolation for now so things should be fine. It would be pretty rare for the resolution of the contour map to be so low that there would be significant distortions between vertices of the path.
# repopulate paths | ||
verts = np.split(vertices, pos[:-1]) | ||
for nn, verti in enumerate(verts): | ||
paths[nn].vertices = verti |
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.
You're modifying paths
in place; if you trace back, this is a reference to self._paths
, which means you're modifying the original contours. That seems really bad.
OK, closing this, because I don't think it'll work for general implimentations of |
PR Summary
As noted in https://github.com/astropy/astropy/pull/7568/files#diff-421f6873fdd2d8a90f39f3050534b0c3R139 and #11464 Collections weren't very snappy if they had lots of separate paths that needed to be run through a non-affine transform that was slow.
@astrofrog came up w/ the solution in astropy/astropy#7568, and it is applied here on a per-collection basis (his soln was for every collection in a contour set, which may make things even faster). The basic idea is that the transform only gets called once for all the vertices in the paths in the collection and then they get re-divided up after transformation.
Test
From @astrofrog with a deliberately slow transform function...
Takes about 20s on master, about 2s on this PR...
PR Checklist