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-26545: Add spline linearizer. #159

Merged
merged 6 commits into from Oct 23, 2020
Merged

DM-26545: Add spline linearizer. #159

merged 6 commits into from Oct 23, 2020

Conversation

czwa
Copy link
Contributor

@czwa czwa commented Oct 8, 2020

This changeset adds the spline linearizer, and some additional metadata handling tools to try and enforce more uniform calibration headers.

Comment on lines +240 to +257
def search(haystack, needles):
"""Search dictionary 'haystack' for an entry in 'needles'
"""
test = [haystack.get(x) for x in needles]
test = set([x for x in test if x is not None])
if len(test) == 0:
if 'metadata' in haystack:
return search(haystack['metadata'], needles)
else:
return None
elif len(test) == 1:
value = list(test)[0]
if value == '':
return None
else:
return value
else:
raise ValueError(f"Too many values found: {len(test)} {test} {needles}")
Copy link
Contributor

Choose a reason for hiding this comment

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

I would likely have had comments here, but we agreed this OOB before hand, so that is fine for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am open to other more elegant solutions, but I don't know that that can exist until some future point where all of the metadata keywords are standardized completely.

self._detectorId = search(dictionary, ['DETECTOR', 'detectorId'])
self._detectorName = search(dictionary, ['DET_NAME', 'DETECTOR_NAME', 'detectorName'])
self._detectorSerial = search(dictionary, ['DET_SER', 'DETECTOR_SERIAL', 'detectorSerial'])
self._filter = search(dictionary, ['FILTER', 'filterName'])
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this the raw_md here, i.e. the untranslated header value? Or will this have been through astro_metadata_translator and obs_package translation at this point?

Copy link
Contributor

Choose a reason for hiding this comment

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

If the latter then that's fine, if the former then this will cause problems.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is an untranslated header value. However, it's the untranslated header value of the calibration product, which is derived either from a user specified value (updateMetadata(filterName='myFavoriteFilter')), or is set by pulling the value from the input exposure, which should have the translated version. My goal of including this was to allow validation (something we don't do yet) to confirm that the calibration matches the input data.

Comment on lines 275 to 276
if not self._detectorId and self._detectorSerial:
self._detectorId = self._detectorSerial
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 have no detId but you do have a serial, just make the detId equal to the serial? 🤨 I don't really understand how these can be interchangeable - is there scary downstream logic somewhere/everywhere for dealing with this, or am I misunderstanding?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm removing this, as I think all of the cases that were inconsistent with the detectorId have been purged from the code.

@@ -555,10 +630,8 @@ def fromTable(cls, tableList):
table = tableList[0]
metadata = table.meta
inDict = dict()

Copy link
Contributor

Choose a reason for hiding this comment

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

Some people care deeply about random whitespace additions, but I am not one of those people 🙂

Comment on lines +293 to +295
fitParams = record['FIT_PARAMS'] if 'FIT_PARAMS' in record.columns else np.array([0.0])
fitParamsErr = record['FIT_PARAMS_ERR'] if 'FIT_PARAMS_ERR' in record.columns else np.array([0.0])
fitChiSq = record['RED_CHI_SQ'] if 'RED_CHI_SQ' in record.columns else np.nan
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm sure there's a good reason for changing all these sentinel values here and above, so that's probably fine, but in a vacuum I certainly prefer None to zero and nan (but like I say, I'm sure there's a good reason, I've just not dug into it here).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

None does not serialize well with astropy tables (where it becomes astropy.io.fits.card.Undefined). I also wanted to keep datatypes consistent for the downstream code.

tableList.append(catalog)

if self.tableData:
catalog = Table([{'LOOKUP_VALUES': value} for value in self.tableData])
tableList.append(catalog)

return(catalog)
return(tableList)
Copy link
Contributor

Choose a reason for hiding this comment

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

Was the previous returning of catalog just a bug?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just a bug.

Comment on lines +703 to +710
image : `lsst.afw.image.Image`
Image to be corrected
Copy link
Contributor

Choose a reason for hiding this comment

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

This is obviously waaaay beyond this scope of this review, but does this really only ever run on the Image? Doesn't the variance plane need to be subjected to (some function of) this correction too? Or is that so n-th order as to be deep in the weeds?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's the way it has been written for a long time. However, it runs in ip_isr after the variance plane has been constructed, which makes me wonder if it should be being run prior to that step. I think in general this is a small enough term to ignore that issue. Since the nonlinearity is at the bright end, I think the astronomical effects of this would be making significant sources more significant, and possibly biasing surface brightness fits to galaxy cores. I'll think about it more, as it's certainly out of scope here.

Comment on lines +709 to +716
``"log"``
Logger to handle messages (`lsst.log.Log`).
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm actually not familiar with documenting kwargs - does this mean it's considered a mandatory kwarg? Either way, it's not made use of here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The lookup table linearizer has log output, and with the goal of "the linearizers should quack identically", I've taken that to mean that they should accept a log kwarg, even if it's unused.

Comment on lines 714 to 715
output : `bool`
If true, a correction was applied successfully.
Copy link
Contributor

Choose a reason for hiding this comment

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

You say you return a single bool, but actually return True, 0.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wonder how many reviews have missed that. Fixed the docstrings to match reality.

delta = interp.interpolate(ampArr.flatten())
ampArr -= np.array(delta).reshape(ampArr.shape)

return True, 0
Copy link
Contributor

Choose a reason for hiding this comment

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

The fact that you return something saying whether it was applied successfully implies that you might return something else, so I expected to see, from the docs/form of API, to maybe see a copy, a try, and a finally resetting to the copy on fail, and a return False. That seems expensive and probably unnecessary, but just saying what I'd expect to happen given that it says you get a return value if successful here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The numeric linearizers (spline/squared/polynomial) have no way to fail. The lookup table can fail, if all pixel values fall outside the lookup table range. This might be an issue with splines, although the low-end of the fit is padded to ensure a linear extrapolation in that direction.

@czwa czwa merged commit 9bbb7f7 into master Oct 23, 2020
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

2 participants