MAINT/BUG: Don't use 5-sided quadrilaterals in Axes3D.plot_surface#10001
MAINT/BUG: Don't use 5-sided quadrilaterals in Axes3D.plot_surface#10001jklymak merged 3 commits intomatplotlib:masterfrom
Conversation
lib/mpl_toolkits/mplot3d/axes3d.py
Outdated
| ztop = a[rs, cs:cs_next ] | ||
| zleft = a[rs:rs_next, cs_next ] | ||
| zbase = a[rs_next, cs_next:cs:-1] | ||
| zright = a[rs_next:rs:-1, cs ] |
There was a problem hiding this comment.
This is the first time I've ever found myself using a:b:-1 and it doing exactly what I wanted!
lib/mpl_toolkits/mplot3d/axes3d.py
Outdated
| for poly_i, ps in enumerate(polys): | ||
| # pick three points around the polygon to find the normal at | ||
| # hard to vectorize, since len(ps) is different at the edges | ||
| i1, i2, i3 = 0, int(len(ps)/3), int(2*len(ps)/3) |
There was a problem hiding this comment.
I feel like there's a much better approach here, but this is at least cheap, I suppose. Taking the middle point or even mean of each edge, and taking the cross product of the vectors joining opposite edges would be be a marginally better heuristic.
Either way, this is just cleanup, so I'm leaving it as is.
There was a problem hiding this comment.
Note that if I want to make the image output unchanged, then this needs to become (len(ps) + 1). Is per-pixel compatibility something that is cared about?
lib/mpl_toolkits/mplot3d/axes3d.py
Outdated
| row_inds = list(xrange(0, rows-1, rstride)) + [rows-1] | ||
| col_inds = list(xrange(0, cols-1, cstride)) + [cols-1] | ||
|
|
||
|
|
lib/mpl_toolkits/mplot3d/axes3d.py
Outdated
|
|
||
|
|
||
| #colset contains the data for coloring: either average z or the facecolor | ||
| #colset contains the sampled facecolors |
lib/mpl_toolkits/mplot3d/axes3d.py
Outdated
| zbase = a[min(rows-1, rs+rstride), cs:min(cols, cs+cstride+1):][::-1] | ||
| zright = a[rs:min(rows-1, rs+rstride):, cs][::-1] | ||
| # the edges of the projected quadrilateral | ||
| # note we use pythons half-open ranges to avoid repeating |
lib/mpl_toolkits/mplot3d/axes3d.py
Outdated
| # the edges of the projected quadrilateral | ||
| # note we use pythons half-open ranges to avoid repeating | ||
| # the corners | ||
| ztop = a[rs, cs:cs_next ] |
There was a problem hiding this comment.
Adds greatly to the clarity here though, so I'd maybe make an exception
| avgzsum = sum(p[2] for p in ps2) | ||
| polys.append(ps2) | ||
| # ps = np.stack(ps, axis=-1) | ||
| ps = np.array(ps).T |
There was a problem hiding this comment.
I wonder if there's some way of pre-allocating this with the lengths of row_inds and col_inds.
There was a problem hiding this comment.
No, because ps varies in length throughout the iteration
There was a problem hiding this comment.
Err, I might have had the wrong loop; the lengths of bits sliced out of X, Y, Z are not known?
There was a problem hiding this comment.
No. They correspond the perimeters of the "square" "cells", which are not always the same size - the cells look like:
+---+---+-+
| | | |
| | | |
+---+---+-+
| | | |
| | | |
+---+---+-+
| | | |
+---+---+-+
where the trailing edges contain non-uniformly sized cells. Perhaps we could vectorize across the uniform ones, and do the rest separately, but that's more work.
There was a problem hiding this comment.
I think we're talking about different things; I mean ps itself which should be uniform since you're calling np.array on it, no?
There was a problem hiding this comment.
Ah, I see. Yes, we could preallocate to (2*(rs_next - rs) + 2*(cs_next - cs), 3)
There was a problem hiding this comment.
Ok, here's the answer - preallocating doesn't help, because concatenate has no out argument until numpy 1.14
lib/mpl_toolkits/mplot3d/axes3d.py
Outdated
| # pick three points around the polygon to find the normal at | ||
| # hard to vectorize, since len(ps) is different at the edges | ||
| i1, i2, i3 = 0, int(len(ps)/3), int(2*len(ps)/3) | ||
| v1[poly_i] = ps[i1] - ps[i2] |
There was a problem hiding this comment.
I prefer to be explicit with known 2D things, i.e., v1[poly_i, :] = ps[i1, :] - ps[i2, :]; I don't think there's any penalty for it.
There was a problem hiding this comment.
There is a small penalty associated with parsing tuple indices over scalar indices, but I suppose it's probably not significant.
|
Looks like I got something wrong here... |
86e1518 to
893801f
Compare
|
Style fixes made, will investigate failures once I get my test setup working again |
893801f to
c38e675
Compare
|
@QuLogic: Worked out why it was failing - previously, we were plotting every quadrilateral with 5 points, which changed the normal vector! I've updated the images, and added another test that makes shading more obvious, and tests striding. |
lib/mpl_toolkits/mplot3d/axes3d.py
Outdated
| # are removed here. | ||
| ps = list(zip(*ps)) | ||
| lastp = np.array([]) | ||
| ps2 = [ps[0]] + [ps[i] for i in xrange(1, len(ps)) if ps[i] != ps[i-1]] |
There was a problem hiding this comment.
This didn't detect the fact that ps[-1] == ps[0], which was always true
c38e675 to
383f34d
Compare
383f34d to
cc92c08
Compare
|
Failures are PEP8 failures - one line that's 80 instead of 79, and then the extra spacing that I argue is worth keeping |
cc92c08 to
12517c8
Compare
|
Ping. The failure is the following PEP8 violation in these lines
|
|
I'm fine with putting a noqa. |
ca04037 to
866135c
Compare
|
Updated with:
|
1a734d8 to
bbc417e
Compare
lib/mpl_toolkits/mplot3d/axes3d.py
Outdated
|
|
||
| def boundary_edge(arr): | ||
| """ | ||
| Get the boundary elements of the rectangle ``arr``, |
There was a problem hiding this comment.
Happy to rename / extract this function if someone else can suggest a name / location.
There was a problem hiding this comment.
heh, I might call it "nibble()", because it nibbles around the edge of the 2D array. At the very least, I would expand the explanation a bit to get that point across. It took me a few minutes to realize that was what was done.
There was a problem hiding this comment.
Not a fan of nibble. How about one of
[array_](perimeter|edge|boundary|border)[_(elements|values)]
I think I like array_perimeter most.
Can you suggest a top-level place to put this in?
There was a problem hiding this comment.
Moved to cbook, and added an example
WeatherGod
left a comment
There was a problem hiding this comment.
This code is cleaner than before, which is definitely an improvement. Might be nice to figure out if there is a way to keep the arrays from getting ragged, but I don't think that is possible by the nature of the problem.
There still remains a little bit more to do here, particularly fixing the py3k issue.
lib/mpl_toolkits/mplot3d/axes3d.py
Outdated
|
|
||
| # evenly spaced, and including both endpoints | ||
| row_inds = list(xrange(0, rows-1, rstride)) + [rows-1] | ||
| col_inds = list(xrange(0, cols-1, cstride)) + [cols-1] |
There was a problem hiding this comment.
this isn't py3k compatible. xrange doesn't exist, right?
There was a problem hiding this comment.
Maybe np.array_split() would be cleaner? Don't know when it was added to numpy, but I do see it in v1.12.
There was a problem hiding this comment.
array_split produces non-overlapping chunks, but an interesting idea.
I assume that there was xrange = range or something, but I guess not. I'll switch to range
There was a problem hiding this comment.
It looks like I this came in after rebasing on #10525. Does matplotlib no longer support python 2?
There was a problem hiding this comment.
Am I right to target this against master?
There was a problem hiding this comment.
Yes, we can always backport if necessary (I don't think it is).
lib/mpl_toolkits/mplot3d/axes3d.py
Outdated
|
|
||
| def boundary_edge(arr): | ||
| """ | ||
| Get the boundary elements of the rectangle ``arr``, |
There was a problem hiding this comment.
heh, I might call it "nibble()", because it nibbles around the edge of the 2D array. At the very least, I would expand the explanation a bit to get that point across. It took me a few minutes to realize that was what was done.
bbc417e to
ac8f801
Compare
|
@WeatherGod: Updated, tests now all pass |
|
@WeatherGod: Anything else needed here? |
lib/matplotlib/cbook/__init__.py
Outdated
| delattr(obj, attr) | ||
| else: | ||
| setattr(obj, attr, orig) | ||
|
|
There was a problem hiding this comment.
Looks like a PEP8 check is not liking the blank line here? I thought we were supposed to have a blank line at the end of files?
There was a problem hiding this comment.
A new line, not a full blank line.
There was a problem hiding this comment.
Ah, I couldn't tell that with the github diffs. I like how git diff shows you those trailing blank lines.
There was a problem hiding this comment.
Bad merge I guess - will fix
lib/mpl_toolkits/mplot3d/axes3d.py
Outdated
| i1, i2, i3 = 0, len(ps)//3, 2*len(ps)//3 | ||
| v1[poly_i, :] = ps[i1, :] - ps[i2, :] | ||
| v2[poly_i, :] = ps[i2, :] - ps[i3, :] | ||
| normals = np.cross(v1, v2) |
There was a problem hiding this comment.
We need an else: normals = [] so that "normals" is always defined for the code blocks ahead.
There was a problem hiding this comment.
I don't think this happens in any real cases, but will fix instead of trying to work out which cases.
Better would be a local function that's only called when the result is needed.
There was a problem hiding this comment.
Setting to [] is an (existing) bug too, which I guess caused normals to be ignored when face colors were specified.
|
Besides those two points, I think this is ready to go. It is certainly easier to understand than before. |
Previously: * "cell" perimeters were clumsily calculated with duplicates, which were then (badly) removed at runtime. As a result, every quadrilateral was drawn with 5 vertices! * code to calculate normals was spread into multiple places * average z was calculated even if not used * normals were sometimes not calculated even when needed * repeated conversion between stride and count was done This will affect shading of plots very slightly, hence the image tests changing in this commit. Adds a `cbook._array_perimeter` function for use here.
|
@WeatherGod: All fixed - thanks for catching that |
|
@WeatherGod: Starting to undergo bitrot here. I've merged and solved conflicts again, but I'd rather not keep having to |
| else : | ||
| normals = [] | ||
|
|
||
| def get_normals(polygons): |
There was a problem hiding this comment.
Just noticed the existing _generate_normals() further down. We should make sure we don't have such duplication of near-similar things without at least justifying the duplication. Or figure out how to get rid of the duplicate logic.
There was a problem hiding this comment.
I'd argue that that's out of scope for this PR - I've not introduced that duplication, I just made it clearer that it exists (which makes it easier for someone else to remove later). Of course, that someone else might be me, but I'd rather do it in a separate unit of work.
There was a problem hiding this comment.
In particular, deciding which version to keep (assemble then cross vs cross then assemble) would require some profiling
|
it is amazing how much cleaner this code has gotten over the revisions! |
|
Once this goes in, I'll pick back up #9990 |
|
(I haven't actually followed the logic of the code, so I'll leave the review to others for now.) |
jklymak
left a comment
There was a problem hiding this comment.
Approving largely based on @WeatherGod approval and the fact that it passes the tests, and doesn't introduce any obvious bugs.
|
Thanks! I'll get back to #9990 in the next few weeks |
Previously:
Should have no visible behavior changes
This should be up to 25% faster, since the quadrilateral polygons now have 4 points, rather than 5 (!)