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

Skip to content

Commit b84c83d

Browse files
committed
MAINT/BUG: Simplify logic in plot_surface
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.
1 parent 98c627d commit b84c83d

File tree

10 files changed

+12158
-15159
lines changed

10 files changed

+12158
-15159
lines changed

lib/matplotlib/cbook/__init__.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2011,6 +2011,45 @@ def method(self, *args, **kwargs):
20112011
return cls
20122012

20132013

2014+
def _array_perimeter(arr):
2015+
"""
2016+
Get the elements on the perimeter of ``arr``,
2017+
2018+
Parameters
2019+
----------
2020+
arr : ndarray, shape (M, N)
2021+
The input array
2022+
2023+
Returns
2024+
-------
2025+
perimeter : ndarray, shape (2*(M - 1) + 2*(N - 1),)
2026+
The elements on the perimeter of the array::
2027+
2028+
[arr[0,0] ... arr[0,-1] ... arr[-1, -1] ... arr[-1,0] ...]
2029+
2030+
Examples
2031+
--------
2032+
>>> i, j = np.ogrid[:3,:4]
2033+
>>> a = i*10 + j
2034+
>>> a
2035+
array([[ 0, 1, 2, 3],
2036+
[10, 11, 12, 13],
2037+
[20, 21, 22, 23]])
2038+
>>> _array_perimeter(arr)
2039+
array([ 0, 1, 2, 3, 13, 23, 22, 21, 20, 10])
2040+
"""
2041+
# note we use Python's half-open ranges to avoid repeating
2042+
# the corners
2043+
forward = np.s_[0:-1] # [0 ... -1)
2044+
backward = np.s_[-1:0:-1] # [-1 ... 0)
2045+
return np.concatenate((
2046+
arr[0, forward],
2047+
arr[forward, -1],
2048+
arr[-1, backward],
2049+
arr[backward, 0],
2050+
))
2051+
2052+
20142053
@contextlib.contextmanager
20152054
def _setattr_cm(obj, **kwargs):
20162055
"""Temporarily set some attributes; restore original state at context exit.

lib/mpl_toolkits/mplot3d/axes3d.py

Lines changed: 36 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1683,72 +1683,60 @@ def plot_surface(self, X, Y, Z, *args, **kwargs):
16831683
if shade and cmap is not None and fcolors is not None:
16841684
fcolors = self._shade_colors_lightsource(Z, cmap, lightsource)
16851685

1686+
# evenly spaced, and including both endpoints
1687+
row_inds = list(range(0, rows-1, rstride)) + [rows-1]
1688+
col_inds = list(range(0, cols-1, cstride)) + [cols-1]
1689+
1690+
colset = [] # the sampled facecolor
16861691
polys = []
1687-
# Only need these vectors to shade if there is no cmap
1688-
if cmap is None and shade :
1689-
totpts = int(np.ceil((rows - 1) / rstride) *
1690-
np.ceil((cols - 1) / cstride))
1691-
v1 = np.empty((totpts, 3))
1692-
v2 = np.empty((totpts, 3))
1693-
# This indexes the vertex points
1694-
which_pt = 0
1695-
1696-
1697-
#colset contains the data for coloring: either average z or the facecolor
1698-
colset = []
1699-
for rs in range(0, rows-1, rstride):
1700-
for cs in range(0, cols-1, cstride):
1701-
ps = []
1702-
for a in (X, Y, Z):
1703-
ztop = a[rs,cs:min(cols, cs+cstride+1)]
1704-
zleft = a[rs+1:min(rows, rs+rstride+1),
1705-
min(cols-1, cs+cstride)]
1706-
zbase = a[min(rows-1, rs+rstride), cs:min(cols, cs+cstride+1):][::-1]
1707-
zright = a[rs:min(rows-1, rs+rstride):, cs][::-1]
1708-
z = np.concatenate((ztop, zleft, zbase, zright))
1709-
ps.append(z)
1710-
1711-
# The construction leaves the array with duplicate points, which
1712-
# are removed here.
1713-
ps = list(zip(*ps))
1714-
lastp = np.array([])
1715-
ps2 = [ps[0]] + [ps[i] for i in range(1, len(ps)) if ps[i] != ps[i-1]]
1716-
avgzsum = sum(p[2] for p in ps2)
1717-
polys.append(ps2)
1692+
for rs, rs_next in zip(row_inds[:-1], row_inds[1:]):
1693+
for cs, cs_next in zip(col_inds[:-1], col_inds[1:]):
1694+
ps = [
1695+
# +1 ensures we share edges between polygons
1696+
cbook._array_perimeter(a[rs:rs_next+1, cs:cs_next+1])
1697+
for a in (X, Y, Z)
1698+
]
1699+
# ps = np.stack(ps, axis=-1)
1700+
ps = np.array(ps).T
1701+
polys.append(ps)
17181702

17191703
if fcolors is not None:
17201704
colset.append(fcolors[rs][cs])
1721-
else:
1722-
colset.append(avgzsum / len(ps2))
1723-
1724-
# Only need vectors to shade if no cmap
1725-
if cmap is None and shade:
1726-
i1, i2, i3 = 0, int(len(ps2)/3), int(2*len(ps2)/3)
1727-
v1[which_pt] = np.array(ps2[i1]) - np.array(ps2[i2])
1728-
v2[which_pt] = np.array(ps2[i2]) - np.array(ps2[i3])
1729-
which_pt += 1
1730-
if cmap is None and shade:
1731-
normals = np.cross(v1, v2)
1732-
else :
1733-
normals = []
17341705

1706+
def get_normals(polygons):
1707+
"""
1708+
Takes a list of polygons and return an array of their normals
1709+
"""
1710+
v1 = np.empty((len(polygons), 3))
1711+
v2 = np.empty((len(polygons), 3))
1712+
for poly_i, ps in enumerate(polygons):
1713+
# pick three points around the polygon at which to find the normal
1714+
# doesn't vectorize because polygons is jagged
1715+
i1, i2, i3 = 0, len(ps)//3, 2*len(ps)//3
1716+
v1[poly_i, :] = ps[i1, :] - ps[i2, :]
1717+
v2[poly_i, :] = ps[i2, :] - ps[i3, :]
1718+
return np.cross(v1, v2)
1719+
1720+
# note that the striding causes some polygons to have more coordinates
1721+
# than others
17351722
polyc = art3d.Poly3DCollection(polys, *args, **kwargs)
17361723

17371724
if fcolors is not None:
17381725
if shade:
1739-
colset = self._shade_colors(colset, normals)
1726+
colset = self._shade_colors(colset, get_normals(polys))
17401727
polyc.set_facecolors(colset)
17411728
polyc.set_edgecolors(colset)
17421729
elif cmap:
1743-
colset = np.array(colset)
1744-
polyc.set_array(colset)
1730+
# doesn't vectorize because polys is jagged
1731+
avg_z = np.array([ps[:,2].mean() for ps in polys])
1732+
polyc.set_array(avg_z)
17451733
if vmin is not None or vmax is not None:
17461734
polyc.set_clim(vmin, vmax)
17471735
if norm is not None:
17481736
polyc.set_norm(norm)
17491737
else:
17501738
if shade:
1751-
colset = self._shade_colors(color, normals)
1739+
colset = self._shade_colors(color, get_normals(polys))
17521740
else:
17531741
colset = color
17541742
polyc.set_facecolors(colset)
Binary file not shown.

0 commit comments

Comments
 (0)