Skip to content
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

DM-41648: Write a Plugin for computing higher-order moments #44

Merged
merged 14 commits into from Dec 21, 2023

Conversation

arunkannawadi
Copy link
Member

Setting up a base branch (+PR) to receive contributions from Tianqing Zhang

COPYRIGHT Outdated
Copyright 2017-2018 University of Washington
Copyright 2015, 2017 The Regents of the University of California
Copyright 2016 University of Illinois Board of Trustees
Copyright 2011-2014 LSST Corporation
Copy link
Member Author

Choose a reason for hiding this comment

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

Pinging @timj to weigh in on the COPYRIGHT file.

Copy link
Member

Choose a reason for hiding this comment

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

LSST Corp copyrights were handed to AURA so these should be combined with AURA entry.

@arunkannawadi arunkannawadi force-pushed the tickets/DM-41648 branch 2 times, most recently from d319a23 to bc63b2a Compare December 14, 2023 18:54
@arunkannawadi arunkannawadi marked this pull request as ready for review December 14, 2023 18:54
Copy link
Contributor

@jmeyers314 jmeyers314 left a comment

Choose a reason for hiding this comment

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

Not done yet, but wanted you to see comments so far since I was also starring at _calculate_higher_order_moments...

@@ -45,7 +45,7 @@ class HsmShapeConfig(measBase.SingleFramePluginConfig):

shearType = pexConfig.ChoiceField[str](
doc="The desired method of PSF correction using GalSim. The first three options return an e-type "
"distortion, whereas the last option returns a g-type shear.",
"distortion, whereas the last option returns a g-type shear.",
Copy link
Contributor

Choose a reason for hiding this comment

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

I know this is just a blackening, but personally, I find the following (black-acceptable) format nicer to read:

shearType = pexConfig.ChoiceField[str](
    doc=(
        "The desired method of PSF correction using GalSim. The first three options return an e-type "
        "distortion, whereas the last option returns a g-type shear."
    ),
)

Your call though.

Parameters
----------
image : `~lsst.afw.image.Image`
Image of the PSF
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe just Image of source?

the object. For any well-sampled image, the zeroth order moment is 1,
the first order moments are 0, and the second order moments are 0.5 for xx
and yy and 0 for xy. For a symmetric profile, the odd order moments are
zeros.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this implies that the normalized x coordinate lies either on the unnormalized major or minor axis. Can you clarify which (assuming I got that first part right)?

Copy link
Member Author

Choose a reason for hiding this comment

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

It does, and I don't think it matters whether it lies along the major or minor axis due to the normalization.

Copy link
Contributor

Choose a reason for hiding this comment

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

If you swapped x from major -> minor, doesn't that mean that you also swap Mpq to Mqp ? That's what I'm trying to clarify, that the moments are in some sense M_major_minor. Or am I missing something?

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh hmm, good point. I think x is along the major axis, but this makes me question if I should think of these numbers are coordinate-independent or not. I had naively assumed it was. If we have to transform them from pixel-coordinates to sky-coordinates, we need to remember to transform these accordingly then.

if bbox.getArea() == 0:
raise measBase.MeasurementError(self.NO_PIXELS.doc, self.NO_PIXELS.number)

# # Ensure that the centroid is within the bounding box.
Copy link
Contributor

Choose a reason for hiding this comment

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

If you're going to leave commented-out code, then I think you need to attach an explanation as to why it's commented out.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, these should be removed actually.

image_weight = weight * image_array
normalization = np.sum(image_weight)

results = {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Just saw the use_linear_algebra bit appear, so seems you're staring at this too :).

Aside from the 2x2 specialization (which I think should be the only code path BTW), I got a tad bit more performance by caching the power arrays. Something like:

    std_x_pow = {}
    std_y_pow = {}

    pmin = min(p for p, q in pqlist)
    qmin = min(q for p, q in pqlist)
    if pmin == 0:
        std_x_pow[0] = 1.0
    elif pmin == 1:
        std_x_pow[1] = std_x
    else:
        std_x_pow[pmin] = std_x**pmin

    if qmin == 0:
        std_y_pow[0] = 1.0
    elif qmin == 1:
        std_y_pow[1] = std_y
    else:
        std_y_pow[qmin] = std_y**qmin

    for p in range(pmin, max(p for p, q in pqlist)+1):
        std_x_pow[p+1] = std_x_pow[p] * std_x
    for q in range(qmin, max(q for p, q in pqlist)+1):
        std_y_pow[q+1] = std_y_pow[q] * std_y

    results = {}
    for p, q in pqlist:
        tmp = std_x_pow[p] * std_y_pow[q]
        tmp *= image_weight
        results[(p, q)] = np.sum(tmp) / normalization

Copy link
Member Author

Choose a reason for hiding this comment

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

Good idea, and I hope this does well even on objects with large footprints.

pmin and qmin are always going to be 0 given how we generate those tuples. So I am going to remove the if-elif-else conditions.

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, it occurs to me that np.vander may be your friend here (but I didn't try 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.

I think I'd like to have the option of having the more expensive code path, given that that was what TQ originally used in the previous HSC data, so I consider that validated. I'd like to have an option to resort to the validated code if something doesn't look right. I could add another unit test to check that both code paths result in consistent answers.

Copy link
Member Author

Choose a reason for hiding this comment

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

It looks like np.vander is only for 1-d arrays. So I went with your code suggestion, minus the condition checking.

@arunkannawadi
Copy link
Member Author

Summary of changes since you last reviewed:

  • We accumulate the powers of standard coordinates instead of computing on the fly
  • Instead of making two copies of arrays to make badpix, there is only one copying now.
  • Added a test to check the validate method of the configs.
  • Added a test that checks that the codepaths with either values for use_linear_algebra produces the same result, even with an asymmetric M matrix.
  • Fixed other documentation related comments.

Copy link
Contributor

@erykoff erykoff left a comment

Choose a reason for hiding this comment

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

I think I found the bug

# Measure all the moments together to save time
try:
hm_measurement = self._calculate_higher_order_moments(
exposure.image[bbox],
Copy link
Contributor

Choose a reason for hiding this comment

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

This needs to be a copy of the sub-image.

Copy link
Contributor

Choose a reason for hiding this comment

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

Might be clearer if the copy is inside _calculate_higher_order_moments, but yeah, good catch.

Copy link
Member Author

Choose a reason for hiding this comment

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

Copying inside _calculate_higher_order_moments would make unnecessary copies for psfImage. I fixed it to make a deep copy on a need-to basis.

Copy link
Member Author

Choose a reason for hiding this comment

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

Alternatively, I could make the modification to the weight image and not have to make a deep copy ever.

Copy link
Contributor

Choose a reason for hiding this comment

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

Right. I just meant we don't want anyone to have to remember that _calculate_higher_order_moments modifies the image. Modifying the weight is a great solution.

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 commit I just pushed looks to me like the cleanest solution - multiply the weight and image with no regards for the badpix and mask this product as configured. It avoids making expensive deep copies, and avoids any division of pixel values (as I currently have) which may become unstable if the pixel values are close to 0.

Copy link
Contributor

@erykoff erykoff left a comment

Choose a reason for hiding this comment

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

Looks much better!

yield f"{p}{q}"

def _generate_powers_of_standard_positions(self, std_x, std_y):
std_x_powers, std_y_powers = {}, {}
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess these could be lists now. Probably doesn't matter in practice...

Copy link
Contributor

@jmeyers314 jmeyers314 left a comment

Choose a reason for hiding this comment

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

LGTM. Looking forward to seeing some higher order moments analysis in the near future!

@arunkannawadi arunkannawadi merged commit b3ef3c6 into main Dec 21, 2023
3 checks passed
@arunkannawadi arunkannawadi deleted the tickets/DM-41648 branch December 21, 2023 04:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants