Description
Bug summary:
SymmetricalLogLocator
appears to suffer a floating-point precision error in calculating the boundaries of the log regions of a symlog
plot, which results in the addition of extra major ticks in the linear region of the plot.
Code reproducing the issue:
import numpy as np
import matplotlib.pyplot as plt # v3.2.2
x = y = np.linspace(-1e5,1e5,100)
plt.plot(x,y)
plt.xscale('symlog', linthreshx=1e3, subsx=[2,3,4,5,6,7,8,9])
(OS=Windows 10, Python=3.8.3, Anaconda=2020.07)
Output (with error highlighted in red):
Discussion:
Major ticks generated by SymmetricalLogLocator
sometimes include an extra tick in the linear region:
>>> locator = mpl.ticker.SymmetricalLogLocator(linthresh=1e3, base=10)
>>> locator.tick_values(vmin=0, vmax=1e6)
# array([0.e+00, 1.e+02, 1.e+03, 1.e+04, 1.e+05, 1.e+06]) <-- 1.e+02 is in the linear region
SymmetricalLogLocator
calculates the inner (towards zero) boundaries of the log regions via the following base-agnostic logarithm:
lo = np.floor(np.log(linthresh) / np.log(base))
However, when base=10
, some power-of-ten thresholds cause floating-point precision problems:
>>> base=10
>>> [np.log(linthresh)/np.log(base) for linthresh in base**np.arange(10)]
# [0.0, 1.0, 2.0, 2.9999999999999996, 4.0, 5.0, 5.999999999999999, 7.0, 8.0, 8.999999999999998]
With the np.floor()
calculation, this introduces an additional erroneous innermost major tick.
@dstansby proposed a possible fix to a related issue in #14309, which changes np.floor()
to np.ceil()
. This looks like it would work with the precision errors I identified above, which all err towards zero, but it would fail with bases that err away from zero, e.g., base=5
:
>>> base=5
>>> [np.log(linthresh)/np.log(base) for linthresh in base**np.arange(10)]
# [0.0, 1.0, 2.0, 3.0000000000000004, 4.0, 5.0, 6.000000000000001, 7.0, 8.0, 9.0]
Other possible solutions I can think of: (1) rounding to base
within a certain proximity, or (2) adding a special case when base=10
wherein np.log10()
is used instead of np.log()/np.log(base)
.