LSST Applications  21.0.0-172-gfb10e10a+18fedfabac,22.0.0+297cba6710,22.0.0+80564b0ff1,22.0.0+8d77f4f51a,22.0.0+a28f4c53b1,22.0.0+dcf3732eb2,22.0.1-1-g7d6de66+2a20fdde0d,22.0.1-1-g8e32f31+297cba6710,22.0.1-1-geca5380+7fa3b7d9b6,22.0.1-12-g44dc1dc+2a20fdde0d,22.0.1-15-g6a90155+515f58c32b,22.0.1-16-g9282f48+790f5f2caa,22.0.1-2-g92698f7+dcf3732eb2,22.0.1-2-ga9b0f51+7fa3b7d9b6,22.0.1-2-gd1925c9+bf4f0e694f,22.0.1-24-g1ad7a390+a9625a72a8,22.0.1-25-g5bf6245+3ad8ecd50b,22.0.1-25-gb120d7b+8b5510f75f,22.0.1-27-g97737f7+2a20fdde0d,22.0.1-32-gf62ce7b1+aa4237961e,22.0.1-4-g0b3f228+2a20fdde0d,22.0.1-4-g243d05b+871c1b8305,22.0.1-4-g3a563be+32dcf1063f,22.0.1-4-g44f2e3d+9e4ab0f4fa,22.0.1-42-gca6935d93+ba5e5ca3eb,22.0.1-5-g15c806e+85460ae5f3,22.0.1-5-g58711c4+611d128589,22.0.1-5-g75bb458+99c117b92f,22.0.1-6-g1c63a23+7fa3b7d9b6,22.0.1-6-g50866e6+84ff5a128b,22.0.1-6-g8d3140d+720564cf76,22.0.1-6-gd805d02+cc5644f571,22.0.1-8-ge5750ce+85460ae5f3,master-g6e05de7fdc+babf819c66,master-g99da0e417a+8d77f4f51a,w.2021.48
LSST Data Management Base Package
Classes | Public Member Functions | Static Public Member Functions | List of all members
lsst::meas::astrom::ScaledPolynomialTransformFitter Class Reference

A fitter class for scaled polynomial transforms. More...

#include <ScaledPolynomialTransformFitter.h>

Classes

class  Keys
 

Public Member Functions

void fit (int order=-1)
 Perform a linear least-squares fit of the polynomial coefficients. More...
 
void updateModel ()
 Update the 'model' field in the data catalog using the current best- fit transform. More...
 
double updateIntrinsicScatter ()
 Infer the intrinsic scatter in the offset between the data points and the best-fit model, and update the uncertainties accordingly. More...
 
double getIntrinsicScatter () const
 Return the current estimate of the intrinsic scatter. More...
 
std::pair< double, size_t > rejectOutliers (OutlierRejectionControl const &ctrl)
 Mark outliers in the data catalog using sigma clipping. More...
 
afw::table::BaseCatalog const & getData () const
 Return a catalog of data points and model values for diagnostic purposes. More...
 
ScaledPolynomialTransform const & getTransform () const
 Return the best-fit transform. More...
 
PolynomialTransform const & getPoly () const
 Return the polynomial part of the best-fit transform. More...
 
geom::AffineTransform const & getInputScaling () const
 Return the input scaling transform that maps input data points to [-1, 1]. More...
 
geom::AffineTransform const & getOutputScaling () const
 Return the output scaling transform that maps output data points to [-1, 1]. More...
 

Static Public Member Functions

static ScaledPolynomialTransformFitter fromMatches (int maxOrder, afw::table::ReferenceMatchVector const &matches, afw::geom::SkyWcs const &initialWcs, double intrinsicScatter)
 Initialize a fit from intermediate world coordinates to pixels using source/reference matches. More...
 
static ScaledPolynomialTransformFitter fromGrid (int maxOrder, geom::Box2D const &bbox, int nGridX, int nGridY, ScaledPolynomialTransform const &toInvert)
 Initialize a fit that inverts an existing transform by evaluating and fitting to points on a grid. More...
 

Detailed Description

A fitter class for scaled polynomial transforms.

This class allows for iteration between actual fitting, outlier rejection, and estimation of intrinsic scatter. It also provides access to the current model for debugging via an afw::table::BaseCatalog that contains the input values, output values, and the best-fit-transformed input values.

ScaledPolynomialTransformFitter has two public construction methods:

In either case, the fitter creates affine transforms that map the input and output data points onto [-1, 1]. It then fits a polynomial transform that, when composed with the input scaling transform and the inverse of the output scaling transform, maps the input data points to the output data points.

The fitter can be used in an outlier-rejection loop with the following pattern (with user-defined convergence criteria):

while (true) {
fitter.fit();
if (converged) break;
fitter.updateModel();
fitter.computeIntrinsicScatter();
fitter.rejectOutliers();
}

This pattern fits a model, uses that model to transform the input points, estimates intrinsic scatter from the differences between model-transformed points and output data points, and finally rejects outliers by sigma-clipping those differences before repeating.

ScaledPolynomialTransformFitter instances should be confined to a single thread.

Definition at line 101 of file ScaledPolynomialTransformFitter.h.

Member Function Documentation

◆ fit()

void lsst::meas::astrom::ScaledPolynomialTransformFitter::fit ( int  order = -1)

Perform a linear least-squares fit of the polynomial coefficients.

Parameters
[in]orderThe maximum order of the polynomial transform. If negative (the default) the maxOrder from construction is used.

After fitting, updateModel() should be called to apply the model transform to the input points if the user wants to make use of the data catalog for diagnostics. Calling fit() alone is sufficient if the user is only interested in getting the model transform itself.

Definition at line 200 of file ScaledPolynomialTransformFitter.cc.

200  {
201  int maxOrder = _transform.getPoly().getOrder();
202  if (order < 0) {
203  order = maxOrder;
204  }
205  if (order > maxOrder) {
206  throw LSST_EXCEPT(
207  pex::exceptions::LengthError,
208  (boost::format("Order (%d) exceeded maximum order for the fitter (%d)") % order % maxOrder)
209  .str());
210  }
211 
212  int const packedSize = detail::computePackedSize(order);
213  std::size_t nGood = 0;
214  if (_keys.rejected.isValid()) {
215  for (auto const &record : _data) {
216  if (!record.get(_keys.rejected)) {
217  ++nGood;
218  }
219  }
220  } else {
221  nGood = _data.size();
222  }
223  // One block of the block-diagonal (2x2) unweighted design matrix M;
224  // m[i,j] = u_i^{p(j)} v_i^{q(j)}. The two nonzero blocks are the same,
225  // because we're using the same polynomial basis for x and y.
226  Eigen::MatrixXd m = Eigen::MatrixXd::Zero(nGood, packedSize);
227  // vx, vy: (2x1) blocks of the unweighted data vector v
228  Eigen::VectorXd vx = Eigen::VectorXd::Zero(nGood);
229  Eigen::VectorXd vy = Eigen::VectorXd::Zero(nGood);
230  // sxx, syy, sxy: (2x2) blocks of the covariance matrix S, each of which is
231  // individually diagonal.
232  Eigen::ArrayXd sxx(nGood);
233  Eigen::ArrayXd syy(nGood);
234  Eigen::ArrayXd sxy(nGood);
235  Eigen::Matrix2d outS = _outputScaling.getLinear().getMatrix();
236  for (std::size_t i1 = 0, i2 = 0; i1 < _data.size(); ++i1) {
237  // check that the 'rejected' field (== 'not outlier-rejected') is both
238  // present in the schema and not rejected.
239  if (!_keys.rejected.isValid() || !_data[i1].get(_keys.rejected)) {
240  geom::Point2D output = _outputScaling(_data[i1].get(_keys.output));
241  vx[i2] = output.getX();
242  vy[i2] = output.getY();
243  m.row(i2) = _vandermonde.row(i1).head(packedSize);
244  if (_keys.outputErr.isValid()) {
245  Eigen::Matrix2d modelErr =
246  outS * _data[i1].get(_keys.outputErr).cast<double>() * outS.adjoint();
247  sxx[i2] = modelErr(0, 0);
248  sxy[i2] = modelErr(0, 1);
249  syy[i2] = modelErr(1, 1);
250  } else {
251  sxx[i2] = 1.0;
252  sxy[i2] = 0.0;
253  syy[i2] = 1.0;
254  }
255  ++i2;
256  }
257  }
258  // Do a blockwise inverse of S. Note that the result F is still symmetric
259  Eigen::ArrayXd fxx = 1.0 / (sxx - sxy.square() / syy);
260  Eigen::ArrayXd fyy = 1.0 / (syy - sxy.square() / sxx);
261  Eigen::ArrayXd fxy = -(sxy / sxx) * fyy;
262 #ifdef LSST_ScaledPolynomialTransformFitter_TEST_IN_PLACE
263  assert((sxx * fxx + sxy * fxy).isApproxToConstant(1.0));
264  assert((syy * fyy + sxy * fxy).isApproxToConstant(1.0));
265  assert((sxx * fxy).isApprox(-sxy * fyy));
266  assert((sxy * fxx).isApprox(-syy * fxy));
267 #endif
268  // Now that we've got all the block quantities, we'll form the full normal equations matrix.
269  // That's H = M^T F M:
270  Eigen::MatrixXd h(2 * packedSize, 2 * packedSize);
271  h.topLeftCorner(packedSize, packedSize) = m.adjoint() * fxx.matrix().asDiagonal() * m;
272  h.topRightCorner(packedSize, packedSize) = m.adjoint() * fxy.matrix().asDiagonal() * m;
273  h.bottomLeftCorner(packedSize, packedSize) = h.topRightCorner(packedSize, packedSize).adjoint();
274  h.bottomRightCorner(packedSize, packedSize) = m.adjoint() * fyy.matrix().asDiagonal() * m;
275  // And here's the corresponding RHS vector, g = M^T F v
276  Eigen::VectorXd g(2 * packedSize);
277  g.head(packedSize) = m.adjoint() * (fxx.matrix().asDiagonal() * vx + fxy.matrix().asDiagonal() * vy);
278  g.tail(packedSize) = m.adjoint() * (fxy.matrix().asDiagonal() * vx + fyy.matrix().asDiagonal() * vy);
279  // Solve the normal equations.
281  auto solution = lstsq.getSolution();
282  // Unpack the solution vector back into the polynomial coefficient matrices.
283  for (int n = 0, j = 0; n <= order; ++n) {
284  for (int p = 0, q = n; p <= n; ++p, --q, ++j) {
285  _transform._poly._xCoeffs(p, q) = solution[j];
286  _transform._poly._yCoeffs(p, q) = solution[j + packedSize];
287  }
288  }
289 }
#define LSST_EXCEPT(type,...)
Create an exception with a given type.
Definition: Exception.h:48
int m
Definition: SpanSet.cc:48
static LeastSquares fromNormalEquations(ndarray::Array< T1, 2, C1 > const &fisher, ndarray::Array< T2, 1, C2 > const &rhs, Factorization factorization=NORMAL_EIGENSYSTEM)
Initialize from the terms in the normal equations, given as ndarrays.
Definition: LeastSquares.h:154
bool isValid() const noexcept
Return True if all the constituent error Keys are valid.
Definition: aggregates.cc:294
bool isValid() const noexcept
Return true if the key was initialized to valid offset.
Definition: Key.h:97
LinearTransform const & getLinear() const noexcept
Matrix const & getMatrix() const noexcept
int getOrder() const
Return the order of the polynomials.
PolynomialTransform const & getPoly() const
Return the polynomial transform applied after the input scaling.
int computePackedSize(int order)
Compute this size of a packed 2-d polynomial coefficient array.
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:174
table::Key< int > order

◆ fromGrid()

ScaledPolynomialTransformFitter lsst::meas::astrom::ScaledPolynomialTransformFitter::fromGrid ( int  maxOrder,
geom::Box2D const &  bbox,
int  nGridX,
int  nGridY,
ScaledPolynomialTransform const &  toInvert 
)
static

Initialize a fit that inverts an existing transform by evaluating and fitting to points on a grid.

Parameters
[in]maxOrderMaximum polynomial order for the fit.
[in]bboxBounding box for the grid in the input coordinates of the toInvert transform (or equivalently the output coordinates of the transform to be fit).
[in]nGridXNumber of grid points in the X direction.
[in]nGridYNumber of grid points in the Y direction.
[in]toInvertTransform to invert

This initializes the data catalog with the following fields:

  • output: grid positions passed as input to the toInvert transform, treated as output data points when fitting its inverse.
  • input: grid positions generated as the output of the toInvert transform, treated as input data points when fitting its inverse.
  • model: best-fit-transformed input points.

The updateIntrinsicScatter and rejectOutliers methods cannot be used on fitters initialized using this method.

Definition at line 150 of file ScaledPolynomialTransformFitter.cc.

152  {
153  Keys const &keys = Keys::forGrid();
154  afw::table::BaseCatalog catalog(keys.schema);
155  catalog.reserve(nGridX * nGridY);
156  geom::Extent2D dx(bbox.getWidth() / nGridX, 0.0);
157  geom::Extent2D dy(0.0, bbox.getHeight() / nGridY);
158  for (int iy = 0; iy < nGridY; ++iy) {
159  for (int ix = 0; ix < nGridX; ++ix) {
160  geom::Point2D point = bbox.getMin() + dx * ix + dy * iy;
161  auto record = catalog.addNew();
162  record->set(keys.output, point);
163  record->set(keys.input, toInvert(point));
164  }
165  }
166  return ScaledPolynomialTransformFitter(catalog, keys, maxOrder, 0.0, computeScaling(catalog, keys.input),
167  computeScaling(catalog, keys.output));
168 }
AmpInfoBoxKey bbox
Definition: Amplifier.cc:117
CatalogT< BaseRecord > BaseCatalog
Definition: fwd.h:72

◆ fromMatches()

ScaledPolynomialTransformFitter lsst::meas::astrom::ScaledPolynomialTransformFitter::fromMatches ( int  maxOrder,
afw::table::ReferenceMatchVector const &  matches,
afw::geom::SkyWcs const &  initialWcs,
double  intrinsicScatter 
)
static

Initialize a fit from intermediate world coordinates to pixels using source/reference matches.

Parameters
[in]maxOrderMaximum polynomial order for the fit.
[in]matchesVector of source-reference matches. The centroids and centroid errors from the sources are used, along with the coords from the reference objects.
[in]initialWcsInitial WCS, used to transform reference positions to intermediate world coordinates and populate the "ref" field in the data catalog.
[in]intrinsicScatterInitial intrinsic scatter to be added in quadrature with source measurement errors in setting the uncertainty on each match.

This initializes the data catalog with the following fields:

  • ref_id: reference object ID
  • src_id: source ID from the match
  • src_{x,y}: source position in pixels
  • src_{xErr,yErr,x_y_Cov}: uncertainty on source positions, including measurement errors and the inferred intrinsic scatter.
  • intermediate_{x,y}: reference positions in intermediate world coordinates.
  • initial_{x,y}: reference positions transformed by initialWcs.
  • model_{x,y}: reference positions transformed by current best-fit distortion.
  • rejected Flag that is set for objects that have been rejected as outliers.

Definition at line 127 of file ScaledPolynomialTransformFitter.cc.

129  {
130  Keys const &keys = Keys::forMatches();
131  afw::table::BaseCatalog catalog(keys.schema);
132  catalog.reserve(matches.size());
133  float var2 = intrinsicScatter * intrinsicScatter;
134  auto initialIwcToSky = getIntermediateWorldCoordsToSky(initialWcs);
135  for (auto const &match : matches) {
136  auto record = catalog.addNew();
137  record->set(keys.refId, match.first->getId());
138  record->set(keys.srcId, match.second->getId());
139  record->set(keys.input, initialIwcToSky->applyInverse(match.first->getCoord()));
140  record->set(keys.initial, initialWcs.skyToPixel(match.first->getCoord()));
141  record->set(keys.output, match.second->getCentroid());
142  record->set(keys.outputErr, match.second->getCentroidErr() + var2 * Eigen::Matrix2f::Identity());
143  record->set(keys.rejected, false);
144  }
145  return ScaledPolynomialTransformFitter(catalog, keys, maxOrder, intrinsicScatter,
146  computeScaling(catalog, keys.input),
147  computeScaling(catalog, keys.output));
148 }
std::shared_ptr< TransformPoint2ToSpherePoint > getIntermediateWorldCoordsToSky(SkyWcs const &wcs, bool simplify=true)
Return a transform from intermediate world coordinates to sky.
Definition: SkyWcs.cc:553

◆ getData()

afw::table::BaseCatalog const& lsst::meas::astrom::ScaledPolynomialTransformFitter::getData ( ) const
inline

Return a catalog of data points and model values for diagnostic purposes.

The values in the returned catalog should not be modified by the user.

For information about the schema, either introspect it programmatically or see fromMatches and fromGrid.

Definition at line 249 of file ScaledPolynomialTransformFitter.h.

249 { return _data; }

◆ getInputScaling()

geom::AffineTransform const& lsst::meas::astrom::ScaledPolynomialTransformFitter::getInputScaling ( ) const
inline

Return the input scaling transform that maps input data points to [-1, 1].

Definition at line 270 of file ScaledPolynomialTransformFitter.h.

270 { return _transform.getInputScaling(); }
geom::AffineTransform const & getInputScaling() const
Return the first affine transform applied to input points.

◆ getIntrinsicScatter()

double lsst::meas::astrom::ScaledPolynomialTransformFitter::getIntrinsicScatter ( ) const
inline

Return the current estimate of the intrinsic scatter.

Because the intrinsic scatter is included in the uncertainty used in both the fit and outlier rejection, this may be called either before updateIntrinsicScatter() to obtain the scatter used in the last such operation or after updateIntrinsicScatter() to obtain the scatter to be used in the next operation.

Definition at line 219 of file ScaledPolynomialTransformFitter.h.

219 { return _intrinsicScatter; }

◆ getOutputScaling()

geom::AffineTransform const& lsst::meas::astrom::ScaledPolynomialTransformFitter::getOutputScaling ( ) const
inline

Return the output scaling transform that maps output data points to [-1, 1].

Definition at line 275 of file ScaledPolynomialTransformFitter.h.

275 { return _outputScaling; }

◆ getPoly()

PolynomialTransform const& lsst::meas::astrom::ScaledPolynomialTransformFitter::getPoly ( ) const
inline

Return the polynomial part of the best-fit transform.

Definition at line 265 of file ScaledPolynomialTransformFitter.h.

265 { return _transform.getPoly(); }

◆ getTransform()

ScaledPolynomialTransform const& lsst::meas::astrom::ScaledPolynomialTransformFitter::getTransform ( ) const
inline

Return the best-fit transform.

If f is an instance of this class, then for an arbitrary point p:

f.getTransform()(p) == f.getOutputScaling().inverted()(f.getPoly()(f.getInputScaling()(p)))

Definition at line 260 of file ScaledPolynomialTransformFitter.h.

260 { return _transform; }

◆ rejectOutliers()

std::pair< double, std::size_t > lsst::meas::astrom::ScaledPolynomialTransformFitter::rejectOutliers ( OutlierRejectionControl const &  ctrl)

Mark outliers in the data catalog using sigma clipping.

A data point is declare an outlier if:

  • the offset between the model prediction and the data position, weighted by the uncertainty (including inferred intrinsic scatter) of that point exceeds ctrl.nSigma
  • the number of points already with larger weighted offsets is less than ctrl.nClipMax. In addition, the worst (in weighted offset) ctrl.nClipMin data points are always rejected.
Returns
A pair of the smallest rejected weighted offset (units of sigma) and the number of point rejected.

Should be called after updateIntrinsicScatter() and before fit() if the fitter is being used in an outlier rejection loop.

Definition at line 367 of file ScaledPolynomialTransformFitter.cc.

368  {
369  // If the 'rejected' field isn't present in the schema (because the fitter
370  // was constructed with fromGrid), we can't do outlier rejection.
371  if (!_keys.rejected.isValid()) {
372  throw LSST_EXCEPT(pex::exceptions::LogicError,
373  "Cannot reject outliers on fitter initialized with fromGrid.");
374  }
375  if (static_cast<std::size_t>(ctrl.nClipMin) >= _data.size()) {
376  throw LSST_EXCEPT(
377  pex::exceptions::LogicError,
378  (boost::format("Not enough values (%d) to clip %d.") % _data.size() % ctrl.nClipMin).str());
379  }
381  for (auto &record : _data) {
382  Eigen::Matrix2d cov = record.get(_keys.outputErr).cast<double>();
383  Eigen::Vector2d d = (record.get(_keys.output) - record.get(_keys.model)).asEigen();
384  double r2 = d.dot(cov.inverse() * d);
385  rankings.insert(std::make_pair(r2, &record));
386  }
387  auto cutoff = rankings.upper_bound(ctrl.nSigma * ctrl.nSigma);
388  int nClip = 0, nGood = 0;
389  for (auto iter = rankings.begin(); iter != cutoff; ++iter) {
390  iter->second->set(_keys.rejected, false);
391  ++nGood;
392  }
393  for (auto iter = cutoff; iter != rankings.end(); ++iter) {
394  iter->second->set(_keys.rejected, true);
395  ++nClip;
396  }
397  assert(static_cast<std::size_t>(nGood + nClip) == _data.size());
398  while (nClip < ctrl.nClipMin) {
399  --cutoff;
400  cutoff->second->set(_keys.rejected, true);
401  ++nClip;
402  }
403  while (nClip > ctrl.nClipMax && cutoff != rankings.end()) {
404  cutoff->second->set(_keys.rejected, false);
405  ++cutoff;
406  --nClip;
407  }
408  std::pair<double, std::size_t> result(ctrl.nSigma, nClip);
409  if (cutoff != rankings.end()) {
410  result.first = std::sqrt(cutoff->first);
411  }
412  return result;
413 }
py::object result
Definition: _schema.cc:429
T begin(T... args)
size_type size() const
Return the number of elements in the catalog.
Definition: Catalog.h:413
T end(T... args)
T insert(T... args)
T make_pair(T... args)
Eigen::Vector3d asEigen(sphgeom::Vector3d const &vector) noexcept
Definition: sphgeomUtils.h:36
T sqrt(T... args)
T upper_bound(T... args)

◆ updateIntrinsicScatter()

double lsst::meas::astrom::ScaledPolynomialTransformFitter::updateIntrinsicScatter ( )

Infer the intrinsic scatter in the offset between the data points and the best-fit model, and update the uncertainties accordingly.

The scatter is calculated as the RMS scatter that maximizes the likelihood of the current positional offsets (assumed fixed) assuming the per-data-point uncertainties are the quadrature sum of the intrinsic scatter and that source's centroid measurement uncertainty.

Should be called after updateModel() and before rejectOutliers() if the fitter is being used in an outlier-rejection loop.

Definition at line 297 of file ScaledPolynomialTransformFitter.cc.

297  {
298  if (!_keys.rejected.isValid()) {
299  throw LSST_EXCEPT(pex::exceptions::LogicError,
300  "Cannot compute intrinsic scatter on fitter initialized with fromGrid.");
301  }
302  double newIntrinsicScatter = computeIntrinsicScatter();
303  float varDiff = newIntrinsicScatter * newIntrinsicScatter - _intrinsicScatter * _intrinsicScatter;
304  for (auto &record : _data) {
305  record.set(_keys.outputErr, record.get(_keys.outputErr) + varDiff * Eigen::Matrix2f::Identity());
306  }
307  _intrinsicScatter = newIntrinsicScatter;
308  return _intrinsicScatter;
309 }
Eigen::Matrix< T, N, N > get(BaseRecord const &record) const override
Get a covariance matrix from the given record.
Definition: aggregates.cc:259

◆ updateModel()

void lsst::meas::astrom::ScaledPolynomialTransformFitter::updateModel ( )

Update the 'model' field in the data catalog using the current best- fit transform.

Should be called after fit() and before updateIntrinsicScatter() if the fitter is being used in an outlier-rejection loop.

Definition at line 291 of file ScaledPolynomialTransformFitter.cc.

291  {
292  for (auto &record : _data) {
293  record.set(_keys.model, _transform(record.get(_keys.input)));
294  }
295 }

The documentation for this class was generated from the following files: