-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Ensure streamplot Euler step is always called when going out of bounds. #11947
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
Ensure streamplot Euler step is always called when going out of bounds. #11947
Conversation
I seem to remember someone already fixed something similar. Can you make sure this is still a problem in master? Thanks! |
lib/matplotlib/streamplot.py
Outdated
@@ -473,6 +473,10 @@ def integrate(x0, y0): | |||
return integrate | |||
|
|||
|
|||
class OutOfBounds(Exception): |
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.
might make sense to inherit from IndexError
here and still catch IndexError
below (so in the case where f
does still raise it (not sure if that is possible) everything still works?
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.
Yep good point. I think the code shouldn't raise IndexError
any more as we are checking bounds prior to interpolating, but I guess a bit safer to do as you suggest so I have pushed this to the PR now. Thanks!
else: | ||
raise OutOfBounds | ||
|
||
except OutOfBounds: |
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.
If all of the raising logic is here it might be clearer to just use if
and else
statements.
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.
The raising logic is all here directly within the try block, including line 529 as well (so can't simply be put in the else
). I can rewrite it using a flag variable like so. What do you think? Happy to update if you prefer it.
try:
out_of_bounds = False
if dmap.grid.within_grid(xi, yi):
xf_traj.append(xi)
yf_traj.append(yi)
k1x, k1y = f(xi, yi)
if dmap.grid.within_grid(xi + ds * k1x, yi + ds * k1y):
k2x, k2y = f(xi + ds * k1x, yi + ds * k1y)
else:
out_of_bounds = True
else:
out_of_bounds = True
if out_of_bounds:
# Out of the domain during this step.
# Take an Euler step to the boundary to improve neatness
# unless the trajectory is currently empty.
if xf_traj:
ds, xf_traj, yf_traj = _euler_step(xf_traj, yf_traj,
dmap, f)
stotal += ds
break
except TerminateTrajectory:
break
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.
Another simple approach could be to move the bounds check and raise OutOfbounds
into f
. Would simplify the logic in the interpolator.
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've pushed a change to the PR moving the raise OutOfBounds
into f
. I tried quite a few things and I think this is the clearest.
Thanks for your work on this! Left a few comments in-line about the exception handling logic. I am a bit concerned about the |
@tacaswell thanks a lot for the feedback. I'll reply inline to the code comments. The @jklymak I've checked and the issue is still in master. The image in the PR that shows the problem is from the master tests here. |
so the |
Yes, the main difference is that it fixes the transforms between mask (which is what decides where to make streamlines) and grid. This is what drew my attention to the The change does have a very small effect on the integration too though, as it rescales the The change makes no difference to the direction of trajectories as |
OK, but i'm not clear that it shouldn't be mask.nx/grid.nx with no -1 on both of them. I think I agree that they should be the same, however... I haven't given it much thought, but a quick skim of |
Another way to explain the
If you substitute in the example The current code, which does Please let me know if anything doesn't make sense. Very happy to clarify further. |
No that’s clear. I just would usually expect that streamlines and data points would be offset from each other by half a grid cell |
That's not the case here - they are on different resolution grids but not offset. The images I added above with the mask size of 3 show that both with and without the change, the streamlines start at the corners of the grid. The difference is that with the change, the whole of the mask is used. |
@tomflannaghan Sorry if this fell off the radar. It needs a rebase, and I'll probably need to spend a bit of time reviewing it. Feel free to ping me. I do find this whole code a bit overengineered - I think it could be made a lot simpler. But lets at least decide if your change is the right one. |
@jklymak Thanks for getting back to this. I've rebased. I agree that simplifying things would be great. Since submitting this PR I've also had a look at issue #12025 and I have got a fix that also simplifies the overall implementation somewhat (mainly as it removes the need for the grid coordinates and grid transforms). Do you think it would be better for me to submit a new PR including both my fix for #12025 and this change, or shall we keep the two separate and continue with this PR? |
@tomflannaghan sorry this fell off the radar again! This has way too many commits 😉 so needs a proper rebase with just the changes you made. I expect this change is a good first step and then we can work on more general improvements. Again, please do ping about these things (within reason). PRs get burried. |
This makes figures neater. Also fix places that were using dmap.grid.nx for the width of the grid, which should be dmap.grid.nx - 1.
Moves the raising to the gradient steps simplifying the integrator code a bit. Also makes OutOfBounds inherit from IndexError as suggested by tacaswell.
b459fdd
to
e00e512
Compare
It looked like what happened here was a rebase and then merging the original code back. I took the liberty of dropping the merge commit, rebasing onto current master, regenerating the tests, and force pushing. I hope that is ok with you @tomflannaghan ! |
Can someone with a mac build this locally and see how the images are failing? |
We are seeing 1bit differences on 2 pixel for this test between linux/win vs mac.
We have seen similar off-by-one-bit differences between the platforms before
so something somewhere is rounding slightly differently between platforms. I pushed a commit upping the tolerance on the test, I don't think it is worth getting into the numerics of why this is happening. |
Please squash merge this. |
As per our call today, here's what it looks like when you do a quiver plot on top with: import matplotlib.pyplot as plt
import numpy as np
def velocity_field():
Y, X = np.mgrid[-3:3:100j, -3:3:100j]
U = -1 - X**2 + Y
V = 1 + X - Y**2
return X, Y, U, V
X, Y, U, V = velocity_field()
plt.streamplot(X, Y, U, V, color=U, density=0.6, linewidth=2,
cmap=plt.cm.autumn)
slices = [slice(None, None, 5), slice(None, None, 5)]
plt.quiver(X[slices], Y[slices], U[slices], V[slices], pivot='mid')
plt.colorbar()
plt.show() The arrows look tangential to the streamlines everywhere, so I'm not seeing any lingering issues in the streamplot calculation, at least as far as offsets are concerned. I'll merge once CI passes. |
PR Summary
When the integrator goes out of bounds of the grid, it can perform a step using Euler integration to make the trajectory stop right at the edge of the grid, rather than stopping before reaching the edge. The current implementation relies on an IndexError being thrown to perform an Euler step, and there are several cases in which this won't happen.
Here I add checks explicitly to the integrator rather than relying on IndexErrors to fix both cases. This results in neater figures without ragged edges.
I also spotted that we were using dmap.grid.nx for the width of the grid, which should be dmap.grid.nx - 1 (and similarly for ny) as the grid goes from 0 to nx-1. This also means that the rightmost and uppermost edges of the mask now correspond to the rightmost and uppermost edges of the data, rather than sitting outside the grid (and therefore being unused).
This change changes all test results for streamplot as streamlines go right to the edge of the plot now. For example,
Before:

After:

PR Checklist