Skip to content

Conversation

@greglucas
Copy link
Contributor

This adds an option to change the contouring method to use the coordinates in projection-space rather than data-space. It is much faster to project the points beforehand, rather than projecting the contours themselves.

Before adding tests, I'd like to get a feel for whether people even think this is a good idea to have in Cartopy or not? This update basically removes the "transform" keyword from matplotlib.axes.Axes.contourf after already transforming the points to projection-space.

Rationale

#1291 shows how slow filled contours can be in Cartopy because it is projecting the contour regions, not the points. This ends up slowing down the transformation pipeline because of the non-affine transforms of potentially many contour regions.

Implications

A new keyword argument to contourf, but no changes to the defaults.
Closes #1291

Timing example

Similar to standard MPL now, ~100x faster than the standard Cartopy for this example.

MPL: 0.4196739196777344
Cartopy-fast: 0.44753384590148926
Cartopy-standard: 35.90342998504639
import time
import numpy as np
import cartopy.crs as ccrs
import matplotlib.pyplot as plt

lat = np.arange(-10, 90, .5)
lon = np.arange(0, 100, .5)
lons, lats = np.meshgrid(lon, lat)
data = np.random.rand(len(lat), len(lon))

# MPL
fig1 = plt.figure(figsize=(12, 9))
ax = fig1.add_subplot(111)
t0 = time.time()
ax.contourf(lons, lats, data)
fig1.savefig('contourf-mpl.png')
print("MPL:", time.time() - t0)

# Cartopy fast path
fig2 = plt.figure(figsize=(12, 9))
ax = fig2.add_subplot(111, projection=ccrs.Robinson())
t0 = time.time()
ax.contourf(lons, lats, data, transform=ccrs.PlateCarree(), fast=True)
ax.coastlines()
fig2.savefig('contourf-cartopy-fast.png')
print("Cartopy-fast:", time.time() - t0)

# Cartopy standard path
fig3 = plt.figure(figsize=(12, 9))
ax = fig3.add_subplot(111, projection=ccrs.Robinson())
t0 = time.time()
ax.contourf(lons, lats, data, transform=ccrs.PlateCarree())
ax.coastlines()
fig3.savefig('contourf-cartopy.png')
print("Cartopy-standard:", time.time() - t0)

@dopplershift
Copy link
Contributor

I'm certainly interested just based on the performance. I'm trying to work through the mental lift here to decide if the new approach is somehow wrong...

@greglucas
Copy link
Contributor Author

I'm not sure this is "wrong", so much as a different choice of when to calculate contour lines. I don't think this should be the default, but it may be OK as a good approximation to add for users who want a faster option.

The most obvious difference is the lack of boundary wrap in the coordinates. Here is the example using the fast-path. This will also make the lines straight in projected space rather than curved to follow the true projection, but I think that those are trade-offs users can decide on if they want this option.
Figure_1

@efiring
Copy link

efiring commented Dec 9, 2020

It sounds like this option makes the behavior match Basemap, correct?

@Gwi7d31
Copy link

Gwi7d31 commented Dec 9, 2020

I would agree that the kwargs fast option should be defaulted to False as everyone's current scripting would then "break" and produce a potentially unwanted result.

@greglucas
Copy link
Contributor Author

I think this is the behavior of Basemap, but I am not 100% sure and I don't have a local install on hand right now to test it out. I am curious if Basemap cuts off the edges of the example or if there is potentially a better way of accounting for the wrapped coordinates in projection-space within Basemap.

@greglucas
Copy link
Contributor Author

I was able to verify that the images look very similar with Basemap. i.e. Basemap does not handle the wrapped coordinates either.
Following on from the previous example:

from mpl_toolkits.basemap import Basemap
fig4 = plt.figure(figsize=(12, 9))
m = Basemap(projection='robin', resolution='c', lon_0=0)
t0 = time.time()
m.contourf(lons, lats, data, latlon=True)
m.drawcoastlines()
fig4.savefig('contourf-basemap.png')
print("Basemap:", time.time() - t0)

Here is the same image with Basemap:
contourf-basemap

@greglucas
Copy link
Contributor Author

I had another thought, maybe rather than fast as the keyword we should use something more descriptive that states what this is actually doing and could potentially even be used in other functions too if it is needed. Something to evoke the idea that we are doing the calculations in projection space rather than source space (and then projecting the result) would be valuable I think.

draw_in_projection_space, transform_early, transform_fast, ... Other ideas/suggestions?

@greglucas greglucas force-pushed the contour_fastpath branch 3 times, most recently from d585962 to 6484d6e Compare December 24, 2020 15:58
@efiring
Copy link

efiring commented Dec 24, 2020

I agree that the kwarg name could be improved. Among your suggestions, only "transform_early" works for me. (Drawing is always in projection space; and speed is a "why" not a "what is done".) Other possibilities:

transform_xy transform_grid transform_input

@efiring
Copy link

efiring commented Dec 24, 2020

Given that cartopy is relatively immature, I think the choice of default behavior should be considered carefully, and not be assumed to match current behavior.

When the transform is done matters most when the grid is coarse. In that case, is it even clear that the contour vertex calculation is done better in the original coordinate system, in general? And, for the expected long-term user base of cartopy, how common are cases where the path transform approach is superior?

@greglucas
Copy link
Contributor Author

I think you bring up some great points. The biggest difference between the approaches is with wrapped coordinates. If you have a model that produces output on a longitude grid from 160 - 200, the current behavior produces a patch that goes all the way out to the (-180, 180) bounds with a big gap in the middle.

contour-fast-False

This new method sorts the transformed points (required by contour) making the bounds go from (-179 to 180) in the contouring algorithm which then fills the internal gap where we really didn't have data originally.

contour-fast-True

I do think making the calculations in data-space is the better default, but I also think that there are good reasons for wanting to transform the points first and skip the slow non-affine projection of patches.


keyword names

The more I think about this, the more I wonder if we should make this an option that could be used by all of the plotting methods, similar to how we have the @_add_transform decorator. So, we should probably make this a generic name that could be used elsewhere too.

I like your transform_xy suggestion. Maybe rather than transform in the name we could use project_xy?

@mikecharles
Copy link

Any progress on this?

greglucas and others added 4 commits September 15, 2021 20:22
This adds an option to change the contouring method to use the
coordinates in projection-space rather than data-space.
It is much faster to project the points beforehand, rather than
projecting the contours themselves.
Contouring requires the x/y arguments to be gridded and sorted,
so adding in an argsort based on the x input array.
Making sure the fast_transform option gives the expected bounds
and raises the proper errors. There is no speed test here because
that may produce misleading results and errors depending on what
capabilities the computer running the test suite has.
Change to the keyword argument transform_first to describe
what the algorithm is doing.
@greglucas
Copy link
Contributor Author

I've just updated this PR for other folks that have been requesting it.

  • I changed the name to transform_first, which I think gets across the idea that we are transforming the points right away, before doing anything else. Other suggestions on naming are still welcome!

  • I pushed the logic up into a decorator and added it to contour as well, so now contour and contourf both get the new keyword argument option.

@QuLogic
Copy link
Member

QuLogic commented Sep 16, 2021

Should we name it something with stage in the name like the new interpolation_stage argument in Matplotlib?

This moves the logic for using transform_first into a decorator that
can be applied to multiple functions. Currently, contour and contourf
make use of it.
This parametrizes the test for transform_first and tests both
contour and contourf.
@greglucas
Copy link
Contributor Author

transform_stage could work. The only negative I have with that is that it would require a string answer to that instead of boolean True/False. I guess the string could be ("projection-space", vs. "data-space") transform_stage="data", but that may be more confusing to explain/use? I still slightly lean towards "transform_first=True" due to the boolean nature, but I'm not opposed to the "stage" suggestion if others have strong preferences.

@QuLogic QuLogic merged commit 21dae8b into SciTools:master Sep 16, 2021
@dopplershift dopplershift added this to the 0.20 milestone Sep 16, 2021
@QuLogic
Copy link
Member

QuLogic commented Sep 16, 2021

Hmm, I just re-read the docstring, and I wonder if you were interested in writing an example that shows what it means?

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

contourf method significantly slower than basemap

6 participants