FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

MPL 3 + Colorbar + PowerNorm bug · Issue #13228 · matplotlib/matplotlib · GitHub

Repository navigation

MPL 3 + Colorbar + PowerNorm bug #13228

Closed

Description

Bug report

Bug summary

In MPL v3, when using Colorbar with a linear norm, then changing norm to PowerNorm, breaks the Colorbar under some circumstances. Worked fine in MPL v1 and v2.

Code for reproduction

Minimum necessary test case:

fig, ax = plt.subplots()

data = np.clip(randn(250, 250)*10, 0, 10)

cax = ax.imshow(data)
cbar = fig.colorbar(cax)

cax.norm = mpl.colors.PowerNorm(gamma=2, vmin=0, vmax=10)
cbar.set_norm(cax.norm)
cbar.update_normal(cax)

plt.show()

On matplotlib 3.0.2 the (bug?) result is:

On matplotlib 2.2.3 the (expected) result is:

Result when adding LogNorm to ColorbarBase._use_auto_colorbar_locator (see discussion below) on matplotlib 3.0.2

Discussion

Few points:

  1. If the image's norm is set prior to fig.colorbar, then the expected result is achieved on 3.0.2 as well.
  2. If vmin/vmax are not set, the result seems correct and matches between 3.0.2 and 2.2.3
  3. cbar.update_bruteforce instead of update_normal does not fix the issue

I looked into the colorbar code, and can see the following:

In ColorbarBase.update_ticks(), if the auto-colorbar is first used (i.e. first branch), then setting PowerNorm calls the second branch (i.e. what it calls the fixed locator branch) once Colorbar.update_normal runs. That exact sequence triggers the issue.

If things are forced to the first branch of ColorbarBase.update_ticks() (e.g. via adding to PowerNorm to ColorbarBase._use_auto_colorbar_locator() ), then a similar result to what one gets from MPL 3.0.2 LogNorm is achieved. If things are forced to the second branch, then the result from MPL 2.2.3 is achieved. However, when first one branch is hit and then PowerNorm hits the second branch, this triggers the issue.

Relevant code from ColorbarBase,

    def _use_auto_colorbar_locator(self):

        return (self.boundaries is None
                and self.values is None
                and ((type(self.norm) == colors.Normalize)
                    or (type(self.norm) == colors.LogNorm)))

    def update_ticks():

        # ... 

        if self._use_auto_colorbar_locator():
            _log.debug('Using auto colorbar locator on colorbar')
            _log.debug('locator: %r', locator)
            long_axis.set_major_locator(locator)
            long_axis.set_major_formatter(formatter)
        else:
            _log.debug('Using fixed locator on colorbar')
            ticks, ticklabels, offset_string = self._ticker(locator, formatter)
            long_axis.set_ticks(ticks)
            long_axis.set_ticklabels(ticklabels)
            long_axis.get_major_formatter().set_offset_string(offset_string)

Expected outcome

Either:

  1. functionality of MPL 2.2.3
  2. functionality of adding PowerNorm to ColorbarBase._use_auto_colorbar_locator.

Activity

  1. jklymak commented on Jan 19, 2019

    Member

    Not fully parsing this. But if PowerNorm is added to the list in _use_auto_color_locator does everything work?

    Note colorbar handling was changed 3.0 for as many cases as possible but it’s possible that it doesn’t fit this pattern of calling things.

  2. QuLogic commented on Jan 19, 2019

    Member

    This looks something like #12682.

  3. self-assigned this
    on Jan 19, 2019
  4. jklymak commented on Jan 19, 2019

    Member
    1. I don't think you should (need) to call cbar.set_norm(cax.norm). If you change the mappable, we should be able to deal with the new norm on the mappable.
    2. update_normal and update_bruteforce are definitely messed up.

    My test code uses LogNorm, just to make it clear that the issue is not PowerNorm.

    import matplotlib.pyplot as plt
    import matplotlib as mpl
    import numpy as np
    
    N = 100
    X, Y = np.mgrid[0:3:complex(0, N), 0:2:complex(0, N)]
    Z1 = (1 + np.sin(Y * 10.)) * X**(2.)
    
    print(np.min(Z1))
    data = Z1
    
    fig, ax = plt.subplots(2, 2, constrained_layout=True)
    ax = ax.flat
    
    cax = ax[0].pcolormesh(data, cmap='RdBu_r', rasterized=True)
    # , norm=mpl.colors.PowerNorm(gamma=2, vmin=0, vmax=50))
    cbar = fig.colorbar(cax, ax = ax[0])
    
    cax = ax[1].pcolormesh(data, cmap='RdBu_r', norm=mpl.colors.LogNorm(vmin=0.001, vmax=18), rasterized=True)
    cbar = fig.colorbar(cax, ax=ax[1])
    
    cax = ax[2].pcolormesh(data, cmap='RdBu_r', rasterized=True)
    cbar = fig.colorbar(cax, ax=ax[2])
    cax.set_norm(mpl.colors.LogNorm(vmin=0.001, vmax=18))
    cbar.update_bruteforce(cax)
    
    cax = ax[3].pcolormesh(data, cmap='RdBu_r', rasterized=True)
    cbar = fig.colorbar(cax, ax=ax[3])
    cax.set_norm(mpl.colors.LogNorm(vmin=0.001, vmax=18))
    cbar.update_normal(cax)
    
    plt.show()

  5. LevN0 commented on Jan 20, 2019

    ContributorAuthor

    @jklymak - Yes, the specific issue I reported with PowerNorm does appear to work if added to _use_auto_color_locator, though I did limited experimentation. See 3rd image in above report for what that particular result looks like if that change was made. I am not exactly sure why hitting the second branch after running the first produces such a strange outcome though, I could not dig down that far. Some solution to the issue would be great, because this has worked since at least ~v1.0.

    Regarding LogNorm, I am not actually seeing much change in your test code/plots compared to behavior in 2.2.3. Some things are clearly inconsistent, but at least all of them can be fixed with a couple lines of code. The issue at the top is different between 2.2.3 and 3.0.2, and the only way I can see to fix them is to revert the action of long_axis.set_major_locator(locator) and long_axis.set_major_formatter(formatter), and this is new behavior. Actually there was an additional thing introduced in that LogNorm sets log axis for colorbar as of MPL v3, but update_normal does not revert to linear if the norm is changed - this is kind of unfortunate, though at least update_bruteforce does work in that case. The PowerNorm issue is not only new, but also not resolved with either update_normal or update_bruteforce.

    @QuLogic - Agreed on similarity.

  6. jklymak commented on Jan 20, 2019

    Member

    OK, Thanks. sure we can make this work - I just had no idea folks would set the norm post-facto so that never got tested. Clearly our testing is inadequate for this feature. But if the above is a good enough test, then I think we can figure out how to make update_normal work.

    Maybe someone should comment on what theidea behaviour should be here. To me:

    cax = ax[2].pcolormesh(data, cmap='RdBu_r', rasterized=True)
    cbar = fig.colorbar(cax, ax=ax[2])
    cax.set_norm(mpl.colors.LogNorm(vmin=0.001, vmax=18))
    

    should just work, and we shouldn't have to call anything like update_normal or update_bruteforce.

    ping @efiring because I think he will know the history here...

  7. LevN0 commented on Jan 20, 2019

    ContributorAuthor

    sure we can make this work

    Sounds good to me!

    I just had no idea folks would set the norm post-facto so that never got tested.

    To explain a bit of reasoning here, the application in question allows user to change the norm. So it comes up as linear by default, but lets them change norm and min/max of norm. Similar to functionality of e.g. SAO DS9.

  8. jklymak commented on Jan 20, 2019

    Member

    Are you able to download PRs and test from the dev version? I think I have a fix, but it'd be good to know it works for you

  9. LevN0 commented on Jan 20, 2019

    ContributorAuthor

    Thank you for working on this! This partially resolves the issue, however here is another method to trigger it:

    fig, ax = plt.subplots()
    
    data = np.clip(randn(250, 250)*10, 0, 10)
    
    cax = ax.imshow(data)
    cbar = fig.colorbar(cax)
    
    cbar.set_ticks([0, 2, 4, 6, 10])
    cax.norm = mpl.colors.PowerNorm(gamma=2, vmin=0, vmax=10)
    cbar.set_norm(cax.norm)
    cbar.update_normal(cax)
    
    plt.show()

    This is identical to the original test case, except for the cbar.set_ticks addition. The same issue occurs if ticks are added directly to fig.colorbar call. The ticks manually set in the example are identical to what MPL v3 comes up with on its own if no ticks are specified.

  10. jklymak commented on Jan 20, 2019

    Member

    OK, I'd propose that changing the norm resets the locator and formatter to whatever the default is for that norm, so if you want to specify ticks, you'd need to do so again after changing the norm. So the following

    fig, ax = plt.subplots()
    cax = ax.pcolormesh(data, cmap='RdBu_r', rasterized=True)
    cbar = fig.colorbar(cax, ax=ax)
    
    print('original', cbar.locator)
    cbar.set_ticks([0, 10, 20])
    print('first set ticks', cbar.locator)
    cax.set_norm(mpl.colors.PowerNorm(gamma=2, vmin=0.001, vmax=18))
    print('after norm changed', cbar.locator)
    cbar.set_ticks(np.arange(0, 20, 2))
    print('after ticks set second time', cbar.locator)
    plt.show()

    gives

    original <matplotlib.colorbar._ColorbarAutoLocator object at 0x127311be0>
    first set ticks <matplotlib.ticker.FixedLocator object at 0x127351320>
    after norm changed <matplotlib.colorbar._ColorbarAutoLocator object at 0x127311d68>
    after ticks set second time <matplotlib.ticker.FixedLocator object at 0x1272f1080>
    
  11. LevN0 commented on Jan 20, 2019

    ContributorAuthor

    I noticed that,

        def update_ticks():
    
            # ... 
    
            if self._use_auto_colorbar_locator():
                _log.debug('Using auto colorbar locator on colorbar')
                _log.debug('locator: %r', locator)
                long_axis.set_major_locator(locator)
                long_axis.set_major_formatter(formatter)
            else:
                _log.debug('Using fixed locator on colorbar')
                locator.set_axis(None)                 # new line
                formatter.set_axis(None)             # new line
                ticks, ticklabels, offset_string = self._ticker(locator, formatter)
                long_axis.set_ticks(ticks)
                long_axis.set_ticklabels(ticklabels)
                long_axis.get_major_formatter().set_offset_string(offset_string)

    Adding the two new lines, that reset the axis prior to self._ticker seems to cure the originally reported issue. Internally self._ticker calls create_dummy_axis on both locator and formatter, so perhaps it is not supposed to have any other axis prior to that? I do not have sufficient understanding to know what ill effects it might have though.

  12. jklymak commented on Jan 20, 2019

    Member

    @LevN0 Can you check vs #13234 and post any code that doesn't work there? I'm not sure how you are getting into the else: part of that loop.

  13. LevN0 commented on Jan 20, 2019

    ContributorAuthor

    @jklymak Apologies, did not realize there had been a further change in the PR since it was first referenced.

    Indeed, the linear -> PowerNorm -> linear works here. Thank you!

    A couple possibly related questions. Not sure if another issue would be more appropriate for the first. Here is a test case,

    fig, ax = plt.subplots()
    
    data = randn(250, 250)*1000
    
    cax = ax.imshow(data)
    cbar = fig.colorbar(cax)
    
    cax.norm = mpl.colors.LogNorm(vmin=1, vmax=1000)
    cbar.set_norm(cax.norm)
    cbar.update_normal(cax)
    
    cax.norm = mpl.colors.Normalize(vmin=-1000, vmax=1000)
    cbar.set_norm(cax.norm)
    cbar.update_normal(cax)

    The result maintains a log axis, and spits out a bunch of warnings.

    I believe the issue is that LogNorm sets axis to log, and nothing in update_normal or elsewhere adjusts it back on norm change. Perhaps there should be an else in ColorbarBase.config_axis?

    The other part is that the new scheme for LogNorm to use log-axis seems to effectively prohibit arbitrary ticks, since they do not show up unless they fall on a major tick mark. Is there any way around this? An option to maintain linear axes for LogNorm as had been the case in v1 and v2 might have been nice, but this maybe asking too much.

  14. jklymak commented on Jan 20, 2019

    Member

    OK, you shouldn't set the mappables norm by hand, but should use set_norm instead. I agree we shouldn't expose cax.norm, but that will be hard to deprecate.

    But agreed this had some issues, so see update.

  15. LevN0 commented on Jan 20, 2019

    ContributorAuthor

    All the previously reported issues seem gone. A change seems to have broken (it is ignored) the format attribute of Colorbar, which worked in MPL 3.0.2. E.g., see:

    fig, ax = plt.subplots()
    
    data = randn(250, 250)
    
    cax = ax.imshow(data)
    cbar = fig.colorbar(cax, format='%4.2e')

    I am (very positively) surprised by the high level of effort you are giving to this issue, thanks!

  16. jklymak commented on Jan 21, 2019

    Member

    Well, I broke it, so I have to fix it.

    We are running into the issue that the user can set and access cbar.locator/formatter directly, but that we want it to magically reset itself when the norm changes. I think we can still make this all work, but its turning out to not be trivial.

  17. efiring commented on Jan 21, 2019

    Member

    Would it help to turn some attributes into properties?

  18. jklymak commented on Jan 21, 2019

    Member

    I think so but o have one other thing to try first.

  19. jklymak commented on Jan 21, 2019

    Member

    I guess the real question is if we want user set locators and formatters to survive a norm change or not. This PR says not. If we want yes, but still do something intelligent if it’s never been set then we prob need properties.

  20. jklymak commented on Jan 21, 2019

    Member

    OK, #13234 has addressed all the issues I think we've identified so far, and added tests for them all...

    Ended up not needing properties.

    @LevN0 - care to see how it is working now?

  21. LevN0 commented on Jan 21, 2019

    ContributorAuthor

    All the previously reported issues seem fixed. I could not find any issues with this version, though I am sure my tests are super incomplete.

    A question: why is Colorbar.minorticks_off disabled for log-axis? I understand minor ticks being turned on by default for LogNorm, but why is the user disallowed from turning them off? If the user adjusts the major ticks via set_ticks and does not have them at powers of 10, minor ticks which seem to locate for powers of 10, make the colorbar more confusing in my opinion.

  22. jklymak commented on Jan 21, 2019

    Member

    OK< great. I'll mark it as not WIP

    The minor ticks thing is a nuisance as well, but can you open a new Issue for that? I think it can be dealt with in another PR. The current PR is long enough as it is..

  23. LevN0 commented on Jan 21, 2019

    ContributorAuthor

    Will do, thanks for the work on this one!

  24. modified the milestones: v3.0.3, v3.1 on Feb 3, 2019
  25. jklymak commented on Feb 3, 2019

    Member

    Re-milestonig 3.1. This is a bug, but the fix is too intrusive to be for a big-release (and 3.1 is on its way soonish anyway).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions


    Back | FazBrowse Home | New Git URL