29 #include "boost/math/tools/minima.hpp" 33 #include "lsst/afw/table/aggregates.h" 34 #include "lsst/afw/table/Match.h" 35 #include "lsst/afw/math/LeastSquares.h" 44 #define LSST_ScaledPolynomialTransformFitter_TEST_IN_PLACE 0 46 namespace lsst {
namespace meas {
namespace astrom {
52 afw::table::Key<afw::table::RecordId>
refId;
53 afw::table::Key<afw::table::RecordId>
srcId;
69 static Keys const it(0);
82 refId(schema.addField<afw::table::RecordId>(
"ref_id",
"ID of reference object in this match.")),
83 srcId(schema.addField<afw::table::RecordId>(
"src_id",
"ID of source object in this match.")),
85 afw::table::Point2DKey::addFields(
86 schema,
"src",
"source positions in pixel coordinates.",
"pix" 90 afw::table::Point2DKey::addFields(
91 schema,
"intermediate",
"reference positions in intermediate world coordinates",
"deg" 95 afw::table::Point2DKey::addFields(
96 schema,
"initial",
"reference positions transformed by initial WCS",
"pix" 100 afw::table::Point2DKey::addFields(
101 schema,
"model",
"result of applying transform to reference positions",
"pix" 105 afw::table::CovarianceMatrixKey<float,2>::addFields(
106 schema,
"src", {
"x",
"y"},
"pix" 110 schema.addField<std::uint16_t>(
112 "True if the match should be rejected from the fit." 116 schema.getCitizen().markPersistent();
122 afw::table::Point2DKey::addFields(
123 schema,
"output",
"grid output positions in intermediate world coordinates",
"deg" 127 afw::table::Point2DKey::addFields(
128 schema,
"input",
"grid input positions in pixel coordinates.",
"pix" 132 afw::table::Point2DKey::addFields(
133 schema,
"model",
"result of applying transform to input positions",
"deg" 137 schema.getCitizen().markPersistent();
145 afw::geom::AffineTransform computeScaling(
146 afw::table::BaseCatalog
const & data,
147 afw::table::Point2DKey
const & key
149 afw::geom::Box2D bbox;
150 for (
auto const & record : data) {
151 bbox.include(afw::geom::Point2D(record.get(key)));
153 return afw::geom::AffineTransform(
154 afw::geom::LinearTransform::makeScaling(0.5*bbox.getWidth(), 0.5*bbox.getHeight())
155 ).invert() * afw::geom::AffineTransform(-afw::geom::Extent2D(bbox.getCenter()));
162 afw::table::ReferenceMatchVector
const & matches,
163 afw::image::Wcs
const & initialWcs,
164 double intrinsicScatter
167 afw::table::BaseCatalog catalog(keys.
schema);
168 catalog.reserve(matches.size());
169 float var2 = intrinsicScatter*intrinsicScatter;
170 for (
auto const & match : matches) {
171 auto record = catalog.addNew();
172 record->set(keys.
refId, match.first->getId());
173 record->set(keys.
srcId, match.second->getId());
174 record->set(keys.
input, initialWcs.skyToIntermediateWorldCoord(match.first->getCoord()));
175 record->set(keys.
initial, initialWcs.skyToPixel(match.first->getCoord()));
176 record->set(keys.
output, match.second->getCentroid());
177 record->set(keys.
outputErr, match.second->getCentroidErr() + var2*Eigen::Matrix2f::Identity());
185 computeScaling(catalog, keys.
input),
186 computeScaling(catalog, keys.
output)
192 afw::geom::Box2D
const & bbox,
193 int nGridX,
int nGridY,
197 afw::table::BaseCatalog catalog(keys.
schema);
198 catalog.reserve(nGridX*nGridY);
199 afw::geom::Extent2D dx(bbox.getWidth()/nGridX, 0.0);
200 afw::geom::Extent2D dy(0.0, bbox.getHeight()/nGridY);
201 for (
int iy = 0; iy < nGridY; ++iy) {
202 for (
int ix = 0; ix < nGridX; ++ix) {
203 afw::geom::Point2D point = bbox.getMin() + dx*ix + dy*iy;
204 auto record = catalog.addNew();
205 record->set(keys.
output, point);
206 record->set(keys.
input, toInvert(point));
214 computeScaling(catalog, keys.
input),
215 computeScaling(catalog, keys.
output)
219 ScaledPolynomialTransformFitter::ScaledPolynomialTransformFitter(
220 afw::table::BaseCatalog
const & data,
223 double intrinsicScatter,
224 afw::geom::AffineTransform
const & inputScaling,
225 afw::geom::AffineTransform
const & outputScaling
228 _intrinsicScatter(intrinsicScatter),
230 _outputScaling(outputScaling),
234 outputScaling.invert()
240 for (std::size_t i = 0; i < data.size(); ++i) {
249 for (
int n = 0, j = 0; n <= maxOrder; ++n) {
250 for (
int p = 0, q = n; p <= n; ++p, --q, ++j) {
251 _vandermonde(i, j) = _transform._poly._u[p] * _transform._poly._v[q];
258 int maxOrder = _transform.getPoly().getOrder();
262 if (order > maxOrder) {
264 pex::exceptions::LengthError,
265 (boost::format(
"Order (%d) exceeded maximum order for the fitter (%d)")
266 % order % maxOrder).str()
271 std::size_t nGood = 0;
272 if (_keys.rejected.isValid()) {
273 for (
auto const & record : _data) {
274 if (!record.get(_keys.rejected)) {
279 nGood = _data.size();
284 Eigen::MatrixXd m = Eigen::MatrixXd::Zero(nGood, packedSize);
286 Eigen::VectorXd vx = Eigen::VectorXd::Zero(nGood);
287 Eigen::VectorXd vy = Eigen::VectorXd::Zero(nGood);
290 Eigen::ArrayXd sxx(nGood);
291 Eigen::ArrayXd syy(nGood);
292 Eigen::ArrayXd sxy(nGood);
293 Eigen::Matrix2d outS = _outputScaling.getLinear().getMatrix();
294 for (std::size_t i1 = 0, i2 = 0; i1 < _data.size(); ++i1) {
297 if (!_keys.rejected.isValid() || !_data[i1].get(_keys.rejected)) {
298 afw::geom::Point2D
output = _outputScaling(_data[i1].
get(_keys.output));
299 vx[i2] = output.getX();
300 vy[i2] = output.getY();
301 m.row(i2) = _vandermonde.row(i1).head(packedSize);
302 if (_keys.outputErr.isValid()) {
303 Eigen::Matrix2d modelErr = outS*_data[i1].get(_keys.outputErr).cast<
double>()*outS.adjoint();
304 sxx[i2] = modelErr(0, 0);
305 sxy[i2] = modelErr(0, 1);
306 syy[i2] = modelErr(1, 1);
316 Eigen::ArrayXd fxx = 1.0/(sxx - sxy.square()/syy);
317 Eigen::ArrayXd fyy = 1.0/(syy - sxy.square()/sxx);
318 Eigen::ArrayXd fxy = -(sxy/sxx)*fyy;
319 #ifdef LSST_ScaledPolynomialTransformFitter_TEST_IN_PLACE 320 assert((sxx*fxx + sxy*fxy).isApproxToConstant(1.0));
321 assert((syy*fyy + sxy*fxy).isApproxToConstant(1.0));
322 assert((sxx*fxy).isApprox(-sxy*fyy));
323 assert((sxy*fxx).isApprox(-syy*fxy));
327 Eigen::MatrixXd h(2*packedSize, 2*packedSize);
328 h.topLeftCorner(packedSize, packedSize) = m.adjoint() * fxx.matrix().asDiagonal() * m;
329 h.topRightCorner(packedSize, packedSize) = m.adjoint() * fxy.matrix().asDiagonal() * m;
330 h.bottomLeftCorner(packedSize, packedSize) = h.topRightCorner(packedSize, packedSize).adjoint();
331 h.bottomRightCorner(packedSize, packedSize) = m.adjoint() * fyy.matrix().asDiagonal() * m;
333 Eigen::VectorXd g(2*packedSize);
334 g.head(packedSize) = m.adjoint() * (fxx.matrix().asDiagonal()*vx + fxy.matrix().asDiagonal()*vy);
335 g.tail(packedSize) = m.adjoint() * (fxy.matrix().asDiagonal()*vx + fyy.matrix().asDiagonal()*vy);
337 auto lstsq = afw::math::LeastSquares::fromNormalEquations(h, g);
338 auto solution = lstsq.getSolution();
340 for (
int n = 0, j = 0; n <= order; ++n) {
341 for (
int p = 0, q = n; p <= n; ++p, --q, ++j) {
342 _transform._poly._xCoeffs(p, q) = solution[j];
343 _transform._poly._yCoeffs(p, q) = solution[j + packedSize];
349 for (
auto & record : _data) {
352 _transform(record.get(_keys.input))
358 if (!_keys.rejected.isValid()) {
360 pex::exceptions::LogicError,
361 "Cannot compute intrinsic scatter on fitter initialized with fromGrid." 364 double newIntrinsicScatter = computeIntrinsicScatter();
365 float varDiff = newIntrinsicScatter*newIntrinsicScatter - _intrinsicScatter*_intrinsicScatter;
366 for (
auto & record : _data) {
367 record.set(_keys.outputErr, record.get(_keys.outputErr) + varDiff*Eigen::Matrix2f::Identity());
369 _intrinsicScatter = newIntrinsicScatter;
370 return _intrinsicScatter;
373 double ScaledPolynomialTransformFitter::computeIntrinsicScatter()
const {
379 double directVariance = 0.0;
380 double maxMeasurementVariance = 0.0;
381 double oldIntrinsicVariance = _intrinsicScatter*_intrinsicScatter;
382 std::size_t nGood = 0;
383 for (
auto const & record : _data) {
384 if (!_keys.rejected.isValid() || !record.get(_keys.rejected)) {
385 auto delta = record.get(_keys.output) - record.get(_keys.model);
386 directVariance += 0.5*delta.computeSquaredNorm();
387 double cxx = _keys.outputErr.getElement(record, 0, 0) - oldIntrinsicVariance;
388 double cyy = _keys.outputErr.getElement(record, 1, 1) - oldIntrinsicVariance;
389 double cxy = _keys.outputErr.getElement(record, 0, 1);
391 double ca2 = 0.5*(cxx + cyy + std::sqrt(cxx*cxx + cyy*cyy + 4*cxy*cxy - 2*cxx*cyy));
392 maxMeasurementVariance = std::max(maxMeasurementVariance, ca2);
396 directVariance /= nGood;
400 auto logLikelihood = [
this](
double intrinsicVariance) {
403 double varDiff = intrinsicVariance - this->_intrinsicScatter*this->_intrinsicScatter;
405 for (
auto & record : this->_data) {
406 double x = record.get(this->_keys.output.getX()) - record.get(_keys.model.getX());
407 double y = record.get(this->_keys.output.getY()) - record.get(_keys.model.getY());
408 double cxx = this->_keys.outputErr.getElement(record, 0, 0) + varDiff;
409 double cyy = this->_keys.outputErr.getElement(record, 1, 1) + varDiff;
410 double cxy = this->_keys.outputErr.getElement(record, 0, 1);
411 double det = cxx*cyy - cxy*cxy;
412 q += (x*x*cyy - 2*x*y*cxy + y*y*cxx)/det + std::log(det);
419 double minIntrinsicVariance = std::max(0.0, directVariance - maxMeasurementVariance);
422 static constexpr
int BITS_REQUIRED = 16;
423 boost::uintmax_t maxIterations = 20;
424 auto result = boost::math::tools::brent_find_minima(
426 minIntrinsicVariance,
431 return std::sqrt(result.first);
440 if (!_keys.rejected.isValid()) {
442 pex::exceptions::LogicError,
443 "Cannot reject outliers on fitter initialized with fromGrid." 446 if (static_cast<std::size_t>(ctrl.
nClipMin) >= _data.size()) {
448 pex::exceptions::LogicError,
449 (boost::format(
"Not enough values (%d) to clip %d.")
450 % _data.size() % ctrl.
nClipMin).str()
453 std::map<double,afw::table::BaseRecord *> rankings;
454 for (
auto & record : _data) {
455 Eigen::Matrix2d cov = record.get(_keys.outputErr).cast<
double>();
456 Eigen::Vector2d d = (record.get(_keys.output) - record.get(_keys.model)).asEigen();
457 double r2 = d.dot(cov.inverse() * d);
458 rankings.insert(std::make_pair(r2, &record));
460 auto cutoff = rankings.upper_bound(ctrl.
nSigma * ctrl.
nSigma);
461 int nClip = 0, nGood = 0;
462 for (
auto iter = rankings.begin(); iter != cutoff; ++iter) {
463 iter->second->set(_keys.rejected,
false);
466 for (
auto iter = cutoff; iter != rankings.end(); ++iter) {
467 iter->second->set(_keys.rejected,
true);
470 assert(static_cast<std::size_t>(nGood + nClip) == _data.size());
473 cutoff->second->set(_keys.rejected,
true);
476 while (nClip > ctrl.
nClipMax && cutoff != rankings.end()) {
477 cutoff->second->set(_keys.rejected,
false);
481 std::pair<double,std::size_t> result(ctrl.
nSigma, nClip);
482 if (cutoff != rankings.end()) {
483 result.first = std::sqrt(cutoff->first);
int nClipMax
"Never clip more than this many matches." ;
int nClipMin
"Always clip at least this many matches." ;
int computePackedSize(int order)
Compute this size of a packed 2-d polynomial coefficient array.
double nSigma
"Number of sigma to clip at." ;
Control object for outlier rejection in ScaledPolynomialTransformFitter.
void computePowers(Eigen::VectorXd &r, double x)
Fill an array with integer powers of x, so .