-
Notifications
You must be signed in to change notification settings - Fork 7
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
Changes from all commits
a595583
38f87da
af5ab64
7e52060
5495fbc
983d7e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,12 +30,14 @@ class ConstrainedPhotometryModel : public PhotometryModel { | |
* @param ccdImageList The list of CCDImages to construct the model for. | ||
* @param focalPlaneBBox The bounding box of the camera's focal plane, defining the domain of the | ||
* visit polynomial. | ||
* @param[in] log An lsst::log::Log instance to log messages to. | ||
* @param[in] visitOrder The order of the visit polynomial. | ||
* @param[in] errorPedestal_ A pedestal in flux or magnitude to apply to all MeasuredStar flux errors. | ||
*/ | ||
explicit ConstrainedPhotometryModel(CcdImageList const &ccdImageList, | ||
afw::geom::Box2D const &focalPlaneBBox, int visitOrder = 7, | ||
double errorPedestal_ = 0) | ||
: PhotometryModel(errorPedestal_), _fittingChips(false), _fittingVisits(false) { | ||
afw::geom::Box2D const &focalPlaneBBox, LOG_LOGGER log, | ||
int visitOrder = 7, double errorPedestal_ = 0) | ||
: PhotometryModel(log, errorPedestal_), _fittingChips(false), _fittingVisits(false) { | ||
_chipVisitMap.reserve(ccdImageList.size()); | ||
} | ||
|
||
|
@@ -103,7 +105,9 @@ class ConstrainedFluxModel : public ConstrainedPhotometryModel { | |
public: | ||
explicit ConstrainedFluxModel(CcdImageList const &ccdImageList, afw::geom::Box2D const &focalPlaneBBox, | ||
int visitOrder = 7, double errorPedestal_ = 0) | ||
: ConstrainedPhotometryModel(ccdImageList, focalPlaneBBox, visitOrder, errorPedestal_) { | ||
: ConstrainedPhotometryModel(ccdImageList, focalPlaneBBox, | ||
LOG_GET("jointcal.ConstrainedFluxModel"), visitOrder, | ||
errorPedestal_) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. 😞 |
||
initialize<FluxTransfoSpatiallyInvariant, FluxTransfoChebyshev, ChipVisitFluxMapping>( | ||
ccdImageList, focalPlaneBBox, visitOrder); | ||
} | ||
|
@@ -145,7 +149,9 @@ class ConstrainedMagnitudeModel : public ConstrainedPhotometryModel { | |
explicit ConstrainedMagnitudeModel(CcdImageList const &ccdImageList, | ||
afw::geom::Box2D const &focalPlaneBBox, int visitOrder = 7, | ||
double errorPedestal_ = 0) | ||
: ConstrainedPhotometryModel(ccdImageList, focalPlaneBBox, visitOrder, errorPedestal_) { | ||
: ConstrainedPhotometryModel(ccdImageList, focalPlaneBBox, | ||
LOG_GET("jointcal.ConstrainedMagnitudeModel"), visitOrder, | ||
errorPedestal_) { | ||
initialize<MagnitudeTransfoSpatiallyInvariant, MagnitudeTransfoChebyshev, ChipVisitMagnitudeMapping>( | ||
ccdImageList, focalPlaneBBox, visitOrder); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,23 +2,25 @@ | |
#ifndef LSST_JOINTCAL_PHOTOMETRY_MODEL_H | ||
#define LSST_JOINTCAL_PHOTOMETRY_MODEL_H | ||
|
||
#include <string> | ||
#include <vector> | ||
|
||
#include "lsst/afw/image/PhotoCalib.h" | ||
#include "lsst/log/Log.h" | ||
|
||
#include "lsst/jointcal/CcdImage.h" | ||
#include "lsst/jointcal/Eigenstuff.h" | ||
#include "lsst/jointcal/PhotometryMapping.h" | ||
#include "lsst/jointcal/FittedStar.h" | ||
#include "lsst/jointcal/MeasuredStar.h" | ||
#include "lsst/jointcal/RefStar.h" | ||
#include <string> | ||
#include <vector> | ||
|
||
namespace lsst { | ||
namespace jointcal { | ||
|
||
class PhotometryModel { | ||
public: | ||
PhotometryModel(double errorPedestal_ = 0) : errorPedestal(errorPedestal_) {} | ||
PhotometryModel(LOG_LOGGER log, double errorPedestal_ = 0) : _log(log), errorPedestal(errorPedestal_) {} | ||
|
||
/** | ||
* Assign indices in the full matrix to the parameters being fit in the mappings, starting at firstIndex. | ||
|
@@ -134,6 +136,24 @@ 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. | ||
* | ||
* A valid photometry model is positive within each sensor's bounding box. | ||
* | ||
* @param ccdImageList The ccdImages to test the model validity on. | ||
* @return True if the model is valid on all ccdImages. | ||
*/ | ||
bool validate(CcdImageList const &ccdImageList) const; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same questions as for |
||
|
||
/** | ||
* Check that the model is positive on the ccdImage bbox. | ||
* | ||
* @param ccdImage The ccdImage to test. | ||
* @return True if the image is positive on a sampling of points of the ccdImage bbox. | ||
*/ | ||
bool checkPositiveOnBBox(CcdImage const &ccdImage) const; | ||
|
||
friend std::ostream &operator<<(std::ostream &s, PhotometryModel const &model) { | ||
model.dump(s); | ||
return s; | ||
|
@@ -163,6 +183,9 @@ class PhotometryModel { | |
/// Return a pointer to the mapping associated with this ccdImage. | ||
virtual PhotometryMappingBase *findMapping(CcdImage const &ccdImage) const = 0; | ||
|
||
/// 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 commentThe 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 Or, if you want a shared public field, why not make the logger a mandatory parameter for the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
||
// Pedestal on flux/magnitude error (percent of flux or delta magnitude) | ||
double errorPedestal; | ||
}; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. The amount of boilerplate around each call to I've added a note to DM-8046, which is a perfect place to make this all better. |
||
chi2 = fit.computeChi2() | ||
self.log.info("%s %s", chi2Label, chi2) | ||
self._check_stars(associations) | ||
if not np.isfinite(chi2.chi2): | ||
raise FloatingPointError('%s chi2 is invalid: %s', chi2Label, chi2) | ||
if not model.validate(associations.getCcdImageList()): | ||
raise ValueError("Model is not valid: check log messages for warnings.") | ||
return chi2 | ||
|
||
def _fit_photometry(self, associations, dataName=None): | ||
""" | ||
Fit the photometric data. | ||
|
@@ -632,37 +643,32 @@ def _fit_photometry(self, associations, dataName=None): | |
doLineSearch = False # purely linear in model parameters, so no line search needed | ||
|
||
fit = lsst.jointcal.PhotometryFit(associations, model) | ||
chi2 = fit.computeChi2() | ||
self._logChi2AndValidate(associations, fit, model, "Initialized") | ||
|
||
# TODO DM-12446: turn this into a "butler save" somehow. | ||
# Save reference and measurement chi2 contributions for this data | ||
if self.config.writeChi2ContributionFiles: | ||
baseName = "photometry_initial_chi2-{}.csv".format(dataName) | ||
fit.saveChi2Contributions(baseName) | ||
|
||
if not np.isfinite(chi2.chi2): | ||
raise FloatingPointError('Initial chi2 is invalid: %s'%chi2) | ||
self.log.info("Initialized: %s", str(chi2)) | ||
# The constrained model needs the visit transfo fit first; the chip | ||
# transfo is initialized from the singleFrame PhotoCalib, so it's close. | ||
dumpMatrixFile = "photometry_preinit" if self.config.writeInitMatrix else "" | ||
if self.config.photometryModel.startswith("constrained"): | ||
# no line search: should be purely (or nearly) linear, | ||
# and we want a large step size to initialize with. | ||
fit.minimize("ModelVisit", dumpMatrixFile=dumpMatrixFile) | ||
chi2 = fit.computeChi2() | ||
self.log.info(str(chi2)) | ||
self._logChi2AndValidate(associations, fit, model) | ||
dumpMatrixFile = "" # so we don't redo the output on the next step | ||
|
||
fit.minimize("Model", doLineSearch=doLineSearch, dumpMatrixFile=dumpMatrixFile) | ||
chi2 = fit.computeChi2() | ||
self.log.info(str(chi2)) | ||
self._logChi2AndValidate(associations, fit, model) | ||
|
||
fit.minimize("Fluxes") # no line search: always purely linear. | ||
chi2 = fit.computeChi2() | ||
self.log.info(str(chi2)) | ||
self._logChi2AndValidate(associations, fit, model) | ||
|
||
fit.minimize("Model Fluxes", doLineSearch=doLineSearch) | ||
chi2 = fit.computeChi2() | ||
if not np.isfinite(chi2.chi2): | ||
raise FloatingPointError('Pre-iteration chi2 is invalid: %s'%chi2) | ||
self.log.info("Fit prepared with %s", str(chi2)) | ||
self._logChi2AndValidate(associations, fit, model, "Fit prepared") | ||
|
||
model.freezeErrorTransform() | ||
self.log.debug("Photometry error scales are frozen.") | ||
|
@@ -725,36 +731,30 @@ def _fit_astrometry(self, associations, dataName=None): | |
order=self.config.astrometrySimpleOrder) | ||
|
||
fit = lsst.jointcal.AstrometryFit(associations, model, self.config.positionErrorPedestal) | ||
chi2 = fit.computeChi2() | ||
self._logChi2AndValidate(associations, fit, model, "Initial") | ||
|
||
# TODO DM-12446: turn this into a "butler save" somehow. | ||
# Save reference and measurement chi2 contributions for this data | ||
if self.config.writeChi2ContributionFiles: | ||
baseName = "astrometry_initial_chi2-{}.csv".format(dataName) | ||
fit.saveChi2Contributions(baseName) | ||
|
||
if not np.isfinite(chi2.chi2): | ||
raise FloatingPointError('Initial chi2 is invalid: %s'%chi2) | ||
self.log.info("Initialized: %s", str(chi2)) | ||
dumpMatrixFile = "astrometry_preinit" if self.config.writeInitMatrix else "" | ||
# The constrained model needs the visit transfo fit first; the chip | ||
# transfo is initialized from the detector's cameraGeom, so it's close. | ||
if self.config.astrometryModel == "constrained": | ||
fit.minimize("DistortionsVisit", dumpMatrixFile=dumpMatrixFile) | ||
chi2 = fit.computeChi2() | ||
self.log.info(str(chi2)) | ||
self._logChi2AndValidate(associations, fit, model) | ||
dumpMatrixFile = "" # so we don't redo the output on the next step | ||
|
||
fit.minimize("Distortions", dumpMatrixFile=dumpMatrixFile) | ||
chi2 = fit.computeChi2() | ||
self.log.info(str(chi2)) | ||
self._logChi2AndValidate(associations, fit, model) | ||
|
||
fit.minimize("Positions") | ||
chi2 = fit.computeChi2() | ||
self.log.info(str(chi2)) | ||
self._logChi2AndValidate(associations, fit, model) | ||
|
||
fit.minimize("Distortions Positions") | ||
chi2 = fit.computeChi2() | ||
self.log.info(str(chi2)) | ||
if not np.isfinite(chi2.chi2): | ||
raise FloatingPointError('Pre-iteration chi2 is invalid: %s'%chi2) | ||
self.log.info("Fit prepared with %s", str(chi2)) | ||
self._logChi2AndValidate(associations, fit, model, "Fit prepared") | ||
|
||
chi2 = self._iterate_fit(associations, | ||
fit, | ||
|
@@ -831,17 +831,15 @@ def _iterate_fit(self, associations, fitter, max_steps, name, whatToFit, | |
doLineSearch=doLineSearch, | ||
dumpMatrixFile=dumpMatrixFile) | ||
dumpMatrixFile = "" # clear it so we don't write the matrix again. | ||
chi2 = fitter.computeChi2() | ||
self._check_stars(associations) | ||
self.log.info(str(chi2)) | ||
chi2 = self._logChi2AndValidate(associations, fitter, fitter.getModel()) | ||
|
||
if result == MinimizeResult.Converged: | ||
if doRankUpdate: | ||
self.log.debug("fit has converged - no more outliers - redo minimization " | ||
"one more time in case we have lost accuracy in rank update.") | ||
# Redo minimization one more time in case we have lost accuracy in rank update | ||
result = fitter.minimize(whatToFit, self.config.outlierRejectSigma) | ||
chi2 = fitter.computeChi2() | ||
self.log.info("Fit completed with: %s", str(chi2)) | ||
chi2 = self._logChi2AndValidate(associations, fitter, fitter.getModel(), "Fit completed") | ||
|
||
# log a message for a large final chi2, TODO: DM-15247 for something better | ||
if chi2.chi2/chi2.ndof >= 4.0: | ||
|
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 likevalidate
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
andAstrometryModel
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 inAstrometryModel
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. 🙄