-
Notifications
You must be signed in to change notification settings - Fork 8
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-41955: Remove unphysical diaSources from the output of detectAndMeasure #287
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need some tests of this code. It won't do what you think, because of local argument scoping in the function.
I think this is ok here as a quick fix, but I still think we should consider removing these sources higher up (as an early measurement plugin, preferably), so that we're not doing any further math on them.
default=("base_PixelFlags_flag_offimage", | ||
"base_PixelFlags_flag_interpolatedCenterAll", | ||
"base_PixelFlags_flag_saturatedCenterAll", | ||
"base_PixelFlags_flag_crCenterAll", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about removing CR flagged sources; is our CR removal code good enough to leave a real source relatively ok? What about if the CR was in one of two snaps? I think we should be conservative on this one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed all of the CenterAll
flags for now pending further investigation to unblock this ticket.
nPurged = np.sum(~flags) | ||
if nPurged > 0: | ||
self.log.warning(f"Found and purged {nPurged} unphysical sources.") | ||
diaSources = diaSources[flags].copy(deep=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't do what you think it does: the function argument is locally scoped, so this doesn't modify it outside the function. You have to return it and assign in the caller.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! That fixes the remaining APDB collisions (without needing Eric and my validity_start change). I will update this, and write a unit test for the renamed purgeSources method.
ab0b980
to
31bc42c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion on how to rework the test to catch the bug you'd had before, and some questions about squashing exceptions and logging.
try: | ||
flags &= ~diaSources[flag] | ||
except Exception as e: | ||
self.log.warning("Could not apply source flag: %s", e) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like this try:catch; if you want to not raise on missing flags, you should only catch KeyError
. More broadly, I think a missing flag should just raise: if someone configures their pipeline to expect a certain flag, it should be an error if it doesn't appear in the schema, otherwise it's hiding unintended behavior, like a typo in the flags list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand your objection to a try:catch here, and can certainly change this to a KeyError
, but I would strongly prefer keeping it. My reasoning is that this should ideally not be finding and removing any bad sources, so I would rather it shout at people with a warning than have it break if the configured flag is missing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I very strongly disagree. An error that occurs on every single quantum (which is what will happen if the config doesn't match the schema) should be an actual exception raised up the chain, not a warning that will get lost. If it's an error, processing will stop early and we'll know to fix the misconfiguration. If it's a warning, we could process a whole data release without knowing that we meant to filter on something but didn't.
I'm not sure what you mean by "should ideally not be finding and removing any bad sources"? We know there will be "bad sources" because difference imaging will produce totally spurious detections due to interpolation. Unless we remove those sources much earlier (which is hard to do, given the design of the plugin system), we know they'll show up here and have to be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's actually a special exception type for configuration/consistency errors that can only be caught during after execution (whenever possible we want to catch them in config validation or task construction, of course): lsst.pipe.base.InvalidQuantumError
. I don't know the context well enough to say for certain whether it's the right call, but it seems potentially relevant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After a side discussion with @parejkoj , lsst.pipe.base.InvalidQuantumError
in the task __init__
looks like the right solution here.
tests/test_detectAndMeasure.py
Outdated
badDiaSrc1 = ~bbox.contains(diaSources.getX(), diaSources.getY()) | ||
nBad1 = np.count_nonzero(badDiaSrc1) | ||
self.assertEqual(nBad1, nSetBad) | ||
diaSources2 = detectionTask.removeBadSources(diaSources) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test wouldn't have caught the bug I described before (local scoping of the catalog), since it's not looking at the output of run
. What if instead you set all of difference.mask
to BAD
, add the badCenterAll
flag to the bad flags list, and then just call detectionTask.run
and confirm that the source is not included in the output? It's crude, but your other tests should be checking that "normal" sources behave correctly, and we don't really care why a given bad flag is set, just that it is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree. If this test were implemented as detectionTask.removeBadSources(diaSources)
then it would fail because the flagged sources are not removed from diaSources
in this scope, as you pointed out. Now that the API is for .removeBadSources
to return a source catalog, it seems appropriate to use that here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the goal is to test removeBadSources
by itself as a unittest, then you don't need any of the other infrastructure here. All you need is to create a trivial SourceCatalog with a couple flags (test_flag_1
, etc.), call removeBadSources
on that and check that the things you didn't want aren't there. There's no need to create an image or run detection or anything.
If the goal is to test that run
behaves correctly, then you need to actually test run
itself. This test as written would not catch run
not assigning the output of removeBadSources
to something that gets returned. If this is the goal of this test you don't need to run detectAndMeasure in a way that doesn't remove anything: you're already testing that with the other tests in this class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code comment here was out of date. With 20 sky sources in the test, there are some with unphysical coordinates which are removed in the first half of the test. I will fix the code comment, and I can also add an additional step that shows that some of the resulting diaSource
s are unphysical if badSourceFlags=[]
.
dfe3531
to
fa1262a
Compare
Downstream code will break in multiple places if there is a single diaSource with a NaN value from .getCentroid()
fa1262a
to
e60df45
Compare
@@ -376,6 +383,45 @@ def processResults(self, science, matchedTemplate, difference, sources, table, | |||
|
|||
return measurementResults | |||
|
|||
def removeBadSources(self, diaSources): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, we should probably make this _removeBadSources
: we're trying to make the "private" parts of Tasks more explicitly so.
# Use slot_Centroid_x/y here instead of getX() method, since the former | ||
# works on non-contiguous source tables and the latter does not. | ||
centroidFlag = np.isfinite(diaSources["slot_Centroid_x"]) & np.isfinite(diaSources["slot_Centroid_y"]) | ||
nBad = np.count_nonzero(~centroidFlag) | ||
if nBad > 0: | ||
self.log.info("Found and removed %d unphysical sources with non-finite centroid.", nBad) | ||
self.metadata.add("nRemovedBadCentroidSources", nBadTotal) | ||
nBadTotal += nBad | ||
selector &= centroidFlag |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should be able to replace this entire thing with adding slot_Centroid_flag
to the bad flags list. See DM-7102: that flag should be catching all cases where the centroid positions are non-finite.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want to explicitly catch diaSources["slot_Centroid_x"]
(and y
) here since that breaks downstream code. As @parejkoj and I discussed, this check should be unnecessary after DM-42313 merges since these nan centroids would then be picked up by the flag base_PixelFlags_flag_offimage
.
Thinking about this more, why can't we just use a ScienceSourceSelector, like Calibrate does, with |
|
With DM-42313 merged, these should be caught by `base_PixelFlags_flag_offimage`
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the cleanups!
No description provided.