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

Skip to content

MAINT/BUG: Don't use 5-sided quadrilaterals in Axes3D.plot_surface #10001

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
merged 3 commits into from
Jul 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions lib/matplotlib/cbook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2011,6 +2011,45 @@ def method(self, *args, **kwargs):
return cls


def _array_perimeter(arr):
"""
Get the elements on the perimeter of ``arr``,

Parameters
----------
arr : ndarray, shape (M, N)
The input array

Returns
-------
perimeter : ndarray, shape (2*(M - 1) + 2*(N - 1),)
The elements on the perimeter of the array::

[arr[0,0] ... arr[0,-1] ... arr[-1, -1] ... arr[-1,0] ...]

Examples
--------
>>> i, j = np.ogrid[:3,:4]
>>> a = i*10 + j
>>> a
array([[ 0, 1, 2, 3],
[10, 11, 12, 13],
[20, 21, 22, 23]])
>>> _array_perimeter(arr)
array([ 0, 1, 2, 3, 13, 23, 22, 21, 20, 10])
"""
# note we use Python's half-open ranges to avoid repeating
# the corners
forward = np.s_[0:-1] # [0 ... -1)
backward = np.s_[-1:0:-1] # [-1 ... 0)
return np.concatenate((
arr[0, forward],
arr[forward, -1],
arr[-1, backward],
arr[backward, 0],
))


@contextlib.contextmanager
def _setattr_cm(obj, **kwargs):
"""Temporarily set some attributes; restore original state at context exit.
Expand Down
95 changes: 42 additions & 53 deletions lib/mpl_toolkits/mplot3d/axes3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -1655,15 +1655,15 @@ def plot_surface(self, X, Y, Z, *args, norm=None, vmin=None,
# Strides have priority over counts in classic mode.
# So, only compute strides from counts
# if counts were explicitly given
if has_count:
rstride = int(max(np.ceil(rows / rcount), 1))
cstride = int(max(np.ceil(cols / ccount), 1))
compute_strides = has_count
else:
# If the strides are provided then it has priority.
# Otherwise, compute the strides from the counts.
if not has_stride:
rstride = int(max(np.ceil(rows / rcount), 1))
cstride = int(max(np.ceil(cols / ccount), 1))
compute_strides = not has_stride

if compute_strides:
rstride = int(max(np.ceil(rows / rcount), 1))
cstride = int(max(np.ceil(cols / ccount), 1))

if 'facecolors' in kwargs:
fcolors = kwargs.pop('facecolors')
Expand All @@ -1681,71 +1681,60 @@ def plot_surface(self, X, Y, Z, *args, norm=None, vmin=None,
if shade and cmap is not None and fcolors is not None:
fcolors = self._shade_colors_lightsource(Z, cmap, lightsource)

# evenly spaced, and including both endpoints
row_inds = list(range(0, rows-1, rstride)) + [rows-1]
col_inds = list(range(0, cols-1, cstride)) + [cols-1]

colset = [] # the sampled facecolor
polys = []
# Only need these vectors to shade if there is no cmap
if cmap is None and shade :
totpts = int(np.ceil((rows - 1) / rstride) *
np.ceil((cols - 1) / cstride))
v1 = np.empty((totpts, 3))
v2 = np.empty((totpts, 3))
# This indexes the vertex points
which_pt = 0


#colset contains the data for coloring: either average z or the facecolor
colset = []
for rs in range(0, rows-1, rstride):
for cs in range(0, cols-1, cstride):
ps = []
for a in (X, Y, Z):
ztop = a[rs,cs:min(cols, cs+cstride+1)]
zleft = a[rs+1:min(rows, rs+rstride+1),
min(cols-1, cs+cstride)]
zbase = a[min(rows-1, rs+rstride), cs:min(cols, cs+cstride+1):][::-1]
zright = a[rs:min(rows-1, rs+rstride):, cs][::-1]
z = np.concatenate((ztop, zleft, zbase, zright))
ps.append(z)

# The construction leaves the array with duplicate points, which
# are removed here.
ps = list(zip(*ps))
ps2 = [ps[0]] + [ps[i] for i in range(1, len(ps)) if ps[i] != ps[i-1]]
avgzsum = sum(p[2] for p in ps2)
polys.append(ps2)
for rs, rs_next in zip(row_inds[:-1], row_inds[1:]):
for cs, cs_next in zip(col_inds[:-1], col_inds[1:]):
ps = [
# +1 ensures we share edges between polygons
cbook._array_perimeter(a[rs:rs_next+1, cs:cs_next+1])
for a in (X, Y, Z)
]
# ps = np.stack(ps, axis=-1)
ps = np.array(ps).T
polys.append(ps)

if fcolors is not None:
colset.append(fcolors[rs][cs])
else:
colset.append(avgzsum / len(ps2))

# Only need vectors to shade if no cmap
if cmap is None and shade:
i1, i2, i3 = 0, int(len(ps2)/3), int(2*len(ps2)/3)
v1[which_pt] = np.array(ps2[i1]) - np.array(ps2[i2])
v2[which_pt] = np.array(ps2[i2]) - np.array(ps2[i3])
which_pt += 1
if cmap is None and shade:
normals = np.cross(v1, v2)
else :
normals = []

def get_normals(polygons):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair enough

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In particular, deciding which version to keep (assemble then cross vs cross then assemble) would require some profiling

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is addressed in #12136

"""
Takes a list of polygons and return an array of their normals
"""
v1 = np.empty((len(polygons), 3))
v2 = np.empty((len(polygons), 3))
for poly_i, ps in enumerate(polygons):
# pick three points around the polygon at which to find the normal
# doesn't vectorize because polygons is jagged
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, :]
return np.cross(v1, v2)

# note that the striding causes some polygons to have more coordinates
# than others
polyc = art3d.Poly3DCollection(polys, *args, **kwargs)

if fcolors is not None:
if shade:
colset = self._shade_colors(colset, normals)
colset = self._shade_colors(colset, get_normals(polys))
polyc.set_facecolors(colset)
polyc.set_edgecolors(colset)
elif cmap:
colset = np.array(colset)
polyc.set_array(colset)
# doesn't vectorize because polys is jagged
avg_z = np.array([ps[:,2].mean() for ps in polys])
polyc.set_array(avg_z)
if vmin is not None or vmax is not None:
polyc.set_clim(vmin, vmax)
if norm is not None:
polyc.set_norm(norm)
else:
if shade:
colset = self._shade_colors(color, normals)
colset = self._shade_colors(color, get_normals(polys))
else:
colset = color
polyc.set_facecolors(colset)
Expand Down
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading