LSST Applications g02d81e74bb+86cf3d8bc9,g180d380827+7a4e862ed4,g2079a07aa2+86d27d4dc4,g2305ad1205+e1ca1c66fa,g29320951ab+012e1474a1,g295015adf3+341ea1ce94,g2bbee38e9b+0e5473021a,g337abbeb29+0e5473021a,g33d1c0ed96+0e5473021a,g3a166c0a6a+0e5473021a,g3ddfee87b4+c429d67c83,g48712c4677+f88676dd22,g487adcacf7+27e1e21933,g50ff169b8f+96c6868917,g52b1c1532d+585e252eca,g591dd9f2cf+b41db86c35,g5a732f18d5+53520f316c,g64a986408d+86cf3d8bc9,g858d7b2824+86cf3d8bc9,g8a8a8dda67+585e252eca,g99cad8db69+84912a7fdc,g9ddcbc5298+9a081db1e4,ga1e77700b3+15fc3df1f7,ga8c6da7877+a2b54eae19,gb0e22166c9+60f28cb32d,gba4ed39666+c2a2e4ac27,gbb8dafda3b+6681f309db,gc120e1dc64+f0fcc2f6d8,gc28159a63d+0e5473021a,gcf0d15dbbd+c429d67c83,gdaeeff99f8+f9a426f77a,ge6526c86ff+0433e6603d,ge79ae78c31+0e5473021a,gee10cc3b42+585e252eca,gff1a9f87cc+86cf3d8bc9,w.2024.17
LSST Data Management Base Package
Loading...
Searching...
No Matches
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.
 
void updateModel ()
 Update the 'model' field in the data catalog using the current best- fit transform.
 
double updateIntrinsicScatter ()
 Infer the intrinsic scatter in the offset between the data points and the best-fit model, and update the uncertainties accordingly.
 
double getIntrinsicScatter () const
 Return the current estimate of the intrinsic scatter.
 
std::pair< double, size_t > rejectOutliers (OutlierRejectionControl const &ctrl)
 Mark outliers in the data catalog using sigma clipping.
 
afw::table::BaseCatalog const & getData () const
 Return a catalog of data points and model values for diagnostic purposes.
 
ScaledPolynomialTransform const & getTransform () const
 Return the best-fit transform.
 
PolynomialTransform const & getPoly () const
 Return the polynomial part of the best-fit transform.
 
geom::AffineTransform const & getInputScaling () const
 Return the input scaling transform that maps input data points to [-1, 1].
 
geom::AffineTransform const & getOutputScaling () const
 Return the output scaling transform that maps output data points to [-1, 1].
 

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.
 
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.
 

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.
std::shared_ptr< RecordT > const get(size_type i) const
Return a pointer to the record at index i.
Definition Catalog.h:463
size_type size() const
Return the number of elements in the catalog.
Definition Catalog.h:412
bool isValid() const noexcept
Return True if all the constituent error Keys are valid.
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.
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}

◆ 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)
T end(T... args)
T insert(T... args)
T make_pair(T... args)
Eigen::Vector3d asEigen(sphgeom::Vector3d const &vector) noexcept
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.

◆ 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: