29 #include "boost/math/tools/minima.hpp" 46 #define LSST_ScaledPolynomialTransformFitter_TEST_IN_PLACE 0 73 static Keys const it(0);
88 "source positions in pixel coordinates.",
"pix")),
90 "reference positions in intermediate world coordinates",
93 schema,
"initial",
"reference positions transformed by initial WCS",
"pix")),
95 schema,
"model",
"result of applying transform to reference positions",
"pix")),
99 "True if the match should be rejected from the fit.")) {
106 schema,
"output",
"grid output positions in intermediate world coordinates",
"deg")),
108 "grid input positions in pixel coordinates.",
"pix")),
110 schema,
"model",
"result of applying transform to input positions",
"deg")) {
120 for (
auto const &record : data) {
133 double intrinsicScatter) {
137 float var2 = intrinsicScatter * intrinsicScatter;
139 for (
auto const &match : matches) {
140 auto record = catalog.
addNew();
141 record->set(keys.
refId, match.first->getId());
142 record->set(keys.
srcId, match.second->getId());
143 record->set(keys.
input, initialIwcToSky->applyInverse(match.first->getCoord()));
145 record->set(keys.
output, match.second->getCentroid());
146 record->set(keys.
outputErr, match.second->getCentroidErr() + var2 * Eigen::Matrix2f::Identity());
150 computeScaling(catalog, keys.
input),
151 computeScaling(catalog, keys.
output));
155 int maxOrder,
geom::Box2D const &bbox,
int nGridX,
int nGridY,
159 catalog.
reserve(nGridX * nGridY);
162 for (
int iy = 0; iy < nGridY; ++iy) {
163 for (
int ix = 0; ix < nGridX; ++ix) {
165 auto record = catalog.
addNew();
166 record->set(keys.
output, point);
167 record->set(keys.
input, toInvert(point));
171 computeScaling(catalog, keys.
output));
176 double intrinsicScatter,
180 _intrinsicScatter(intrinsicScatter),
182 _outputScaling(outputScaling),
196 for (
int n = 0, j = 0; n <= maxOrder; ++n) {
197 for (
int p = 0, q = n; p <= n; ++p, --q, ++j) {
198 _vandermonde(i, j) = _transform._poly._u[p] * _transform._poly._v[q];
205 int maxOrder = _transform.getPoly().getOrder();
209 if (order > maxOrder) {
212 (boost::format(
"Order (%d) exceeded maximum order for the fitter (%d)") % order % maxOrder)
218 if (_keys.rejected.isValid()) {
219 for (
auto const &record : _data) {
220 if (!record.get(_keys.rejected)) {
225 nGood = _data.size();
230 Eigen::MatrixXd
m = Eigen::MatrixXd::Zero(nGood, packedSize);
232 Eigen::VectorXd vx = Eigen::VectorXd::Zero(nGood);
233 Eigen::VectorXd vy = Eigen::VectorXd::Zero(nGood);
236 Eigen::ArrayXd sxx(nGood);
237 Eigen::ArrayXd syy(nGood);
238 Eigen::ArrayXd sxy(nGood);
239 Eigen::Matrix2d outS = _outputScaling.getLinear().getMatrix();
240 for (
std::size_t i1 = 0, i2 = 0; i1 < _data.size(); ++i1) {
243 if (!_keys.rejected.isValid() || !_data[i1].get(_keys.rejected)) {
245 vx[i2] = output.getX();
246 vy[i2] = output.getY();
247 m.row(i2) = _vandermonde.row(i1).head(packedSize);
248 if (_keys.outputErr.isValid()) {
249 Eigen::Matrix2d modelErr =
250 outS * _data[i1].get(_keys.outputErr).cast<
double>() * outS.adjoint();
251 sxx[i2] = modelErr(0, 0);
252 sxy[i2] = modelErr(0, 1);
253 syy[i2] = modelErr(1, 1);
263 Eigen::ArrayXd fxx = 1.0 / (sxx - sxy.square() / syy);
264 Eigen::ArrayXd fyy = 1.0 / (syy - sxy.square() / sxx);
265 Eigen::ArrayXd fxy = -(sxy / sxx) * fyy;
266 #ifdef LSST_ScaledPolynomialTransformFitter_TEST_IN_PLACE 267 assert((sxx * fxx + sxy * fxy).isApproxToConstant(1.0));
268 assert((syy * fyy + sxy * fxy).isApproxToConstant(1.0));
269 assert((sxx * fxy).isApprox(-sxy * fyy));
270 assert((sxy * fxx).isApprox(-syy * fxy));
274 Eigen::MatrixXd h(2 * packedSize, 2 * packedSize);
275 h.topLeftCorner(packedSize, packedSize) = m.adjoint() * fxx.matrix().asDiagonal() * m;
276 h.topRightCorner(packedSize, packedSize) = m.adjoint() * fxy.matrix().asDiagonal() * m;
277 h.bottomLeftCorner(packedSize, packedSize) = h.topRightCorner(packedSize, packedSize).adjoint();
278 h.bottomRightCorner(packedSize, packedSize) = m.adjoint() * fyy.matrix().asDiagonal() * m;
280 Eigen::VectorXd g(2 * packedSize);
281 g.head(packedSize) = m.adjoint() * (fxx.matrix().asDiagonal() * vx + fxy.matrix().asDiagonal() * vy);
282 g.tail(packedSize) = m.adjoint() * (fxy.matrix().asDiagonal() * vx + fyy.matrix().asDiagonal() * vy);
285 auto solution = lstsq.getSolution();
287 for (
int n = 0, j = 0; n <= order; ++n) {
288 for (
int p = 0, q = n; p <= n; ++p, --q, ++j) {
289 _transform._poly._xCoeffs(p, q) = solution[j];
290 _transform._poly._yCoeffs(p, q) = solution[j + packedSize];
296 for (
auto &record : _data) {
297 record.set(_keys.model, _transform(record.get(_keys.input)));
302 if (!_keys.rejected.isValid()) {
304 "Cannot compute intrinsic scatter on fitter initialized with fromGrid.");
306 double newIntrinsicScatter = computeIntrinsicScatter();
307 float varDiff = newIntrinsicScatter * newIntrinsicScatter - _intrinsicScatter * _intrinsicScatter;
308 for (
auto &record : _data) {
309 record.set(_keys.outputErr, record.get(_keys.outputErr) + varDiff * Eigen::Matrix2f::Identity());
311 _intrinsicScatter = newIntrinsicScatter;
312 return _intrinsicScatter;
315 double ScaledPolynomialTransformFitter::computeIntrinsicScatter()
const {
321 double directVariance = 0.0;
322 double maxMeasurementVariance = 0.0;
323 double oldIntrinsicVariance = _intrinsicScatter * _intrinsicScatter;
325 for (
auto const &record : _data) {
326 if (!_keys.rejected.isValid() || !record.get(_keys.rejected)) {
327 auto delta = record.get(_keys.output) - record.get(_keys.model);
328 directVariance += 0.5 * delta.computeSquaredNorm();
329 double cxx = _keys.outputErr.getElement(record, 0, 0) - oldIntrinsicVariance;
330 double cyy = _keys.outputErr.getElement(record, 1, 1) - oldIntrinsicVariance;
331 double cxy = _keys.outputErr.getElement(record, 0, 1);
333 double ca2 = 0.5 * (cxx + cyy +
std::sqrt(cxx * cxx + cyy * cyy + 4 * cxy * cxy - 2 * cxx * cyy));
334 maxMeasurementVariance =
std::max(maxMeasurementVariance, ca2);
338 directVariance /= nGood;
342 auto logLikelihood = [
this](
double intrinsicVariance) {
345 double varDiff = intrinsicVariance - this->_intrinsicScatter * this->_intrinsicScatter;
347 for (
auto &record : this->_data) {
348 double x = record.get(this->_keys.output.getX()) - record.get(_keys.model.getX());
349 double y = record.get(this->_keys.output.getY()) - record.get(_keys.model.getY());
350 double cxx = this->_keys.outputErr.getElement(record, 0, 0) + varDiff;
351 double cyy = this->_keys.outputErr.getElement(record, 1, 1) + varDiff;
352 double cxy = this->_keys.outputErr.getElement(record, 0, 1);
353 double det = cxx * cyy - cxy * cxy;
354 q += (x * x * cyy - 2 * x * y * cxy + y * y * cxx) / det +
std::log(det);
361 double minIntrinsicVariance =
std::max(0.0, directVariance - maxMeasurementVariance);
364 static constexpr
int BITS_REQUIRED = 16;
365 boost::uintmax_t maxIterations = 20;
366 auto result = boost::math::tools::brent_find_minima(logLikelihood, minIntrinsicVariance, directVariance,
367 BITS_REQUIRED, maxIterations);
375 if (!_keys.rejected.isValid()) {
377 "Cannot reject outliers on fitter initialized with fromGrid.");
379 if (static_cast<std::size_t>(ctrl.
nClipMin) >= _data.size()) {
382 (boost::format(
"Not enough values (%d) to clip %d.") % _data.size() % ctrl.
nClipMin).str());
385 for (
auto &record : _data) {
386 Eigen::Matrix2d cov = record.get(_keys.outputErr).cast<
double>();
387 Eigen::Vector2d d = (record.get(_keys.output) - record.get(_keys.model)).
asEigen();
388 double r2 = d.dot(cov.inverse() * d);
391 auto cutoff = rankings.upper_bound(ctrl.
nSigma * ctrl.
nSigma);
392 int nClip = 0, nGood = 0;
393 for (
auto iter = rankings.begin(); iter != cutoff; ++iter) {
394 iter->second->set(_keys.rejected,
false);
397 for (
auto iter = cutoff; iter != rankings.end(); ++iter) {
398 iter->second->set(_keys.rejected,
true);
401 assert(static_cast<std::size_t>(nGood + nClip) == _data.size());
404 cutoff->second->set(_keys.rejected,
true);
407 while (nClip > ctrl.
nClipMax && cutoff != rankings.end()) {
408 cutoff->second->set(_keys.rejected,
false);
413 if (cutoff != rankings.end()) {
daf::base::Citizen & getCitizen()
double getHeight() const noexcept
static PointKey addFields(Schema &schema, std::string const &name, std::string const &doc, std::string const &unit)
double getWidth() const noexcept
int nClipMax
"Never clip more than this many matches." ;
lsst::geom::Point2D skyToPixel(lsst::geom::SpherePoint const &sky) const
Point2D const getCenter() const noexcept
static LeastSquares fromNormalEquations(ndarray::Array< T1, 2, C1 > const &fisher, ndarray::Array< T2, 1, C2 > const &rhs, Factorization factorization=NORMAL_EIGENSYSTEM)
int nClipMin
"Always clip at least this many matches." ;
void include(Point2D const &point) noexcept
Point2D const getMin() const noexcept
Key< T > addField(Field< T > const &field, bool doReplace=false)
std::shared_ptr< TransformPoint2ToSpherePoint > getIntermediateWorldCoordsToSky(SkyWcs const &wcs, bool simplify=true)
int computePackedSize(int order)
Compute this size of a packed 2-d polynomial coefficient array.
void markPersistent(void)
Eigen::Vector3d asEigen(sphgeom::Vector3d const &vector) noexcept
void reserve(size_type n)
#define LSST_EXCEPT(type,...)
double nSigma
"Number of sigma to clip at." ;
Control object for outlier rejection in ScaledPolynomialTransformFitter.
std::shared_ptr< RecordT > addNew()
void computePowers(Eigen::VectorXd &r, double x)
Fill an array with integer powers of x, so .