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

Skip to content

Commit 2b8103b

Browse files
PERF: Speed up log and symlog scale transforms (matplotlib#30993)
* speed up symlog transforms * speed up log transform * Privatize log and symlog transform attrs, expose via properties * Deprecate InvertedSymmetricalLogTransform._invlinthresh * Setters for log and symlog properties * Update stub file * Switch to lambda functions for logscale * Fix pickling of log transforms * Simplify log symlog structure * Deprecation decorator --------- Co-authored-by: Scott Shambaugh <[email protected]>
1 parent c1e4831 commit 2b8103b

5 files changed

Lines changed: 53 additions & 24 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
``InvertedSymmetricalLogTransform.invlinthresh``
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
4+
The ``invlinthresh`` attribute of `.InvertedSymmetricalLogTransform` is
5+
deprecated. Use the ``.inverted().transform(linthresh)`` method instead.

lib/matplotlib/scale.py

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ def __init__(self, base, nonpositive='clip'):
292292
self.base = base
293293
self._clip = _api.getitem_checked(
294294
{"clip": True, "mask": False}, nonpositive=nonpositive)
295+
self._log_funcs = {np.e: np.log, 2: np.log2, 10: np.log10}
295296

296297
def __str__(self):
297298
return "{}(base={}, nonpositive={!r})".format(
@@ -300,12 +301,11 @@ def __str__(self):
300301
def transform_non_affine(self, values):
301302
# Ignore invalid values due to nans being passed to the transform.
302303
with np.errstate(divide="ignore", invalid="ignore"):
303-
log = {np.e: np.log, 2: np.log2, 10: np.log10}.get(self.base)
304-
if log: # If possible, do everything in a single call to NumPy.
305-
out = log(values)
304+
log_func = self._log_funcs.get(self.base)
305+
if log_func:
306+
out = log_func(values)
306307
else:
307-
out = np.log(values)
308-
out /= np.log(self.base)
308+
out = np.log(values) / np.log(self.base)
309309
if self._clip:
310310
# SVG spec says that conforming viewers must support values up
311311
# to 3.4e38 (C float); however experiments suggest that
@@ -329,12 +329,17 @@ class InvertedLogTransform(Transform):
329329
def __init__(self, base):
330330
super().__init__()
331331
self.base = base
332+
self._exp_funcs = {np.e: np.exp, 2: np.exp2}
332333

333334
def __str__(self):
334335
return f"{type(self).__name__}(base={self.base})"
335336

336337
def transform_non_affine(self, values):
337-
return np.power(self.base, values)
338+
exp_func = self._exp_funcs.get(self.base)
339+
if exp_func:
340+
return exp_func(values)
341+
else:
342+
return np.exp(values * np.log(self.base))
338343

339344
def inverted(self):
340345
return LogTransform(self.base)
@@ -449,17 +454,20 @@ def __init__(self, base, linthresh, linscale):
449454
self.base = base
450455
self.linthresh = linthresh
451456
self.linscale = linscale
452-
self._linscale_adj = (linscale / (1.0 - self.base ** -1))
453-
self._log_base = np.log(base)
454457

455458
def transform_non_affine(self, values):
459+
linscale_adj = self.linscale / (1.0 - 1.0 / self.base)
460+
log_base = np.log(self.base)
461+
456462
abs_a = np.abs(values)
463+
inside = abs_a <= self.linthresh
464+
if np.all(inside): # Fast path: all values in linear region
465+
return values * linscale_adj
457466
with np.errstate(divide="ignore", invalid="ignore"):
458467
out = np.sign(values) * self.linthresh * (
459-
self._linscale_adj +
460-
np.log(abs_a / self.linthresh) / self._log_base)
461-
inside = abs_a <= self.linthresh
462-
out[inside] = values[inside] * self._linscale_adj
468+
linscale_adj - np.log(self.linthresh) / log_base +
469+
np.log(abs_a) / log_base)
470+
out[inside] = values[inside] * linscale_adj
463471
return out
464472

465473
def inverted(self):
@@ -472,21 +480,35 @@ class InvertedSymmetricalLogTransform(Transform):
472480

473481
def __init__(self, base, linthresh, linscale):
474482
super().__init__()
475-
symlog = SymmetricalLogTransform(base, linthresh, linscale)
483+
if base <= 1.0:
484+
raise ValueError("'base' must be larger than 1")
485+
if linthresh <= 0.0:
486+
raise ValueError("'linthresh' must be positive")
487+
if linscale <= 0.0:
488+
raise ValueError("'linscale' must be positive")
476489
self.base = base
477490
self.linthresh = linthresh
478-
self.invlinthresh = symlog.transform(linthresh)
479491
self.linscale = linscale
480-
self._linscale_adj = (linscale / (1.0 - self.base ** -1))
492+
493+
@_api.deprecated("3.11", name="invlinthresh", obj_type="attribute",
494+
alternative=".inverted().transform(linthresh)")
495+
@property
496+
def invlinthresh(self):
497+
invlinthresh = self.inverted().transform(self.linthresh)
498+
return invlinthresh
481499

482500
def transform_non_affine(self, values):
501+
linscale_adj = self.linscale / (1.0 - 1.0 / self.base)
502+
invlinthresh = self.inverted().transform(self.linthresh)
503+
483504
abs_a = np.abs(values)
505+
inside = abs_a <= invlinthresh
506+
if np.all(inside): # Fast path: all values in linear region
507+
return values / linscale_adj
484508
with np.errstate(divide="ignore", invalid="ignore"):
485-
out = np.sign(values) * self.linthresh * (
486-
np.power(self.base,
487-
abs_a / self.linthresh - self._linscale_adj))
488-
inside = abs_a <= self.invlinthresh
489-
out[inside] = values[inside] / self._linscale_adj
509+
out = np.sign(values) * self.linthresh * np.exp(
510+
(abs_a / self.linthresh - linscale_adj) * np.log(self.base))
511+
out[inside] = values[inside] / linscale_adj
490512
return out
491513

492514
def inverted(self):

lib/matplotlib/scale.pyi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,9 @@ class InvertedSymmetricalLogTransform(Transform):
9898
output_dims: int
9999
base: float
100100
linthresh: float
101-
invlinthresh: float
102101
linscale: float
102+
@property
103+
def invlinthresh(self) -> float: ...
103104
def __init__(self, base: float, linthresh: float, linscale: float) -> None: ...
104105
def inverted(self) -> SymmetricalLogTransform: ...
105106

lib/matplotlib/tests/test_axes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7378,7 +7378,7 @@ def test_loglog():
73787378

73797379

73807380
@image_comparison(["test_loglog_nonpos.png"], remove_text=True, style='mpl20',
7381-
tol=0 if platform.machine() == 'x86_64' else 0.029)
7381+
tol=0.029)
73827382
def test_loglog_nonpos():
73837383
fig, axs = plt.subplots(3, 3)
73847384
x = np.arange(1, 11)

lib/matplotlib/tests/test_colors.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -632,8 +632,9 @@ def inverse(x):
632632
norm = mcolors.FuncNorm((forward, inverse), vmin=0.1, vmax=10)
633633
lognorm = mcolors.LogNorm(vmin=0.1, vmax=10)
634634
assert_array_almost_equal(norm([0.2, 5, 10]), lognorm([0.2, 5, 10]))
635-
assert_array_almost_equal(norm.inverse([0.2, 5, 10]),
636-
lognorm.inverse([0.2, 5, 10]))
635+
# use assert_allclose here for rtol on large numbers
636+
np.testing.assert_allclose(norm.inverse([0.2, 5, 10]),
637+
lognorm.inverse([0.2, 5, 10]))
637638

638639

639640
def test_TwoSlopeNorm_autoscale():

0 commit comments

Comments
 (0)