From 88e40cb8936778952de4c7ebbb6512e3f401d98c Mon Sep 17 00:00:00 2001 From: tillsten Date: Mon, 20 Apr 2020 22:21:16 +0200 Subject: [PATCH 1/2] Micro-optimize transforms, copy given array upon transform initialization. Fix revealed bug in backend bases, save some transformation copys --- lib/matplotlib/backend_bases.py | 17 ++++++------ lib/matplotlib/tests/test_backend_bases.py | 21 +++++++-------- lib/matplotlib/transforms.py | 30 ++++++++++++---------- 3 files changed, 36 insertions(+), 32 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index cf9453959853..8f4ae356dc7b 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -43,8 +43,8 @@ import matplotlib as mpl from matplotlib import ( - backend_tools as tools, cbook, colors, textpath, tight_bbox, transforms, - widgets, get_backend, is_interactive, rcParams) + backend_tools as tools, cbook, colors, textpath, tight_bbox, + transforms, widgets, get_backend, is_interactive, rcParams) from matplotlib._pylab_helpers import Gcf from matplotlib.backend_managers import ToolManager from matplotlib.transforms import Affine2D @@ -220,18 +220,17 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, recommended to use those generators, so that changes to the behavior of :meth:`draw_path_collection` can be made globally. """ - path_ids = [ - (path, transforms.Affine2D(transform)) - for path, transform in self._iter_collection_raw_paths( - master_transform, paths, all_transforms)] + path_ids = self._iter_collection_raw_paths(master_transform, + paths, all_transforms) for xo, yo, path_id, gc0, rgbFace in self._iter_collection( - gc, master_transform, all_transforms, path_ids, offsets, + gc, master_transform, all_transforms, list(path_ids), offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): path, transform = path_id - transform = transforms.Affine2D( - transform.get_matrix()).translate(xo, yo) + if xo != 0 or yo != 0: + transform = transform.frozen() + transform.translate(xo, yo) self.draw_path(gc0, path, transform, rgbFace) def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight, diff --git a/lib/matplotlib/tests/test_backend_bases.py b/lib/matplotlib/tests/test_backend_bases.py index 469b601ce0a7..842ed1f1ffce 100644 --- a/lib/matplotlib/tests/test_backend_bases.py +++ b/lib/matplotlib/tests/test_backend_bases.py @@ -14,7 +14,7 @@ def test_uses_per_path(): id = transforms.Affine2D() paths = [path.Path.unit_regular_polygon(i) for i in range(3, 7)] - tforms = [id.rotate(i) for i in range(1, 5)] + tforms_matrices = [id.rotate(i).get_matrix().copy() for i in range(1, 5)] offsets = np.arange(20).reshape((10, 2)) facecolors = ['red', 'green'] edgecolors = ['red', 'green'] @@ -37,17 +37,18 @@ def check(master_transform, paths, all_transforms, seen = np.bincount(ids, minlength=len(raw_paths)) assert set(seen).issubset([uses - 1, uses]) - check(id, paths, tforms, offsets, facecolors, edgecolors) - check(id, paths[0:1], tforms, offsets, facecolors, edgecolors) - check(id, [], tforms, offsets, facecolors, edgecolors) - check(id, paths, tforms[0:1], offsets, facecolors, edgecolors) + check(id, paths, tforms_matrices, offsets, facecolors, edgecolors) + check(id, paths[0:1], tforms_matrices, offsets, facecolors, edgecolors) + check(id, [], tforms_matrices, offsets, facecolors, edgecolors) + check(id, paths, tforms_matrices[0:1], offsets, facecolors, edgecolors) check(id, paths, [], offsets, facecolors, edgecolors) for n in range(0, offsets.shape[0]): - check(id, paths, tforms, offsets[0:n, :], facecolors, edgecolors) - check(id, paths, tforms, offsets, [], edgecolors) - check(id, paths, tforms, offsets, facecolors, []) - check(id, paths, tforms, offsets, [], []) - check(id, paths, tforms, offsets, facecolors[0:1], edgecolors) + check(id, paths, tforms_matrices, offsets[0:n, :], + facecolors, edgecolors) + check(id, paths, tforms_matrices, offsets, [], edgecolors) + check(id, paths, tforms_matrices, offsets, facecolors, []) + check(id, paths, tforms_matrices, offsets, [], []) + check(id, paths, tforms_matrices, offsets, facecolors[0:1], edgecolors) def test_get_default_filename(tmpdir): diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index 2a8fc834f3ff..2c01a2344e5b 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -36,6 +36,7 @@ import functools import textwrap import weakref +import math import numpy as np from numpy.linalg import inv @@ -1831,7 +1832,7 @@ def __init__(self, matrix=None, **kwargs): if matrix is None: # A bit faster than np.identity(3). matrix = IdentityTransform._mtx.copy() - self._mtx = matrix + self._mtx = matrix.copy() self._invalid = 0 __str__ = _make_str_method("_mtx") @@ -1914,8 +1915,8 @@ def rotate(self, theta): calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate` and :meth:`scale`. """ - a = np.cos(theta) - b = np.sin(theta) + a = math.cos(theta) + b = math.sin(theta) rotate_mtx = np.array([[a, -b, 0.0], [b, a, 0.0], [0.0, 0.0, 1.0]], float) self._mtx = np.dot(rotate_mtx, self._mtx) @@ -1930,7 +1931,7 @@ def rotate_deg(self, degrees): calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate` and :meth:`scale`. """ - return self.rotate(np.deg2rad(degrees)) + return self.rotate(math.radians(degrees)) def rotate_around(self, x, y, theta): """ @@ -1962,9 +1963,8 @@ def translate(self, tx, ty): calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate` and :meth:`scale`. """ - translate_mtx = np.array( - [[1.0, 0.0, tx], [0.0, 1.0, ty], [0.0, 0.0, 1.0]], float) - self._mtx = np.dot(translate_mtx, self._mtx) + self._mtx[0, 2] += tx + self._mtx[1, 2] += ty self.invalidate() return self @@ -1981,9 +1981,13 @@ def scale(self, sx, sy=None): """ if sy is None: sy = sx - scale_mtx = np.array( - [[sx, 0.0, 0.0], [0.0, sy, 0.0], [0.0, 0.0, 1.0]], float) - self._mtx = np.dot(scale_mtx, self._mtx) + # explicit element-wise scaling is fastest + self._mtx[0, 0] *= sx + self._mtx[0, 1] *= sx + self._mtx[0, 2] *= sx + self._mtx[1, 0] *= sy + self._mtx[1, 1] *= sy + self._mtx[1, 2] *= sy self.invalidate() return self @@ -1998,8 +2002,8 @@ def skew(self, xShear, yShear): calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate` and :meth:`scale`. """ - rotX = np.tan(xShear) - rotY = np.tan(yShear) + rotX = math.tan(xShear) + rotY = math.tan(yShear) skew_mtx = np.array( [[1.0, rotX, 0.0], [rotY, 1.0, 0.0], [0.0, 0.0, 1.0]], float) self._mtx = np.dot(skew_mtx, self._mtx) @@ -2017,7 +2021,7 @@ def skew_deg(self, xShear, yShear): calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate` and :meth:`scale`. """ - return self.skew(np.deg2rad(xShear), np.deg2rad(yShear)) + return self.skew(math.radians(xShear), math.radians(yShear)) class IdentityTransform(Affine2DBase): From 740ffbfa6d23414159b8f140d6d9c1fab922047e Mon Sep 17 00:00:00 2001 From: tillsten Date: Thu, 30 Apr 2020 10:13:31 +0200 Subject: [PATCH 2/2] added comment --- lib/matplotlib/backend_bases.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 8f4ae356dc7b..c5d9407bc6e3 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -228,7 +228,12 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): path, transform = path_id + # Only apply another translation if we have an offset, else we + # resuse the inital transform. if xo != 0 or yo != 0: + # The transformation can be used by multiple paths. Since + # translate is a inplace operation, we need to copy the + # transformation by .frozen() before applying the translation. transform = transform.frozen() transform.translate(xo, yo) self.draw_path(gc0, path, transform, rgbFace)