Skip to content

add imshow_simple_norm - #19623

Merged
pllim merged 7 commits into
astropy:mainfrom
eteq:add-simple-imshow-norm
May 1, 2026
Merged

pllim merged 7 commits into
astropy:mainfrom
eteq:add-simple-imshow-norm

Conversation

@eteq

@eteq eteq commented Apr 30, 2026 •

Copy link
Copy Markdown
Member

Description

This PR adds a function @larrybradley and I discussed in the hazy distant past when imshow_norm was first made: a convenient version of it that uses simple_norm's interface instead of ImageNormalize directly. The gist is that this allows a convenient and compact specification of imshow like this:

imshow_norm(image, min_percent=1, max_percent=99.9, stretch='log')

which makes imports more compact for simple quick-viz cases.

  • By checking this box, the PR author has requested that maintainers do NOT use the "Squash and Merge" button. Maintainers should respect this when possible; however, the final decision is at the discretion of the maintainer that merges the PR.

@github-actions

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Astropy! 🌌 This checklist is meant to remind the package maintainers who will review this pull request of some common things to look for.

  • Do the proposed changes actually accomplish desired goals?
  • Do the proposed changes follow the Astropy coding guidelines?
  • Are tests added/updated as required? If so, do they follow the Astropy testing guidelines?
  • Are docs added/updated as required? If so, do they follow the Astropy documentation guidelines?
  • Is rebase and/or squash necessary? If so, please provide the author with appropriate instructions. Also see instructions for rebase and squash.
  • Did the CI pass? If no, are the failures related? If you need to run daily and weekly cron jobs as part of the PR, please apply the "Extra CI" label. Codestyle issues can be fixed by the bot.
  • Is a change log needed? If yes, did the change log check pass? If no, add the "no-changelog-entry-needed" label. If this is a manual backport, use the "skip-changelog-checks" label unless special changelog handling is necessary.
  • Is this a big PR that makes a "What's new?" entry worthwhile and if so, is (1) a "what's new" entry included in this PR and (2) the "whatsnew-needed" label applied?
  • At the time of adding the milestone, if the milestone set requires a backport to release branch(es), apply the appropriate "backport-X.Y.x" label(s) before merge.

@neutrinoceros neutrinoceros left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hi Erik ! welcome to the astropy project, we hope you'll enjoy the contribution process !

fig.colorbar(im)
"""
if ax is None:
if not HAS_MATPLOTLIB:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why not move the import dance a the top level of the function ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This was just following from the above - but I think the motivation is that it's possible (albiet unlikely) that the user might pass in an ax object that is not from matplotlib but some matplotlib look-alike. And in that case we might as well try instead of erroring out. But if they give None we have to have plt to do plt.gca()

So I'd rather not change it since it is addressing a conceivable (albiet unlikely) use case.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we might as well try instead of erroring out

I think this hits close to a point I fundamentally disagree with. As a user, I much prefer my tools to say "sorry, I don't know what to do with this", than getting some uncontrolled error message nobody actually thought about later on.
But, you have status quo on your side, and there's no point making this function acting different than the rest of the API, so I won't push for it. Just wanted to highlight this one philosophical point of friction.

@neutrinoceros neutrinoceros May 1, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(anecdoctally, I think we, as a community, effectively trained users to see any traceback as a question mark (is this a bug, or it is just not supported ?), to the point I've seen people reporting NotImplementedErrors, asking if they found a bug, or if they could somehow work around it)

@eteq eteq May 1, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm not sure I agree with your broader point, but I'm not sure I disagree - that is, it's definitely a good point I'm just not sure how to balance the two conflicting desires/concerns. That said, I think, as you pointed out, this is a problem shared with the other function, so I'd rather we discuss this in a separate follow-on PR/issue (ideally after feature freeze! Since its been there for several years I think there's little harm in waiting another 6 mo to fix/change this)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

oh I can definitely get behind this plan ! I don't mean to block this, I'm just treating it as an opportunity to discover what hidden (to me) corners of astropy have in store

"want that."
)

imshow_kwargs = dict(kwargs)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

better reflects what I think is the intention here

Suggested change
imshow_kwargs = dict(kwargs)
imshow_kwargs = kwargs.copy()

"want that."
)

imshow_kwargs = dict(kwargs)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

actually why even bother with a copy ? this dict is owned by the function, we can mutate it as needed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It's because the next few lines split up the kwargs between snorm_kwargs and imshow_kwargs, and I felt it was confusing to leave one with just the name kwargs. And once I'm doing that it seemed better to keep kwargs itself pristine, and to me the dict(kwargs) construct better reflects the intention of "creating a new dictionary with a different purpose, but starting out with the same content as kwargs until we start popping things from it a few lines below".

That said, I guess the following would be equivalent as long as no one changes this in the future to use kwargs below:

Suggested change
imshow_kwargs = dict(kwargs)
imshow_kwargs = kwargs

and is like a few microseconds more performant 🤷 . I'm fine with it either way so whoever ends up merging I'm comfortable with them either applying the change above or not as they like.

imshow_kwargs["norm"] = SimpleNorm(**snorm_kwargs)(data)
imshow_result = ax.imshow(data, **imshow_kwargs)

return imshow_result, imshow_kwargs["norm"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

returning a dataclass instance would make it instantly clearer how to access which part. And if you absolutely need or want the tuple interface (mostly, unpacking ?), you could use a NamedTuple instead.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I disagree. I think it is more important that this function is consistent with imshow_norm above (an existing implementation not added by this PR).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agree with @pllim !

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

making return values into NamedTuple would be backward compatible and can easily be made consistently throughout the API. Though, if it's a consistency issue, better to stick with what you have for this PR. What do you guys think of moving to NamedTuples later ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

NamedTuple should be discussed as a new issue. Ultimately, as long as the changes here are consistent with existing implementation, I think it should be acceptable unless it is a bug. Thanks, all!

Comment thread astropy/visualization/mpl_normalize.py Outdated
return imshow_result, imshow_kwargs["norm"]


def imshow_simple_norm(data, ax=None, **kwargs):

@neutrinoceros neutrinoceros Apr 30, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

none of the arguments are easy to annotate, but the return value is, and that's very useful in itself

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Unfortunately, this is not as clean as one would like because I would need to import matplotlib to include the full annotation which triggers all the optional dependency annoyances. But I did add just tuple which is better than nothing!

@neutrinoceros neutrinoceros May 1, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

for reference, this is how you do it for a type-only import

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from matplotlib import MyType

def foo() -> "MyType": ...

the reason this works is that typing.TYPE_CHECKING is exactly False at runtime, but the standard specifies that type checkers treat it as True.



@pytest.mark.skipif(not HAS_PLT, reason="requires matplotlib")
def test_imshow_simple_norm():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

looks like 3 image tests in a trench coat to me

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it is fine as long as it passes?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, I was just following the other test like this - I think at the time the sub-package maintainers (which was I think @larrybradley and someone else?) preferred it that way for the convenience functions. I'm not against changing that, but I'd prefer to defer it to later when it can be done for the rest of the tests in the sub-pagcakge.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

My issue is that, in their current form, I can't really say that these test actually check the output. Without changing the entire nature of the test, adding a couple more assertions would already help.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@eteq To get this in before feature freeze, could you add a few more assertions?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Point taken, done in the last commit! Probably would make sense to port those tests back to simple_imshow but I think that's not this PR so as not to mix cocnerns.

Comment thread astropy/visualization/mpl_normalize.py Resolved
Co-authored-by: P. L. Lim <[email protected]>
Co-authored-by: Erik Tollerud <[email protected]>
@pllim pllim added this to the v8.0.0 milestone May 1, 2026
@eteq

eteq commented May 1, 2026

Copy link
Copy Markdown
Member Author

I believe the last couple of commits addressed all the comments (@neutrinoceros may not entirely agree but I think we're on the same page of "not critical so could be addressed in a PR after feature freeze to let this get in today)

@larrybradley larrybradley left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, @eteq. We can address the followup issues after feature freeze.

@pllim
pllim merged commit b3807dd into astropy:main May 1, 2026
37 of 38 checks passed
@pllim

pllim commented May 1, 2026

Copy link
Copy Markdown
Member

I opened #19635 as follow-up. Hopefully I didn't miss anything. Feel free to correct if I did.

Thanks, all!

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.

4 participants