-
Notifications
You must be signed in to change notification settings - Fork 367
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
fix: Invalid Polygon from Contouring (Issue #2370) #2373
Conversation
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.
Hi @Clarmy thanks for digging into this and sorry I didn't get a chance to think about it properly till now. Your change makes sense to me and I just have a couple of minor comments about the code structure.
Ideally this should also get a test but, since this problem is so data dependent, I'm struggling to think how it could be written without uploading a lot of sample data (which we don't want to do!)
One other thing I'm not sure about is which branch the PR should target. @greglucas since you have handled recent releases can you comment on that?
We should target main and then backport the commit to the other branch if we want to make a v0.23.1 release. I've updated this PR to target main now. Do we know why we are getting duplicate points/bad geometries from our other code paths? This is likely OK to fix here, but it might be nice to at some point to figure out why we are producing the bad geometries in the first place and see if we can fix this issue at the source. |
It might be somewhat difficult for me to identify the root cause of this issue, but I can provide some helpful clues. Based on the Traceback information I initially encountered:
According to the hint, the conflicting coordinate is at (97, 16.5). Therefore, when I catch the GEOSException, I map the polygon object to geojson and save it, with the file available for download: invalid.geojson.zip In the invalid.geojson, we can look for clues at the (97, 16.5) coordinate. Here is a segment from the file:
It is evident that the consecutive points from [97.0, 16.5] to [97.0, 16.5] are actually along the meridian at longitude 97.0 (i.e., on the same line), which is where the topology error in the polygon arises. |
@rcomer I'm not very familiar with the CI process for cartopy, but I see that some tests have failed in the CI, and pre-commit.ci has also been failing continuously. I'm not sure what impact this has on the final merge, and how to check and resolve the issue of inconsistent test drawing results under some versions in the CI? |
pre-commit is failing on the ruff step, with the issue here just being that For the test failures on two of the builds, it shouldn't affect this PR as it appears related to an unexplained issue where some builds are intermittently failing (currently being investigated in #2367). |
Guess it also is looking for two empty lines after the the last import, instead of one. |
@lgolston It worked! Thank you for your guidance. |
If we plot the data with Matplotlib's So I agree with @Clarmy that the problem does not originate in Cartopy and the best we can do here is handle it when it comes up. We could open an issue with Contourpy to see if there is anything to be done there though. |
I have opened an issue at contourpy/contourpy#377, and in the process came up with an example that I think is small enough to make into a test (though I see you have also added a test in the meantime @Clarmy): import matplotlib.pyplot as plt
import numpy as np
import cartopy.crs as ccrs
lons = [96.75, 97. , 97.25]
lats = np.arange(17.25, 15.9, -0.25)
lons, lats = np.meshgrid(lons, lats)
data = [[26.9, 43.7, 26.8],
[33.2, 65.8, 34.6],
[54.7, 66.9, 55.5],
[65.3, 65. , 65.7],
[65.9, 65. , 65.6],
[65.8, 65.2, 65.5]]
fig = plt.figure()
ax = fig.add_subplot(projection=ccrs.Mercator())
ax.contourf(lons, lats, data, levels=[60, 65], transform=ccrs.PlateCarree()) |
@rcomer Thank you for your continued effort on this issue; your test case is superbly done. The test case I added was specifically for testing the function |
I just took your data and sliced it as much as I could while still reproducing the error! It's great to have the example by the way - the error has been reported previously (e.g. #1546) but we did not have enough information to reproduce. We have a test file for contour plots here: |
By the way, you can also install pre-commit locally to help with linting each time you commit: https://scitools.org.uk/cartopy/docs/latest/contribute.html |
Okay, I understand. Thank you very much! |
Sorry, I've found that the modified code introduced a new bug in actual use, and I'm currently addressing it. I will provide a more detailed description later. |
The situation we are now facing is more complex than it was at the beginning. I've discovered a new issue: if we use this data to plot a graph, it still causes an error when run on my modified cartopy code. The error it generates is:
The reason for this error is that in my modified code, the step To investigate the cause, I exported both the original It seems there is no apparent problem, so why does it turn into multiple polygons? After reading the I'm sharing this information for now, and as for the solution, I am still exploring. |
lib/cartopy/mpl/patch.py
Outdated
# is invalid. In this case, we can't use the contains method. | ||
# Therefore, we need to perform a repair on the Polygon object | ||
# and then attempt the contains method again. Related Issue: #2370 | ||
polygon = collection[-1][0].buffer(0.001).buffer(-0.001) |
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 have tried using the .buffer(0.001).buffer(-0.001)
to perform a dilation and contraction operation on the polygon, and so far, the results are quite satisfactory. Although the code is not particularly elegant, it can solve the actual problem. At the same time, it's important to note that this kind of scaling operation produces a slight rounded effect in the inner corners. When I set the scaling parameter small enough (for example, 0.001), the difference is almost negligible.
@Clarmy Are you able to recursively or iteratively add the geometries to the original set of geometries? I think it makes sense to have multiple distinct polygons if we need them and not buffer to add artificial gaps to connect geometries. if geom is multi-polygon:
for poly in geom:
# Might need to check for linestrings here too?
append to collection
elif geom is Polygon:
append to collection Perhaps using "make_valid" is better than try/except as well? contourpy/contourpy#377 |
Just to note that I tried I was also thinking that try/except should be faster than |
@greglucas I have referred to the example in contourpy/contourpy#377, but unfortunately, based on my actual testing, I found that the |
I'm not sure if I'm misunderstanding the |
contour/contourf used to create one path per contour as you say but there was a change at Matplotlib v3.8 so they now create one path per level. Note that this part of Cartopy is quite general and handles anything that is drawn with a path (which I think is most things?) I think the main other example of needing to sort out internal/external polygons is for the FeatureArtist, hence the code comments about land masses with lakes in. So at this point in the code we don't actually know if we are working with contours or something else. It does bother me that, specifically for the contour case the overall process is effectively
It would be nice if we could somehow intercept the contours straight out of ContourPy when it is presumably more obvious what the individual contours are. I did have a couple of attempts studying the relevant parts of Matplotlib to figure that out, but I didn't come up with anything. |
I understand now, so If so, I believe that the modifications in this PR should not affect other functionalities (assuming they were previously working properly). The issue of generating invalid polygon objects would likely only occur when generating contours, as these are interpolated on the spot from grid data. For other data like those used in FeatureArtist, which are pre-generated, the input graphic paths should theoretically already be free from topological errors and valid. Of course, I cannot guarantee that my understanding is completely accurate, so please point out any obvious errors if there are any. Moreover, if we directly obtain the paths from ContourPy, does it imply that this part of the code needs to be refactored? |
I think your understanding is correct
This is really just a vague aspiration and I don't know if it's even possible, so I wouldn't worry about that. |
If we are getting 1 The +/- buffering seems like a hack to me and may not be suitable for all resolutions / scales. One other thought that comes to mind is that this is due to the |
I can start by running some tests to see. |
@greglucas Regarding your suggestions, I have conducted the following work:
|
I tried @Clarmy's original example with an old environment we have lying around (Cartopy 0.18, Matplotlib 3.3.4) and the error is the same, so I don't think the recent Does the [I see see @Clarmy pushed another change while I was typing this so maybe this suggestion is redundant.] |
I just tried locally to only judge the value of |
Where does this PR stand? Is it ready for review or were we going to update some of the other areas of code to handle these cases instead? |
Hi all,
Maybe this helps for testing. |
@greglucas Sorry for not paying attention to this PR for quite some time. It seems that there have been significant changes in Cartopy's code since this PR was submitted. I'm also not sure if the issue that this PR aims to address still exists. I can run some tests with the latest code and the previous data to see if the issue has already been resolved. It might turn out that there's nothing more for me to do. |
@greglucas @rcomer Due to this PR being outdated, there are some conflicts with the latest code as versions have iterated. Therefore, I made modifications based on the latest code and submitted a new PR: #2463. |
New PR: #2463 |
Rationale
Resolve the issue mentioned in #2370
Implications
This change will affect the behavior of contourf and essentially fixes a bug in the contourf function.