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-16235: Jointcal PhotoCalib returns negative mean calibrations #111
Conversation
252f484
to
bd7dd61
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.
I feel like you've sacrificed code quality (encapsulation, clear purpose for methods, etc.) in trying to include the negativity checks as soon as possible. Most of my individual suggestions should be quick fixes.
@@ -61,6 +62,9 @@ class AstrometryModel { | |||
|
|||
virtual ~AstrometryModel(){}; | |||
|
|||
/// Return true if this is a "reasonable" model. | |||
bool validate(CcdImageList const &ccdImageList) const { return 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.
Kind of a vague spec. What counts as "reasonable"? Physical plausibility? No degenerate terms?
Also, please document ccdImageList
; from the description it sounds like validate
should not need any parameters.
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 assume validate
has a default implementation so that overriding methods can call it? If so, it might be worth documenting as a must/should...
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, I've clarified the docstrings.
Sadly, PhotometryModel
and AstrometryModel
don't actually share a parent class, though they probably should (so that this API can be defined up there). AstrometryModel.validate()
exists to fulfill that API, but I need to think through what it should be in practice. I've filed a ticket for the implementation: it would almost certainly be implemented in AstrometryModel
itself, not in a child 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.
Oh wait, validate
can't be overridden. I've been working in Python too long. 🙄
include/lsst/jointcal/MeasuredStar.h
Outdated
MeasuredStar(double xx, double yy, double flux, double fluxErr, double instFlux, double instFluxErr) | ||
: MeasuredStar(BaseStar(xx, yy, flux, fluxErr)) { | ||
setInstFluxAndErr(instFlux, instFluxErr); | ||
} |
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 really don't like this constructor; its six consecutive double
parameters seem more than a little error-prone.
Why not call MeasuredStar(BaseStar, double, double)
? It's less ambiguous (though four parameters is pushing it even when they are of different types), assuming there's a natural reason why instFlux
does not belong in a BaseStar
.
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've deleted this constructor: turns out I don't really need it.
BaseStar is any kind of star (Measured, Fitted, Reference), and only the former has instFlux
(since it's from a SourceCatalog measurement).
@@ -134,6 +136,17 @@ class PhotometryModel { | |||
/// Dump the contents of the transfos, for debugging. | |||
virtual void dump(std::ostream &stream = std::cout) const = 0; | |||
|
|||
/// Return true if this is a "reasonable" model. | |||
bool validate(CcdImageList const &ccdImageList) const; |
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.
Same questions as for AstrometryModel::validate
.
@@ -165,6 +178,9 @@ class PhotometryModel { | |||
|
|||
// Pedestal on flux/magnitude error (percent of flux or delta magnitude) | |||
double errorPedestal; | |||
|
|||
/// lsst.logging instance, to be created by a subclass so that messages have consistent name. | |||
LOG_LOGGER _log; |
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.
Kind of a brittle implementation, because a subclass author needs to remember to initialize this in all constructors. Why not have an abstract getLogger()
method, and let subclasses decide how to implement it (a logger field, a static logger inside the method, etc.)?
Or, if you want a shared public field, why not make the logger a mandatory parameter for the PhotometryModel
constructor? That would also guarantee it's correctly initialized.
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 is the only comment that I'm not entirely sure what to do with. I agree with your point about brittleness, but the other versions are either more work for the subclasses (e.g. abstract getLogger()
, or more work for the caller (construction parameter). Unless I'm missing something about how to implement getLogger()
?
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.
Ok, think I came up with a better solution using the constructor. Do you mind taking a quick look at it to see if it's what you meant? Should be the last commit.
@@ -585,6 +585,17 @@ def _check_star_lists(self, associations, name): | |||
if associations.refStarListSize() == 0: | |||
raise RuntimeError('No stars in the {} reference star list!'.format(name)) | |||
|
|||
def _logChi2AndValidate(self, associations, fit, model, chi2Label="Model"): | |||
"""Compute chi2, log it, validate the model, and return chi2.""" |
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.
So, this is a pretty blatant violation of the "do one thing" rule. I can understand moving the logger and finiteness tests in here (though logging is not normally considered part of a function/program's spec, it just happens), but why can't you have a separate call for validating the model?
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 amount of boilerplate around each call to fit.minimize()
during initialization was getting ridiculous and hard to keep uniform, so I flattened it all into one place. There are a number of different calls to minimize()
and they each need to be logged, checked, and validated.
I've added a note to DM-8046, which is a perfect place to make this all better.
src/PhotometryModel.cc
Outdated
std::vector<double> xRange = {bbox.xMin, bbox.getCenter().x, bbox.xMax}; | ||
std::vector<double> yRange = {bbox.yMin, bbox.getCenter().y, bbox.yMax}; | ||
for (auto const &x : xRange) | ||
for (auto const &y : yRange) { |
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.
Why not put the initializer lists in the loop definition (i.e., get rid of xRange
and yRange
entirely)? I think it would improve readability.
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.
Because I didn't even think of it. Good idea.
src/PhotometryModel.cc
Outdated
|
||
bool PhotometryModel::checkPositiveOnBBox(CcdImage const &ccdImage) const { | ||
bool check = true; | ||
double result; |
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.
Please don't define result
until it's used.
@@ -39,20 +41,25 @@ def setUp(self): | |||
self.badChi2.chi2 = 600.0 | |||
self.badChi2.ndof = 100 | |||
|
|||
self.nanChi2 = lsst.jointcal.chi2.Chi2Statistic() | |||
self.nanChi2.chi2 = np.nan | |||
self.nanChi2.ndof = 100 |
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 surprised you can't just initialize Chi2Statistic
to the value you want (especially since degrees of freedom seems like something that shouldn't change).
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.
Their primary usage is to be incremented during fitting, so they initialize to zero. I'm just making a fake one here (could Mock it, but I don't think it's worth it in this case).
tests/test_jointcal.py
Outdated
self.maxSteps, self.name, self.whatToFit) | ||
self.assertEqual(chi2, self.badChi2) | ||
log.info.assert_called_with("Fit completed with: %s", str(self.badChi2)) | ||
log.info.assert_called_with("%s %s", "Fit completed", str(self.badChi2)) |
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.
Given that logging is something that happens on the side, should you really be testing for it?
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.
Do we really assert that "logging happens on the side" in the stack? It's the only way to output some information (like whether something completed successfully, in this case).
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 assumption behind logging (and frameworks like log4* specifically) is that the program is running with little to no human oversight, possibly continuously, and that logging statements are only there to provide information that might be useful for reviewing things after the fact (especially if something went wrong). In other words, it should be possible to remove all logging statements and the program will still be correct.
That said, I don't know of any Stack programs that are supposed to produce text output -- we're very much operating in the non-interactive, machine-readable regime.
@@ -170,7 +170,7 @@ def setUp(self): | |||
self.visitOrder = 3 | |||
self.focalPlaneBBox = self.camera.getFpBBox() | |||
# tweak to get more than just a constant field for the second ccdImage | |||
self.delta = np.arange(20, dtype=float)*-0.2 + 1 | |||
self.delta = (np.arange(20, dtype=float)*-0.2 + 1)[::-1] |
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.
Why are you reversing the order of delta
(if that's what this syntax 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.
Added a comment: it makes the lowest order terms largest (which is what a realistic model would have).
8a14595
to
47d23d2
Compare
_log = LOG_GET("jointcal.ConstrainedFluxModel"); | ||
: ConstrainedPhotometryModel(ccdImageList, focalPlaneBBox, | ||
LOG_GET("jointcal.ConstrainedFluxModel"), visitOrder, | ||
errorPedestal_) { |
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.
Yes, this is exactly what I had in mind... though I underestimated the number of classes whose constructor signatures would need to be changed. 😞
Cleanup ConstrainedPhotometryModel docstring
Previously some of the test PhotometryModels were negative on all of their bbox, because I hadn't thought through what a "correct" PhotoCalibMean should be (answer: less than one!). Adding this method showed that error, which I've fixed.
Validate models after each fit step. AstrometryModel doesn't have any "validation" conditions yet, but we can probably think of some later. Cleanup the steps that happen after `fitter.minimize()`, so that we're always logging chi2 and testing the same things. Add getModel() for AstrometryFit/PhotometryFit (all the more reason to want a common base class).
74accd8
to
983d7e4
Compare
No description provided.