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-16235: Jointcal PhotoCalib returns negative mean calibrations #111

Merged
merged 6 commits into from Oct 26, 2018

Conversation

parejkoj
Copy link
Collaborator

No description provided.

@parejkoj parejkoj force-pushed the tickets/DM-16235 branch 8 times, most recently from 252f484 to bd7dd61 Compare October 25, 2018 06:42
Copy link
Member

@kfindeisen kfindeisen left a 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; }
Copy link
Member

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.

Copy link
Member

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...

Copy link
Collaborator Author

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.

Copy link
Member

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. 🙄

MeasuredStar(double xx, double yy, double flux, double fluxErr, double instFlux, double instFluxErr)
: MeasuredStar(BaseStar(xx, yy, flux, fluxErr)) {
setInstFluxAndErr(instFlux, instFluxErr);
}
Copy link
Member

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.

Copy link
Collaborator Author

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;
Copy link
Member

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;
Copy link
Member

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.

Copy link
Collaborator Author

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()?

Copy link
Collaborator Author

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."""
Copy link
Member

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?

Copy link
Collaborator Author

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.

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) {
Copy link
Member

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.

Copy link
Collaborator Author

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.


bool PhotometryModel::checkPositiveOnBBox(CcdImage const &ccdImage) const {
bool check = true;
double result;
Copy link
Member

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
Copy link
Member

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).

Copy link
Collaborator Author

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).

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))
Copy link
Member

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?

Copy link
Collaborator Author

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).

Copy link
Member

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]
Copy link
Member

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)?

Copy link
Collaborator Author

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).

@parejkoj parejkoj force-pushed the tickets/DM-16235 branch 2 times, most recently from 8a14595 to 47d23d2 Compare October 26, 2018 16:04
_log = LOG_GET("jointcal.ConstrainedFluxModel");
: ConstrainedPhotometryModel(ccdImageList, focalPlaneBBox,
LOG_GET("jointcal.ConstrainedFluxModel"), visitOrder,
errorPedestal_) {
Copy link
Member

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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants