diff --git a/kwant/plotter.py b/kwant/plotter.py index c096ddd0bd058cd3bbc4e422398d9be151ec3b0d..04e3f1c235ce7d2f2532c7b61f42576ffd4639bd 100644 --- a/kwant/plotter.py +++ b/kwant/plotter.py @@ -60,270 +60,271 @@ def isarray(var): def nparray_if_array(var): return np.asarray(var) if isarray(var) else var - -class LineCollection(collections.LineCollection): - def __init__(self, segments, reflen=None, **kwargs): - super(LineCollection, self).__init__(segments, **kwargs) - self.reflen = reflen - - def set_linewidths(self, linewidths): - self._linewidths_orig = nparray_if_array(linewidths) - - def draw(self, renderer): - linewidths = self._linewidths_orig - if self.reflen is not None: - # Note: only works for aspect ratio 1! - # 72.0 - there is 72 points in an inch - factor = (self.axes.transData.frozen().to_values()[0] * 72.0 * - self.reflen / self.figure.dpi) - linewidths *= factor - - super(LineCollection, self).set_linewidths(linewidths) - return super(LineCollection, self).draw(renderer) - - -class PathCollection(collections.PathCollection): - def __init__(self, paths, sizes=None, reflen=None, **kwargs): - super(PathCollection, self).__init__(paths, sizes=sizes, **kwargs) - - self.reflen = reflen - self._linewidths_orig = nparray_if_array(self.get_linewidths()) - - self._transforms = [matplotlib.transforms.Affine2D().scale(x) for x in - sizes] - - def get_transforms(self): - return self._transforms - - def get_transform(self): - Affine2D = matplotlib.transforms.Affine2D - if self.reflen is not None: - # For the paths, use the data transformation but strip the offset - # (will be added later with offsets) - args = self.axes.transData.frozen().to_values()[:4] + (0, 0) - return Affine2D().from_values(*args).scale(self.reflen) - else: - return Affine2D().scale(self.figure.dpi / 72.0) - - def draw(self, renderer): - if self.reflen: - # Note: only works for aspect ratio 1! - factor = (self.axes.transData.frozen().to_values()[0] / - self.figure.dpi * 72.0 * self.reflen) - self.set_linewidths(self._linewidths_orig * factor) - - return collections.Collection.draw(self, renderer) - - -if has3d: - # Sorting is optional. - sort3d = True - - # Compute the projection of a 3D length into 2D data coordinates - # for this we use 2 3D half-circles that are projected into 2D. - # (This gives the same length as projecting the full unit sphere.) - - phi = np.linspace(0, pi, 21) - xyz = np.c_[np.cos(phi), np.sin(phi), 0 * phi].T.reshape(-1, 1, 21) - unit_sphere = np.bmat([[xyz[0], xyz[2]], [xyz[1], xyz[0]], - [xyz[2], xyz[1]]]) - unit_sphere = np.asarray(unit_sphere) - - def projected_length(ax, length): - rc = np.array([ax.get_xlim3d(), ax.get_ylim3d(), ax.get_zlim3d()]) - rc = np.apply_along_axis(np.sum, 1, rc) / 2. - - rs = unit_sphere * length + rc.reshape(-1, 1) - - transform = mplot3d.proj3d.proj_transform - rp = np.asarray(transform(*(list(rs) + [ax.get_proj()]))[:2]) - rc[:2] = transform(*(list(rc) + [ax.get_proj()]))[:2] - - coords = rp - np.repeat(rc[:2].reshape(-1, 1), len(rs[0]), axis=1) - return sqrt(np.sum(coords**2, axis=0).max()) - - - # Auxiliary array for calculating corners of a cube. - corners = np.zeros((3, 8, 6), np.float_) - corners[0, [0, 1, 2, 3], 0] = corners[0, [4, 5, 6, 7], 1] = \ - corners[0, [0, 1, 4, 5], 2] = corners[0, [2, 3, 6, 7], 3] = \ - corners[0, [0, 2, 4, 6], 4] = corners[0, [1, 3, 5, 7], 5] = 1. - - - class Line3DCollection(mplot3d.art3d.Line3DCollection): - def __init__(self, segments, reflen=None, zorder=0, **kwargs): - super(Line3DCollection, self).__init__(segments, **kwargs) +if mpl_enabled: + class LineCollection(collections.LineCollection): + def __init__(self, segments, reflen=None, **kwargs): + super(LineCollection, self).__init__(segments, **kwargs) self.reflen = reflen - self._zorder3d = zorder def set_linewidths(self, linewidths): self._linewidths_orig = nparray_if_array(linewidths) - def do_3d_projection(self, renderer): - super(Line3DCollection, self).do_3d_projection(renderer) - # The whole 3D ordering is flawed in mplot3d when several - # collections are added. We just use normal zorder. Note the - # "-" due to the different logic in the 3d plotting, we still want - # larger zorder values to be plotted on top of smaller ones. - return -self._zorder3d - def draw(self, renderer): linewidths = self._linewidths_orig - if self.reflen: - proj_len = projected_length(self.axes, self.reflen) - args = self.axes.transData.frozen().to_values() - # Note: unlike in the 2D case, where we can enforce equal - # aspect ratio, this (currently) does not work with - # 3D plots in matplotlib. As an approximation, we - # thus scale with the average of the x- and y-axis - # transformation. - factor = proj_len * (args[0] + - args[3]) * 0.5 * 72.0 / self.figure.dpi + if self.reflen is not None: + # Note: only works for aspect ratio 1! + # 72.0 - there is 72 points in an inch + factor = (self.axes.transData.frozen().to_values()[0] * 72.0 * + self.reflen / self.figure.dpi) linewidths *= factor - super(Line3DCollection, self).set_linewidths(linewidths) - super(Line3DCollection, self).draw(renderer) - - - class Path3DCollection(mplot3d.art3d.Patch3DCollection): - def __init__(self, paths, sizes, reflen=None, zorder=0, offsets=None, - **kwargs): - paths = [matplotlib.patches.PathPatch(path) for path in paths] + super(LineCollection, self).set_linewidths(linewidths) + return super(LineCollection, self).draw(renderer) - if offsets is not None: - kwargs['offsets'] = offsets[:, :2] - super(Path3DCollection, self).__init__(paths, **kwargs) - - if offsets is not None: - self.set_3d_properties(zs=offsets[:, 2], zdir="z") + class PathCollection(collections.PathCollection): + def __init__(self, paths, sizes=None, reflen=None, **kwargs): + super(PathCollection, self).__init__(paths, sizes=sizes, **kwargs) self.reflen = reflen - self._zorder3d = zorder - - self._paths_orig = np.array(paths, dtype='object') self._linewidths_orig = nparray_if_array(self.get_linewidths()) - self._linewidths_orig2 = self._linewidths_orig - self._array_orig = nparray_if_array(self.get_array()) - self._facecolors_orig = nparray_if_array(self.get_facecolors()) - self._edgecolors_orig = nparray_if_array(self.get_edgecolors()) - - Affine2D = matplotlib.transforms.Affine2D - self._orig_transforms = np.array([Affine2D().scale(x) for x in - sizes], dtype='object') - self._transforms = self._orig_transforms - def set_array(self, array): - self._array_orig = nparray_if_array(array) - super(Path3DCollection, self).set_array(array) - - def set_color(self, colors): - self._facecolors_orig = nparray_if_array(colors) - self._edgecolors_orig = self._facecolors_orig - super(Path3DCollection, self).set_color(colors) - - def set_edgecolors(self, colors): - colors = matplotlib.colors.colorConverter.to_rgba_array(colors) - self._edgecolors_orig = nparray_if_array(colors) - super(Path3DCollection, self).set_edgecolors(colors) + self._transforms = [matplotlib.transforms.Affine2D().scale(x) for x + in sizes] def get_transforms(self): - # this is exact only for an isometric projection, for the - # perspective projection used in mplot3d it's an approximation return self._transforms def get_transform(self): Affine2D = matplotlib.transforms.Affine2D - if self.reflen: - proj_len = projected_length(self.axes, self.reflen) - + if self.reflen is not None: # For the paths, use the data transformation but strip the - # offset (will be added later with the offsets). + # offset (will be added later with offsets) args = self.axes.transData.frozen().to_values()[:4] + (0, 0) - return Affine2D().from_values(*args).scale(proj_len) + return Affine2D().from_values(*args).scale(self.reflen) else: return Affine2D().scale(self.figure.dpi / 72.0) - def do_3d_projection(self, renderer): - xs, ys, zs = self._offsets3d + def draw(self, renderer): + if self.reflen: + # Note: only works for aspect ratio 1! + factor = (self.axes.transData.frozen().to_values()[0] / + self.figure.dpi * 72.0 * self.reflen) + self.set_linewidths(self._linewidths_orig * factor) + + return collections.Collection.draw(self, renderer) + + + if has3d: + # Sorting is optional. + sort3d = True + + # Compute the projection of a 3D length into 2D data coordinates + # for this we use 2 3D half-circles that are projected into 2D. + # (This gives the same length as projecting the full unit sphere.) + + phi = np.linspace(0, pi, 21) + xyz = np.c_[np.cos(phi), np.sin(phi), 0 * phi].T.reshape(-1, 1, 21) + unit_sphere = np.bmat([[xyz[0], xyz[2]], [xyz[1], xyz[0]], + [xyz[2], xyz[1]]]) + unit_sphere = np.asarray(unit_sphere) - # numpy complains about zero-length index arrays - if len(xs) == 0: + def projected_length(ax, length): + rc = np.array([ax.get_xlim3d(), ax.get_ylim3d(), ax.get_zlim3d()]) + rc = np.apply_along_axis(np.sum, 1, rc) / 2. + + rs = unit_sphere * length + rc.reshape(-1, 1) + + transform = mplot3d.proj3d.proj_transform + rp = np.asarray(transform(*(list(rs) + [ax.get_proj()]))[:2]) + rc[:2] = transform(*(list(rc) + [ax.get_proj()]))[:2] + + coords = rp - np.repeat(rc[:2].reshape(-1, 1), len(rs[0]), axis=1) + return sqrt(np.sum(coords**2, axis=0).max()) + + + # Auxiliary array for calculating corners of a cube. + corners = np.zeros((3, 8, 6), np.float_) + corners[0, [0, 1, 2, 3], 0] = corners[0, [4, 5, 6, 7], 1] = \ + corners[0, [0, 1, 4, 5], 2] = corners[0, [2, 3, 6, 7], 3] = \ + corners[0, [0, 2, 4, 6], 4] = corners[0, [1, 3, 5, 7], 5] = 1. + + + class Line3DCollection(mplot3d.art3d.Line3DCollection): + def __init__(self, segments, reflen=None, zorder=0, **kwargs): + super(Line3DCollection, self).__init__(segments, **kwargs) + self.reflen = reflen + self._zorder3d = zorder + + def set_linewidths(self, linewidths): + self._linewidths_orig = nparray_if_array(linewidths) + + def do_3d_projection(self, renderer): + super(Line3DCollection, self).do_3d_projection(renderer) + # The whole 3D ordering is flawed in mplot3d when several + # collections are added. We just use normal zorder. Note the + # "-" due to the different logic in the 3d plotting, we still + # want larger zorder values to be plotted on top of smaller + # ones. return -self._zorder3d - proj = mplot3d.proj3d.proj_transform_clip - vs = np.array(proj(xs, ys, zs, renderer.M)[:3]) - - if sort3d: - indx = vs[2].argsort()[::-1] - - self.set_offsets(vs[:2, indx].T) - - if len(self._paths_orig) > 1: - paths = np.resize(self._paths_orig, (vs.shape[1],)) - self.set_paths(paths[indx]) - - if len(self._orig_transforms) > 1: - self._transforms = np.resize(self._orig_transforms, - (vs.shape[1],)) - self._transforms = self._transforms[indx] - - if (isinstance(self._linewidths_orig, np.ndarray) and - len(self._linewidths_orig) > 1): - self._linewidths_orig2 = np.resize(self._linewidths_orig, - (vs.shape[1],))[indx] - - # Note: here array, facecolors and edgecolors are guaranteed to - # be 2d numpy arrays or None. (And array is the same - # length as the coordinates) - - if self._array_orig is not None: - super(Path3DCollection, - self).set_array(self._array_orig[indx]) - - if (self._facecolors_orig is not None and - self._facecolors_orig.shape[0] > 1): - shape = list(self._facecolors_orig.shape) - shape[0] = vs.shape[0] - super(Path3DCollection, self).set_facecolors( - np.resize(self._facecolors_orig, shape)[indx]) - - if (self._edgecolors_orig is not None and - self._edgecolors_orig.shape[0] > 1): - shape = list(self._edgecolors_orig.shape) - shape[0] = vs.shape[0] - super(Path3DCollection, self).set_edgecolors( - np.resize(self._edgecolors_orig, - shape)[indx]) - else: - self.set_offsets(vs[:2].T) + def draw(self, renderer): + linewidths = self._linewidths_orig + if self.reflen: + proj_len = projected_length(self.axes, self.reflen) + args = self.axes.transData.frozen().to_values() + # Note: unlike in the 2D case, where we can enforce equal + # aspect ratio, this (currently) does not work with + # 3D plots in matplotlib. As an approximation, we + # thus scale with the average of the x- and y-axis + # transformation. + factor = proj_len * (args[0] + + args[3]) * 0.5 * 72.0 / self.figure.dpi + linewidths *= factor + + super(Line3DCollection, self).set_linewidths(linewidths) + super(Line3DCollection, self).draw(renderer) + + + class Path3DCollection(mplot3d.art3d.Patch3DCollection): + def __init__(self, paths, sizes, reflen=None, zorder=0, + offsets=None, **kwargs): + paths = [matplotlib.patches.PathPatch(path) for path in paths] + + if offsets is not None: + kwargs['offsets'] = offsets[:, :2] + + super(Path3DCollection, self).__init__(paths, **kwargs) + + if offsets is not None: + self.set_3d_properties(zs=offsets[:, 2], zdir="z") + + self.reflen = reflen + self._zorder3d = zorder + + self._paths_orig = np.array(paths, dtype='object') + self._linewidths_orig = nparray_if_array(self.get_linewidths()) + self._linewidths_orig2 = self._linewidths_orig + self._array_orig = nparray_if_array(self.get_array()) + self._facecolors_orig = nparray_if_array(self.get_facecolors()) + self._edgecolors_orig = nparray_if_array(self.get_edgecolors()) + + Affine2D = matplotlib.transforms.Affine2D + self._orig_transforms = np.array([Affine2D().scale(x) for x in + sizes], dtype='object') + self._transforms = self._orig_transforms + + def set_array(self, array): + self._array_orig = nparray_if_array(array) + super(Path3DCollection, self).set_array(array) + + def set_color(self, colors): + self._facecolors_orig = nparray_if_array(colors) + self._edgecolors_orig = self._facecolors_orig + super(Path3DCollection, self).set_color(colors) + + def set_edgecolors(self, colors): + colors = matplotlib.colors.colorConverter.to_rgba_array(colors) + self._edgecolors_orig = nparray_if_array(colors) + super(Path3DCollection, self).set_edgecolors(colors) + + def get_transforms(self): + # this is exact only for an isometric projection, for the + # perspective projection used in mplot3d it's an approximation + return self._transforms + + def get_transform(self): + Affine2D = matplotlib.transforms.Affine2D + if self.reflen: + proj_len = projected_length(self.axes, self.reflen) + + # For the paths, use the data transformation but strip the + # offset (will be added later with the offsets). + args = self.axes.transData.frozen().to_values()[:4] + (0, 0) + return Affine2D().from_values(*args).scale(proj_len) + else: + return Affine2D().scale(self.figure.dpi / 72.0) + + def do_3d_projection(self, renderer): + xs, ys, zs = self._offsets3d + + # numpy complains about zero-length index arrays + if len(xs) == 0: + return -self._zorder3d + + proj = mplot3d.proj3d.proj_transform_clip + vs = np.array(proj(xs, ys, zs, renderer.M)[:3]) + + if sort3d: + indx = vs[2].argsort()[::-1] + + self.set_offsets(vs[:2, indx].T) + + if len(self._paths_orig) > 1: + paths = np.resize(self._paths_orig, (vs.shape[1],)) + self.set_paths(paths[indx]) + + if len(self._orig_transforms) > 1: + self._transforms = np.resize(self._orig_transforms, + (vs.shape[1],)) + self._transforms = self._transforms[indx] + + lw_orig = self._linewidths_orig + if (isinstance(lw_orig, np.ndarray) and len(lw_orig) > 1): + self._linewidths_orig2 = np.resize(lw_orig, + (vs.shape[1],))[indx] + + # Note: here array, facecolors and edgecolors are + # guaranteed to be 2d numpy arrays or None. (And + # array is the same length as the coordinates) + + if self._array_orig is not None: + super(Path3DCollection, + self).set_array(self._array_orig[indx]) + + if (self._facecolors_orig is not None and + self._facecolors_orig.shape[0] > 1): + shape = list(self._facecolors_orig.shape) + shape[0] = vs.shape[0] + super(Path3DCollection, self).set_facecolors( + np.resize(self._facecolors_orig, shape)[indx]) + + if (self._edgecolors_orig is not None and + self._edgecolors_orig.shape[0] > 1): + shape = list(self._edgecolors_orig.shape) + shape[0] = vs.shape[0] + super(Path3DCollection, self).set_edgecolors( + np.resize(self._edgecolors_orig, + shape)[indx]) + else: + self.set_offsets(vs[:2].T) - # the whole 3D ordering is flawed in mplot3d when several - # collections are added. We just use normal zorder, but correct - # by the projected z-coord of the "center of gravity", normalized - # by the projected z-coord of the world coordinates. - # In doing so, several Path3DCollections are plotted probably - # in the right order (it's not exact) if they have the same - # zorder. Still, smaller and larger integer zorders are plotted - # below or on top. + # the whole 3D ordering is flawed in mplot3d when several + # collections are added. We just use normal zorder, but correct + # by the projected z-coord of the "center of gravity", + # normalized by the projected z-coord of the world coordinates. + # In doing so, several Path3DCollections are plotted probably + # in the right order (it's not exact) if they have the same + # zorder. Still, smaller and larger integer zorders are plotted + # below or on top. - bbox = np.asarray(self.axes.get_w_lims()) + bbox = np.asarray(self.axes.get_w_lims()) - proj = mplot3d.proj3d.proj_transform_clip - cz = proj(*(list(np.dot(corners, bbox)) + [renderer.M]))[2] + proj = mplot3d.proj3d.proj_transform_clip + cz = proj(*(list(np.dot(corners, bbox)) + [renderer.M]))[2] - return -self._zorder3d + vs[2].mean() / cz.ptp() + return -self._zorder3d + vs[2].mean() / cz.ptp() - def draw(self, renderer): - if self.reflen: - proj_len = projected_length(self.axes, self.reflen) - args = self.axes.transData.frozen().to_values() - factor = proj_len * (args[0] + - args[3]) * 0.5 * 72.0 / self.figure.dpi + def draw(self, renderer): + if self.reflen: + proj_len = projected_length(self.axes, self.reflen) + args = self.axes.transData.frozen().to_values() + factor = proj_len * (args[0] + + args[3]) * 0.5 * 72.0 / self.figure.dpi - self.set_linewidths(self._linewidths_orig2 * factor) + self.set_linewidths(self._linewidths_orig2 * factor) - super(Path3DCollection, self).draw(renderer) + super(Path3DCollection, self).draw(renderer) # matplotlib helper functions. diff --git a/kwant/tests/test_plotter.py b/kwant/tests/test_plotter.py index bd12ef6f76eb964d742ef45be0c51f46566bdf28..55f87faff44492c7cabbea2dce025a17b7fc506c 100644 --- a/kwant/tests/test_plotter.py +++ b/kwant/tests/test_plotter.py @@ -26,7 +26,13 @@ def test_importable_without_matplotlib(): code = f.read() code = code.replace('from . import', 'from kwant import') code = code.replace('matplotlib', 'totalblimp') - exec code + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + exec code # Trigger the warning. + nose.tools.assert_equal(len(w), 1) + assert issubclass(w[0].category, RuntimeWarning) + assert "only iterator-providing functions" in str(w[0].message) def sys_2d(W=3, r1=3, r2=8):