Skip to content

PSF image representation in NDData/CCDData#13743

Merged
pllim merged 21 commits into
astropy:mainfrom
parejkoj:ccddata-psf-representation
Oct 28, 2022
Merged

PSF image representation in NDData/CCDData#13743
pllim merged 21 commits into
astropy:mainfrom
parejkoj:ccddata-psf-representation

Conversation

@parejkoj
Copy link
Copy Markdown
Contributor

@parejkoj parejkoj commented Sep 23, 2022

Description

This pull request adds a simple PSF image to NDData/CCDData, to support the Rubin Observatory LSST Prompt Processing alert distribution system. The LSST alert packet image cutouts will be sent as astropy CCDData objects, and the only thing we see as missing from that format is the PSF. For our purposes for these image cutouts, all we need is a realization of the PSF image at the image center, not a generic parameterized or model PSF.

TODO:

  • Are any further tests or functionality necessary?
  • Does this serialize to the various astropy outputs, or do I need to add code for that?
  • Document how to make use of this PSF representation (but I'm not sure where such documentation would go).
  • Document how to use the astropy flux-preserving warping code to move the PSF around within the image?

Checklist for package maintainer(s)

This checklist is meant to remind the package maintainer(s) who will review this pull request of some common things to look for. This list is not exhaustive.

  • 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 "When to rebase and squash commits".
  • 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?
  • Is a milestone set? Milestone must be set but astropy-bot check might be missing; do not let the green checkmark fool you.
  • 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.

Comment thread astropy/nddata/nddata.py Outdated
meta = MetaData(doc=_meta_doc, copy=False)

def __init__(self, data, uncertainty=None, mask=None, wcs=None,
def __init__(self, data, uncertainty=None, mask=None, wcs=None, psf=None,
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.

Is it safer to put new keyword in the end in case people been passing in keywords as pos args?

Copy link
Copy Markdown
Contributor Author

@parejkoj parejkoj Sep 28, 2022

Choose a reason for hiding this comment

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

While adding this, I wondered if we shouldn't modify this API to make everything after data be keyword-only, since there's a lot of them? So it would look like: __init__(self, data, *, uncertainty=None, ...).

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.

Depends on who you ask. It would definitely break someone's code but if you ask @Cadair , he would be like 🔥 🔥 🔥

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.

While adding this, I wondered if we shouldn't modify this API to make everything after data be keyword-only, since there's a lot of them?

I don't think we want to do this in minor version bump; there may be issues also with the @use_nddata decorator but I don't recall the details of that.

@pllim pllim added this to the v5.2 milestone Sep 27, 2022
@pllim pllim requested a review from mwcraig September 27, 2022 22:40
@pllim
Copy link
Copy Markdown
Member

pllim commented Sep 27, 2022

Thanks! Definitely need a change log.

Maybe a What's New too but I'll let others decide on that one.

Copy link
Copy Markdown
Member

@mwcraig mwcraig left a comment

Choose a reason for hiding this comment

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

@parejkoj -- this looks good. Can you please add an example in the narrative documentation?

It also needs a changelog, and I think it warrants a "What's new" entry too.

One question: why not just shove the PSF into the metadata?

@parejkoj
Copy link
Copy Markdown
Contributor Author

One question: why not just shove the PSF into the metadata?

That is one of the things I was wondering about: I think it hinges on the question of how much structure you want to have in the CCDData object. Having a dedicated name for the PSF makes it readily discoverable, and we can enforce the type of the object. LSST's equivalent to CCDData (afw.Exposure) is a C++ object underneath, so it needs to have a pretty rigid structure and contain explicit types and names for all that "metadata".

I can think of a handful of other things one might add as named objects on CCDData (e.g. an LSF for spectra), but none of them are as broadly useful as the PSF, to my mind. Eventually extending this PSF to include model representations seems like a future next step, but we don't need it for our purposes, and there's not really any way to generically represent a model PSF. I think the question is "what, besides WCS, should be a first-class named parameter?" I think the PSF is the next thing. An object representing the photometric calibration isn't as broadly applicable as the WCS and PSF are, otherwise it would be my next suggestion.

@mwcraig
Copy link
Copy Markdown
Member

mwcraig commented Oct 17, 2022

@parejkoj -- it is best to mention me in replies -- my github notifications are a train wreck because of conda-forge (I know I could fix that, but) (and still my bad for not checking back sooner).

Having a dedicated name for the PSF makes it readily discoverable, and we can enforce the type of the object. LSST's equivalent to CCDData (afw.Exposure) is a C++ object underneath, so it needs to have a pretty rigid structure and contain explicit types and names for all that "metadata".

That makes sense -- I'm fine with adding it at a fairly high level.

Do you have time to do the changelog and some basics documentation this week?

@parejkoj
Copy link
Copy Markdown
Contributor Author

@mwcraig :

Do you have time to do the changelog and some basics documentation this week?

I'm trying to finish it up now.

One of the difficulties I've had is figuring out exactly where I should put all the necessary functionality and tests. I'm fixing the one line I missed that the coverage test pointed out, but I don't really know where else I need to add code. Suggestions very welcome there.

@parejkoj parejkoj force-pushed the ccddata-psf-representation branch 2 times, most recently from 1b8062f to 76b639f Compare October 20, 2022 23:40
@parejkoj
Copy link
Copy Markdown
Contributor Author

@mwcraig : Ok, I believe I've added working FITS write/read support and a test that demonstrates it, there's a note in the changelog and what's new, and I've added some notes to the narrative docs (though I'm not sure what else should go there). Further comments very welcome.

Copy link
Copy Markdown
Contributor

@dhomeier dhomeier left a comment

Choose a reason for hiding this comment

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

Some comments from a relatively outsider point of view.
To elaborate a bit more on the requirements a PSF image has to fulfil, all the docs mention it should be 'normalized', but that does not seem to be checked on input anywhere – at least the somewhat pathological np.random.normal(size=[20, 20]) used in the tests generally is not (where by 'normalized' I'd understand that convolution with such a PSF will be automatically flux-preserving; if something else is meant, that should probably be pointed out in the docs as well).

Comment thread astropy/nddata/tests/test_ccddata.py Outdated
Comment thread astropy/nddata/ccddata.py Outdated
Comment thread docs/nddata/ccddata.rst Outdated
Copy link
Copy Markdown
Member

@mwcraig mwcraig left a comment

Choose a reason for hiding this comment

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

This is close to ready to go. One last big question: how should PSF's be handled in arithmetic operations? My inclination is to treat it like metadata, where I think we punt to the user to decide what the resulting metadata should be. The logic for several of the other properties is here: https://github.com/astropy/astropy/blob/main/astropy/nddata/mixins/ndarithmetic.py#L231

Comment thread astropy/nddata/nddata.py Outdated
@parejkoj
Copy link
Copy Markdown
Contributor Author

@mwcraig : The PSF should definitely not be modified during arithmetic operations. This sort of question is one of the reasons the LSST CCDData-like object (afw.Exposure) does not have any arithmetic operations itself, only the "image array" class does (afw.MaskedImage, which includes the mask and variance planes and which are properly handled during arithmetic operations).

@mwcraig
Copy link
Copy Markdown
Member

mwcraig commented Oct 27, 2022

pre-commit.ci autofix

Copy link
Copy Markdown
Member

@mwcraig mwcraig left a comment

Choose a reason for hiding this comment

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

Assuming the CI passes I am ok with merging this. Thanks for getting this going @parejkoj!

@dhomeier -- thanks for your detailed review! I think all of your comments have been addressed, and I've opened an issue about improving the documentation landing page with the intent of fixing it for 6.0.

@pllim
Copy link
Copy Markdown
Member

pllim commented Oct 27, 2022

@dhomeier requested changes -- does he want to re-review?

@pllim
Copy link
Copy Markdown
Member

pllim commented Oct 27, 2022

Do the commits need squashing? 😸

@dhomeier
Copy link
Copy Markdown
Contributor

Thanks for opening the issue on documentation!

@mwcraig
Copy link
Copy Markdown
Member

mwcraig commented Oct 27, 2022

Do the commits need squashing? 😸

@pllim -- I can do the squash if you want (I don't have strong feelings either way)

Comment thread astropy/nddata/ccddata.py Outdated
Parameters
----------
hdu_mask, hdu_uncertainty, hdu_flags : str or None, optional
hdu_mask, hdu_uncertainty, hdu_flags, hdu_psf: str or None, optional
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.

Suggested change
hdu_mask, hdu_uncertainty, hdu_flags, hdu_psf: str or None, optional
hdu_mask, hdu_uncertainty, hdu_flags, hdu_psf : str or None, optional

Comment thread astropy/nddata/ccddata.py Outdated
Comment thread astropy/nddata/ccddata.py
If the unit is ``None`` or not otherwise specified it will raise a
``ValueError``

psf : `numpy.ndarray` or None, optional
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.

tl;dr -- Is Quantity support planned for the future?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I believe a psf image should always be unitless, but I'm not positive of that.

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 am just thinking what if someone stored it as data with pixel unit attached to it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

hmmm... I'm not sure what to do on that point: I think the psf image as being purely an array (because the thing you do with it is (de)convolve it with the image), and I can't think of a reason someone would want it to have pixel units or what would be gained by that. Might be worth waiting to see how use-cases shake out and if anyone requests it?

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.

That is fine. I was just curious if this was discussed.

Comment thread astropy/nddata/ccddata.py Outdated
Comment thread astropy/nddata/mixins/ndarithmetic.py Outdated

# If both are None, there is nothing to do.
if self.psf is not None or operand.psf is not None:
warnings.warn(f"Not setting psf attribute during {operation.__name__}.", RuntimeWarning)
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.

Why not AstropyUserWarning?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I didn't even know that was a thing. I don't have a strong opinion either way; your suggestion might make the warning more obvious to astropy users?

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.

Yes, I do think AstropyUserWarning is more explicit (short of creating your own warning class).

def test_ccddata_with_psf():
psf = _random_psf.copy()
ccd = CCDData(_random_array.copy(), unit=u.adu, psf=psf)
assert (ccd.psf == psf).all()
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.

Why not use np.testing.assert_array_equal? Same question for all similar occurrences.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was just following the other tests above.

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 the reasoning was that the created ccd should have an exact copy of the input psf in this case (and the other similar cases) so we really do want to check for exact equality.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not that it really matters, but np.testing.assert_array_equal is exact equality. np.testing.assert_array_almost_equal is probably the one you're thinking of.

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.

Maybe changing assert (a == b).all() to np.testing.assert_array_equal(a, b) can be a follow-up PR if you are interested.

Comment thread astropy/nddata/tests/test_ccddata.py Outdated
Comment thread astropy/nddata/tests/test_ccddata.py Outdated
ccd.psf = "something"


def test_write_read_psf(tmpdir):
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.

Please use tmp_path. We just went through a mini-campaign to get rid of tmpdir.

Comment thread docs/changes/nddata/13743.feature.rst Outdated
Comment thread docs/nddata/ccddata.rst Outdated
@mwcraig
Copy link
Copy Markdown
Member

mwcraig commented Oct 27, 2022

Thanks for the review @pllim -- I'll update around noon locally

Co-authored-by: P. L. Lim <2090236+pllim@users.noreply.github.com>
Comment thread astropy/nddata/ccddata.py Outdated
Comment thread astropy/nddata/ccddata.py Outdated
Comment thread astropy/nddata/mixins/ndarithmetic.py
Comment thread astropy/nddata/mixins/ndarithmetic.py Outdated

# If both are None, there is nothing to do.
if self.psf is not None or operand.psf is not None:
warnings.warn(f"Not setting psf attribute during {operation.__name__}.", RuntimeWarning)
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.

You'll have to add the import yourself. I cannot suggest it at the line where it is supposed to go. Now precommit complains if it is not at the right place.

Suggested change
warnings.warn(f"Not setting psf attribute during {operation.__name__}.", RuntimeWarning)
warnings.warn(f"Not setting psf attribute during {operation.__name__}.", AstropyUserWarning)

Comment thread astropy/nddata/mixins/tests/test_ndarithmetic.py Outdated
Comment on lines +1225 to +1228
# no warning if both are None
with warnings.catch_warnings():
warnings.simplefilter("error")
ndd2.add(ndd2)
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 this still applies.

warnings.simplefilter("error")
ndd2.add(ndd2)

with pytest.warns(RuntimeWarning, match="Not setting psf attribute during add"):
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.

Suggested change
with pytest.warns(RuntimeWarning, match="Not setting psf attribute during add"):
with pytest.warns(AstropyUserWarning, match="Not setting psf attribute during add"):


with pytest.warns(RuntimeWarning, match="Not setting psf attribute during add"):
ndd1.add(ndd2)
with pytest.warns(RuntimeWarning, match="Not setting psf attribute during add"):
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.

Suggested change
with pytest.warns(RuntimeWarning, match="Not setting psf attribute during add"):
with pytest.warns(AstropyUserWarning, match="Not setting psf attribute during add"):

ndd1.add(ndd2)
with pytest.warns(RuntimeWarning, match="Not setting psf attribute during add"):
ndd2.add(ndd1)
with pytest.warns(RuntimeWarning, match="Not setting psf attribute during add"):
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.

Suggested change
with pytest.warns(RuntimeWarning, match="Not setting psf attribute during add"):
with pytest.warns(AstropyUserWarning, match="Not setting psf attribute during add"):

@mwcraig
Copy link
Copy Markdown
Member

mwcraig commented Oct 27, 2022

@pllim -- thanks for the review. I think all of your comments have been addressed.

Comment thread astropy/nddata/tests/test_ccddata.py Outdated
Co-authored-by: P. L. Lim <2090236+pllim@users.noreply.github.com>
@mwcraig
Copy link
Copy Markdown
Member

mwcraig commented Oct 27, 2022

Hopefully this time I really have them all!

Copy link
Copy Markdown
Member

@pllim pllim left a comment

Choose a reason for hiding this comment

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

LGTM now. Thanks!

@pllim
Copy link
Copy Markdown
Member

pllim commented Oct 27, 2022

Interesting... Could it be that you actually support Quantity by accident?

_______________________________ test_psf_setter ________________________________

    def test_psf_setter():
        psf = _random_psf.copy()
        ccd = CCDData(_random_array.copy(), unit=u.adu)
        ccd.psf = psf
        assert (ccd.psf == psf).all()
    
        # cannot set with non-ndarray
>       with pytest.raises(TypeError, match="The psf must be a numpy array."):
E       Failed: DID NOT RAISE <class 'TypeError'>

.../astropy/nddata/tests/test_ccddata.py:1104: Failed

@mwcraig
Copy link
Copy Markdown
Member

mwcraig commented Oct 27, 2022

Interesting... Could it be that you actually support Quantity by accident?
🤔 maybe? Will check this evening but will be away from the keyboard for a few hours now

@mwcraig
Copy link
Copy Markdown
Member

mwcraig commented Oct 28, 2022

Turns out quantity is supported (though we don't get credit for that):

In [1]: from astropy import units as u

In [3]: import numpy as np

In [4]: foo = 5 * u.pixel

In [5]: foo
Out[5]: <Quantity 5. pix>

In [6]: isinstance(foo, np.ndarray)
Out[6]: True

Comment thread astropy/nddata/tests/test_ccddata.py Outdated
@pllim pllim merged commit d0a14f7 into astropy:main Oct 28, 2022
@pllim
Copy link
Copy Markdown
Member

pllim commented Oct 28, 2022

Thanks, all!

@parejkoj
Copy link
Copy Markdown
Contributor Author

Thank you for all the help in getting this merged!

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