Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

Merged

Conversation

tomflannaghan
Copy link
Contributor

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.

  • It won't currently check the end point, but only intermediate points
  • It doesn't always raise an IndexError if the trajectory leaves the left or bottom as negative indices are valid.

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:
streamplot_colormap

After:
streamplot_colormap

PR Checklist

  • Has Pytest style unit tests
  • Code is Flake 8 compliant
  • New features are documented, with examples if plot related
  • Documentation is sphinx and numpydoc compliant
  • Added an entry to doc/users/next_whats_new/ if major new feature (follow instructions in README.rst there)
  • Documented in doc/api/api_changes.rst if API changed in a backward-incompatible way

@jklymak
Copy link
Member

jklymak commented Aug 27, 2018

I seem to remember someone already fixed something similar. Can you make sure this is still a problem in master? Thanks!

@tacaswell tacaswell added this to the v3.1 milestone Aug 27, 2018
@@ -473,6 +473,10 @@ def integrate(x0, y0):
return integrate


class OutOfBounds(Exception):
Copy link
Member

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?

Copy link
Contributor Author

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:
Copy link
Member

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.

Copy link
Contributor Author

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

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

@tacaswell
Copy link
Member

Thanks for your work on this! Left a few comments in-line about the exception handling logic.

I am a bit concerned about the - 1 change, that seems to be a pretty big change to the integrator?

@tomflannaghan
Copy link
Contributor Author

@tacaswell thanks a lot for the feedback. I'll reply inline to the code comments. The - 1 change is most obvious at low densities as it makes a row and a column of the mask redundant. These plots show density=3/30 which gives a mask size of 3x3. Without the - 1, the last row and column are off the grid so it appears only to be 2x2.

Before
figure_2

After
figure_1

@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.

@tacaswell
Copy link
Member

so the - 1 change is mostly about deciding if we need a stream line is a given location, not the computation of stream line once we decide we need one?

@tomflannaghan
Copy link
Contributor Author

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 - 1 issue.

The change does have a very small effect on the integration too though, as it rescales the error and speed by nx / (nx - 1) so they are in correct axes coordinates.

The change makes no difference to the direction of trajectories as u and v are already transformed correctly and interpolation isn't affected.

@jklymak
Copy link
Member

jklymak commented Sep 1, 2018

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 grid2mask would seem to bear that out.

@tomflannaghan
Copy link
Contributor Author

Another way to explain the -1 change in grid2mask is the following. The grid and mask are both ny by nx arrays, so the index [grid.ny-1, grid.nx-1] is the upper right corner of the data. This should map to the upper right corner of the mask which is at [mask.ny-1, mask.nx-1]. For grid2mask to do this correctly, it must map index xg in the grid to xm in the mask by

xm = xg * (mask.nx - 1) / (grid.nx - 1)

If you substitute in the example xg = grid.nx - 1, this gives xm = mask.nx - 1.

The current code, which does xm = xg * (mask.nx - 1) / grid.nx gets this slightly wrong. It would map the top right of the grid to [(mask.ny - 1) * (grid.ny - 1) / grid.ny, (mask.nx- 1) * (grid.nx - 1) / grid.nx]. In practice this means that the top-most and right-most columns of mask map to locations outside the grid.

Please let me know if anything doesn't make sense. Very happy to clarify further.

@jklymak
Copy link
Member

jklymak commented Sep 1, 2018

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

@tomflannaghan
Copy link
Contributor Author

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.

@jklymak
Copy link
Member

jklymak commented Oct 26, 2018

@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.

@tomflannaghan
Copy link
Contributor Author

@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?

@jklymak
Copy link
Member

jklymak commented Feb 26, 2019

@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.

@jklymak jklymak modified the milestones: v3.1.0, v3.2.0 Feb 26, 2019
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.
@tacaswell tacaswell force-pushed the streamplot_grid_bounds branch from b459fdd to e00e512 Compare August 28, 2019 01:14
@tacaswell
Copy link
Member

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 !

@tacaswell
Copy link
Member

Can someone with a mac build this locally and see how the images are failing?

@dopplershift
Copy link
Contributor

Looks like truly minor differences.

Baseline:
streamplot_maxlength

Output:
streamplot_maxlength

Diff:
streamplot_maxlength-failed-diff

I can't even see the difference in the diff images. I had to open them up in numpy to see the differences:
image

Just 2 pixels, only the red channel for some reason.

We are seeing 1bit differences on 2 pixel for this test between
linux/win vs mac.
@tacaswell
Copy link
Member

tacaswell commented Sep 1, 2019

We have seen similar off-by-one-bit differences between the platforms before

In [1]: 1 / 256                                                                                                                                               
Out[1]: 0.00390625

In [2]: 0.69411767 - 0.69803923                                                                                                                               
Out[2]: -0.003921559999999991

In [3]:  

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.

@tacaswell
Copy link
Member

Please squash merge this.

@dopplershift
Copy link
Contributor

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()

Old:
old
New:
new

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.

@dopplershift dopplershift merged commit a368170 into matplotlib:master Sep 3, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants