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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions include/lsst/jointcal/AstrometryFit.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ class AstrometryFit : public FitterBase {
/// @copydoc FitterBase::saveChi2RefContributions
void saveChi2RefContributions(std::string const &baseName) const override;

/// Return the model being fit.
std::shared_ptr<AstrometryModel> getModel() const { return _astrometryModel; }

/**
* DEBUGGING routine
*/
Expand Down
12 changes: 12 additions & 0 deletions include/lsst/jointcal/AstrometryModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include "memory"

#include "lsst/jointcal/CcdImage.h"
#include "lsst/jointcal/Gtransfo.h"
#include "lsst/jointcal/Eigenstuff.h"
#include "lsst/jointcal/AstrometryMapping.h"
Expand Down Expand Up @@ -61,6 +62,17 @@ class AstrometryModel {

virtual ~AstrometryModel(){};

/**
* Return true if this is a "reasonable" model.
*
* Not yet implemented: DM-16324
* An example might be that the model produces finite RA/Dec on 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 { 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. 🙄


protected:
/// Return a pointer to the mapping associated with this ccdImage.
virtual AstrometryMapping *findMapping(CcdImage const &ccdImage) const = 0;
Expand Down
16 changes: 11 additions & 5 deletions include/lsst/jointcal/ConstrainedPhotometryModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand Down Expand Up @@ -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_) {
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. 😞

initialize<FluxTransfoSpatiallyInvariant, FluxTransfoChebyshev, ChipVisitFluxMapping>(
ccdImageList, focalPlaneBBox, visitOrder);
}
Expand Down Expand Up @@ -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);
}
Expand Down
4 changes: 2 additions & 2 deletions include/lsst/jointcal/MeasuredStar.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ class MeasuredStar : public BaseStar {
_instMag(0.),
_instMagErr(0.) {}

/// No move, allow copy constructor: we may copy the fitted StarLists when associating and matching
/// catalogs, otherwise Stars should be managed by shared_ptr only.
/// No move, allow copy constructor: we may copy the fitted StarLists when associating and
/// matching catalogs, otherwise Stars should be managed by shared_ptr only.
MeasuredStar(MeasuredStar const &) = default;
MeasuredStar(MeasuredStar &&) = delete;
MeasuredStar &operator=(MeasuredStar const &) = delete;
Expand Down
5 changes: 3 additions & 2 deletions include/lsst/jointcal/PhotometryFit.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ class PhotometryFit : public FitterBase {
*
* @param associations The associations catalog to use in the fitter.
* @param photometryModel The model to build the fitter for.
* @param fluxError The systematic error pedestal to apply to measured instFlux errors
* (as a percent of instFlux).
*/
PhotometryFit(std::shared_ptr<Associations> associations,
std::shared_ptr<PhotometryModel> photometryModel)
Expand Down Expand Up @@ -68,6 +66,9 @@ class PhotometryFit : public FitterBase {
/// @copydoc FitterBase::saveChi2RefContributions
void saveChi2RefContributions(std::string const &baseName) const override;

/// Return the model being fit.
std::shared_ptr<PhotometryModel> getModel() const { return _photometryModel; }

private:
bool _fittingModel, _fittingFluxes;
std::shared_ptr<PhotometryModel> _photometryModel;
Expand Down
29 changes: 26 additions & 3 deletions include/lsst/jointcal/PhotometryModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
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.


/**
* 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;
Expand Down Expand Up @@ -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;
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.


// Pedestal on flux/magnitude error (percent of flux or delta magnitude)
double errorPedestal;
};
Expand Down
6 changes: 5 additions & 1 deletion include/lsst/jointcal/PhotometryTransfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,8 @@ class PhotometryTransfoChebyshev : public PhotometryTransfo {
/**
* Create a Chebyshev transfo with the specified coefficients.
*
* The polynomial order is determined from the number of coefficients.
* The polynomial order is determined from the number of coefficients, taking only the
* anti-diagonal upper triangle portion of the passed-in coefficients
*
* @param coefficients The polynomial coefficients.
* @param[in] bbox The bounding box it is valid within, to rescale it to [-1,1].
Expand Down Expand Up @@ -291,6 +292,9 @@ class FluxTransfoChebyshev : public PhotometryTransfoChebyshev {
}
};

/**
* nth-order 2d Chebyshev photometry transfo, plus the input flux.
*/
class MagnitudeTransfoChebyshev : public PhotometryTransfoChebyshev {
public:
MagnitudeTransfoChebyshev(size_t order, afw::geom::Box2D const &bbox)
Expand Down
4 changes: 2 additions & 2 deletions include/lsst/jointcal/SimplePhotometryModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ class Point;
//! Photometric response model which has a single photometric factor per CcdImage.
class SimplePhotometryModel : public PhotometryModel {
public:
SimplePhotometryModel(CcdImageList const &ccdImageList, double errorPedestal_ = 0)
: PhotometryModel(errorPedestal_) {
SimplePhotometryModel(CcdImageList const &ccdImageList, LOG_LOGGER log, double errorPedestal_ = 0)
: PhotometryModel(log, errorPedestal_) {
_myMap.reserve(ccdImageList.size());
}

Expand Down
1 change: 1 addition & 0 deletions python/lsst/jointcal/astrometryModels.cc
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ void declareAstrometryModel(py::module &mod) {
cls.def("getSky2TP", &AstrometryModel::getSky2TP);
cls.def("makeSkyWcs", &AstrometryModel::makeSkyWcs);
cls.def("getTotalParameters", &AstrometryModel::getTotalParameters);
cls.def("validate", &AstrometryModel::validate);
}

void declareSimpleAstrometryModel(py::module &mod) {
Expand Down
4 changes: 4 additions & 0 deletions python/lsst/jointcal/fitter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,17 @@ void declareAstrometryFit(py::module &mod) {

cls.def(py::init<std::shared_ptr<Associations>, std::shared_ptr<AstrometryModel>, double>(),
"associations"_a, "astrometryModel"_a, "posError"_a);

cls.def("getModel", &AstrometryFit::getModel, py::return_value_policy::reference_internal);
}

void declarePhotometryFit(py::module &mod) {
py::class_<PhotometryFit, std::shared_ptr<PhotometryFit>, FitterBase> cls(mod, "PhotometryFit");

cls.def(py::init<std::shared_ptr<Associations>, std::shared_ptr<PhotometryModel>>(), "associations"_a,
"photometryModel"_a);

cls.def("getModel", &PhotometryFit::getModel, py::return_value_policy::reference_internal);
}

PYBIND11_MODULE(fitter, mod) {
Expand Down
66 changes: 32 additions & 34 deletions python/lsst/jointcal/jointcal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

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.
Expand Down Expand Up @@ -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.")
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions python/lsst/jointcal/photometryModels.cc
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ void declarePhotometryModel(py::module &mod) {
cls.def("getRefError", &PhotometryModel::getRefError);
cls.def("computeRefResidual", &PhotometryModel::computeRefResidual);

cls.def("checkPositiveOnBBox", &PhotometryModel::checkPositiveOnBBox);
cls.def("validate", &PhotometryModel::validate);

cls.def("getMappingIndices", &PhotometryModel::getMappingIndices);
cls.def("computeParameterDerivatives",
[](PhotometryModel const &self, MeasuredStar const &star, CcdImage const &ccdImage) {
Expand Down