Skip to content

DM-54397: Add task for computing a rough shapelet expansion of isolated stars.#467

Merged
TallJimbo merged 8 commits intomainfrom
tickets/DM-54397
Mar 31, 2026
Merged

DM-54397: Add task for computing a rough shapelet expansion of isolated stars.#467
TallJimbo merged 8 commits intomainfrom
tickets/DM-54397

Conversation

@TallJimbo
Copy link
Copy Markdown
Member

No description provided.

Copy link
Copy Markdown
Contributor

@laurenam laurenam left a comment

Choose a reason for hiding this comment

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

Just some flyby comments while perusing…

* `lsst.afw.geom.SpanSet`.
*
* This class provides low-level pixel-processing code for
* `ComputeRoughtPsfShapeletsTask`.
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.

Drop the “t” in Rought

/// The pixels actually used to compute the moments.
std::shared_ptr<afw::geom::SpanSet> spans;

/// Flag if there were too many bad pixels to compute the moments.
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.

“Flag if” —> “Flag set if” to match those below?

/// Flag set if there was a bad pixel too close to the center.
bool bad_pixel_in_center = false;

/// Flag set if the second moments do not resolve to an ellipse.
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.

Tense is changed from above, i.e. “do not” vs. “did not” & “was”.

/// Flag set if the second moments do not resolve to an ellipse.
bool singular_second_moments = false;

/// Test whether any failure falg is set.
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.

“falg” —> “flag”

*
* @param[in] spans Pixel region to use.
* @param[in] masked_image Image to measure.
* @param[in] bad_bitmask Mask of bad pixels to remove from `spans` to
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.

Extra “to”?

* before giving up.
* @param[in] bad_pixel_exlusion_radius Radius around the estimated
* center where bad pixels will
* cause the algorithm to give up.
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.

I don’t quite follow this description…

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 a radius that tries to act analogously to a cut on base_PixelFlags_[bad_bitmask]Center, but about the center the algorithm is computing itself. I've reworded slightly.

Copy link
Copy Markdown
Contributor

@laurenam laurenam left a comment

Choose a reason for hiding this comment

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

A few more fly-by comments...

bad_mask_planes = ListField(
"Mask planes to identify pixels to drop from the calculation.",
dtype=str,
default=["SAT", "SUSPECT"],
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.

Any reason not to include BAD and/or NO_DATA in the list? (I always forget what SUSPECT includes.)

)


class ComputePsfChiSqTask(Task):
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.

Docs?

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.

Oops, the commit that adds this task was not supposed to be in the PR, as I didn't think it was useful in the end. I deleted it, and then it accidentally came back in a rebase when I was transferring the code between USDF and my laptop. I'll go delete it again.

default=2.0,
)
max_footprint_area = Field(
"Footprints larger than this threshold are dropped before computing moments.",
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.

“Footprints whose area has a pixel count larger…”?

cls.def_readonly("flux", &SpanSetMoments::flux);
cls.def_readonly("variance", &SpanSetMoments::variance);
// def_readonly doesn't ever seem to do the right thing w.r.t.
// memory management, even when explicit return value policies.
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.

Sentence (after the comma) needs some rewording.

"""Exception raised when ComputeRawPsfMomentsTask fails to find any usable
stars.
"""

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.

Unnecessary blank line?

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.

No, I think there's always supposed to be a blank line after the docstring. It might be one of those things that I'm used to having ruff complain about or fix while flake8 doesn't care.

min_count,
name,
kind,
values[sorter[min_count - 1]],
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.

In adding ComputeRoughPsfShapeletsTask as subtask in calibrateImage over on DM-54482, I got this in running the tests:

File "/sdf/data/rubin/user/laurenma/LSST/meas_algorithms/python/lsst/meas/algorithms/computeRoughPsfShapelets.py", line 709, in _threshold_with_bounds
    values[sorter[min_count - 1]],
           ~~~~~~^^^^^^^^^^^^^^^
IndexError: index 9 is out of bounds for axis 0 with size 4

I think values[sorter[min_count - 1]] needs to be something like values[sorter[min(min_count, len(values)) - 1]]. Do we want to add a hard minimum for this task to pass?

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.

The min_count values were supposed to be that hard minimum, but as you noticed the logic was bad. Fixed in a new commit.

This means we'll raise an AlgorithmError in these cases, and that might mean CalibrateImageTask should catch that in a lower-level try block (rather than the big one in runQuantum) if it wants to proceed anyway when this new task can't do its job. Whether that's a good idea or not depends on whether this failure mode happens on good (but e.g. crowded or super-shallow-u) data.

Copy link
Copy Markdown
Member Author

@TallJimbo TallJimbo left a comment

Choose a reason for hiding this comment

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

I've just deleted a file that wasn't supposed to be here. Sorry!

)


class ComputePsfChiSqTask(Task):
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.

Oops, the commit that adds this task was not supposed to be in the PR, as I didn't think it was useful in the end. I deleted it, and then it accidentally came back in a rebase when I was transferring the code between USDF and my laptop. I'll go delete it again.

"""Exception raised when ComputeRawPsfMomentsTask fails to find any usable
stars.
"""

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.

No, I think there's always supposed to be a blank line after the docstring. It might be one of those things that I'm used to having ruff complain about or fix while flake8 doesn't care.

min_count,
name,
kind,
values[sorter[min_count - 1]],
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.

The min_count values were supposed to be that hard minimum, but as you noticed the logic was bad. Fixed in a new commit.

This means we'll raise an AlgorithmError in these cases, and that might mean CalibrateImageTask should catch that in a lower-level try block (rather than the big one in runQuantum) if it wants to proceed anyway when this new task can't do its job. Whether that's a good idea or not depends on whether this failure mode happens on good (but e.g. crowded or super-shallow-u) data.

Copy link
Copy Markdown
Member

@ctslater ctslater left a comment

Choose a reason for hiding this comment

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

Looks good, just a few questions on things I didn't understand.

peaks, _ = scipy.signal.find_peaks(scores)
if not peaks.size:
raise NoStarsForShapeletsError("Radius distribute has no mode.")
return sorted_radii[peaks.min()], kde
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 don't understand how peaks can be used to index into sorted_radii, since the scores were created from the original radii?

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.

Yup, that's a bug; good catch! scores should have been created from sorted_radii.

I think that says that this logic isn't super important, and that sort of makes sense, because this is about keeping galaxies and blends out of the star sample. I think at S/N=50 we usually mostly get stars without trying very hard, and the rest of the robust statistics can kind of take it from there.

ndarray::Array<float, 2, 2>(ndarray::allocate(n_coeff, n_pixels)).transpose();
design_matrix.deep() = 0.0;
std::size_t start = 0;
for (auto const& source_moments : moments) {
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.

It took me a long time to decode that this is a loop over sources. The naming is tricky here because the relevant values per-source are moments, and so a vector of those can fairly be called "moments", but that doesn't leave any room in the naming scheme for saying what distinguishes these elements from each other. I'm tempted to suggest the input parameter be named sources? I'm not sure it's much better but it's the best alternative I can think of.

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've renamed the argument to sources and the loop variable to source; adding "moments" to either doesn't add much information for the reader, even if it's correct.

default=False,
)
radius_mode_min = Field(
"Minimum radius at which to start searching for the first mode. "
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.

Units are pixels?

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.

👍, will update.

@TallJimbo TallJimbo merged commit 6bc04b0 into main Mar 31, 2026
4 checks passed
@TallJimbo TallJimbo deleted the tickets/DM-54397 branch March 31, 2026 15:41
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.

3 participants