Skip to content

SymmetricalLogLocator adds extra erroneous major ticks due to floating-point precision error #18757

Open
@JS3xton

Description

@JS3xton

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).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions