Skip to content

MNT: simplify valid tick logic #12158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 8, 2019

Conversation

jklymak
Copy link
Member

@jklymak jklymak commented Sep 18, 2018

PR Summary

This is a much scaled back first-step PR for #12105.

The logic for deciding whether or not to draw a tick was rather convoluted, and depended on the size of a pixel. This PR simply changes the test for whether a tick is in the view limit to one that depends on a relative tolerance to take up floating point slack (like np.allclose, but for an interval). The relevant code is in transforms.inverval_contains.

See also #11004

This passes all previous tests except for the test_axes/specgram_* tests.

These had a tick at 0 before becuase 0 was within half a pixel of the minimimum view limit of 0.2. Clearly this tick should not be drawn, and the half a pixel slop was far too large. Worse, if the figure was widened in interactive mode, the tick would disappear:

import numpy as np
import matplotlib.pyplot as plt

fig, ax = plt.subplots(dpi=100, figsize=(4, 2))
ax.set_xlim([0.2, 500])
plt.show()

figure_1a

figure_1b

With this PR, the tick is never drawn (because 0<0.2).

Another minimal example (w/o resizing)

import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.set_ylim(0, 1.199)

fig.savefig('boo100.png', dpi=100)
fig.savefig('boo200.png', dpi=200)

Before

100 dpi

boo100

200 dpi

boo200

After

100 dpi

boo100

200 dpi

As above...

PR Checklist

  • Has Pytest style unit tests
  • Code is Flake 8 compliant
  • New features are documented, with examples if plot related
  • Documentation is sphinx and numpydoc compliant
  • Added an entry to doc/users/next_whats_new/ if major new feature (follow instructions in README.rst there)
  • Documented in doc/api/api_changes.rst if API changed in a backward-incompatible way

@jklymak jklymak force-pushed the mnt-simplify-validtick branch from 157b0e1 to f49d364 Compare September 18, 2018 21:55
@jklymak jklymak added this to the v3.1 milestone Sep 19, 2018
@jklymak
Copy link
Member Author

jklymak commented Sep 19, 2018

Ping @efiring

if tick is None:
continue
# NB: always update labels and position to avoid issues like #9397
tick.update_position(loc)
tick.set_label1(label)
tick.set_label2(label)
if not mtransforms.interval_contains(interval_expanded, loc):
try:
loct = self.get_transform().transform(loc)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if it really makes a performance difference, but AFAICS transform() works on numpy arrays. So this could be pulled out of the loop and calculated in one go. Well, except for the AssertionError - when does that occur?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still have to do the loop, so can probably keep as-is for now.

I think the Assertion Error happened in CI, possibly get_transform returned None for some wacky axis.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah its from the doc build: gallery/scales/custom_scale.py.

Exception occurred:
  File "/home/circleci/project/lib/matplotlib/transforms.py", line 1436, in transform
    assert not np.ma.is_masked(res)  # just to be on the safe side
AssertionError

The custom scale uses masks, and transforms.transform doesn't like it.

The previous implementation didn't call the transform, but this one does so that we can do the tolerance checking in log-scale rather than data space.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure that there are still any ticks on that example when we hit this?

@@ -2892,7 +2892,7 @@ def nonsingular(vmin, vmax, expander=0.001, tiny=1e-15, increasing=True):
return vmin, vmax


def interval_contains(interval, val):
def interval_contains(interval, val, rtol=1e-10):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like so as to not break downstream users relying on this being exact, the default should be rtol=0

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eric-wieser you probably understand the numerical issues better than me, but is this comparison ever "exact"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, comparison is always exact. The danger is when you make assumptions about rounding in operations prior to the comparison, but that's for the caller to decide.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, done...

except AssertionError:
loct = None
continue
if ((loct is None) or
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can transform() return None? Otherwise we do not need the loct is None check here.

@jklymak jklymak force-pushed the mnt-simplify-validtick branch from 98f1560 to 2ee3e6d Compare October 20, 2018 21:29
@jklymak jklymak force-pushed the mnt-simplify-validtick branch from 2ee3e6d to 3d4976b Compare November 5, 2018 03:10
@@ -2906,10 +2906,39 @@ def interval_contains(interval, val):
Returns
-------
bool
Returns true if given val is within the interval.
Returns true if given val is within the interval (with tolerance)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one says nothing about tolerance

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

@jklymak jklymak force-pushed the mnt-simplify-validtick branch from c6df889 to 227bed5 Compare November 17, 2018 22:00
@tacaswell
Copy link
Member

We should make sure this does not break wcsaxes (attn @astrofrog )

# some scales might make them, so we need this try/except.
loct = None
continue
if not mtransforms.interval_contains_close(inter, loct):
Copy link
Contributor

@anntzer anntzer Jan 3, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have examples where we need interval_contains_close and not interval_contains here? Why couldn't the locators be in charge of making sure whatever they return is in the interval and adjusting the values if needed (except for those outside the interval needed for classic mode)?

I don't really like the idea of hardcoding the 1e-10 tolerance here when it's something that can clearly(?) be avoided.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hah, we currently have a 1/2-pixel tolerance! Surely 1e-10 * interval is preferable?

The problem with limits is when people compute them.

On my machine:

3.51-1.51 == 2.0
False

In [17]: abs(3.51-1.51- 2.0) < 1e-10
Out[17]: True

Copy link
Contributor

@anntzer anntzer Jan 3, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it doesn't make things worse, but at least let's keep interval_contains_close private?

The point is that the Locator API should say something like the following to implementers (including 3rd party locators):

You may return tick values inside or outside of the axis bounds, but any value outside of the bounds (with no tolerance) will be dropped (except in "classic" mode, where we will keep the first outside value on either side).

and the locator classes are the one which should do something like

xmin, xmax = ...
ticks = ...
if xmin - tol < ticks[0] < xmin: ticks[0] = xmin

(or ticks[1] if that's the one corresponding to xmin).
i.e. the locator displaces the tick to match xmin if it's just below xmin.

But I guess that can wait.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue with locators is that some uses want the bracketing ticks, unfortunately, for contour for instance... We could fix/change that as well, but beyond this PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed it's beyond this PR.
The design above does not preclude bracketing ticks.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anntzer did you really want interval_contains_close to be private? I actually think its useful to take into account floating point slop, but don't feel strongly about it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it feels like a hack while waiting for locators to be fixed, but I'm not going to insist on it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okey doke - done!

FIX: make contains_close private
@jklymak jklymak force-pushed the mnt-simplify-validtick branch from 227bed5 to 01d7455 Compare January 4, 2019 17:08
Copy link
Member

@timhoffm timhoffm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks ok by itself.

Not merging yet because of @tacaswell's concern with respect to wcsaxes. Can someone confirm, that this is not breaking them?

@astrofrog
Copy link
Contributor

I'm checking it and will report back shortly

Copy link
Contributor

@astrofrog astrofrog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works fine with WCSAxes (all tests, including image tests, pass), in part because we do all the tick drawing ourselves.

@timhoffm timhoffm merged commit d23bcea into matplotlib:master Jan 8, 2019
@timhoffm
Copy link
Member

timhoffm commented Jan 8, 2019

@astrofrog Thanks for testing.

@jklymak
Copy link
Member Author

jklymak commented Jan 8, 2019

Yes, thanks all!

@jklymak jklymak deleted the mnt-simplify-validtick branch January 8, 2019 19:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants