22 __all__ = [
"FitTanSipWcsTask", 
"FitTanSipWcsConfig"]
 
   31 import lsst.pex.config 
as pexConfig
 
   33 from .setMatchDistance 
import setMatchDistance
 
   34 from .sip 
import makeCreateWcsWithSip
 
   38     """Config for FitTanSipWcsTask.""" 
   39     order = pexConfig.RangeField(
 
   40         doc=
"order of SIP polynomial",
 
   45     numIter = pexConfig.RangeField(
 
   46         doc=
"number of iterations of fitter (which fits X and Y separately, and so benefits from " 
   52     numRejIter = pexConfig.RangeField(
 
   53         doc=
"number of rejection iterations",
 
   58     rejSigma = pexConfig.RangeField(
 
   59         doc=
"Number of standard deviations for clipping level",
 
   64     maxScatterArcsec = pexConfig.RangeField(
 
   65         doc=
"maximum median scatter of a WCS fit beyond which the fit fails (arcsec); " 
   66         "be generous, as this is only intended to catch catastrophic failures",
 
   74     """Fit a TAN-SIP WCS given a list of reference object/source matches. 
   76     ConfigClass = FitTanSipWcsConfig
 
   77     _DefaultName = 
"fitWcs" 
   80     def fitWcs(self, matches, initWcs, bbox=None, refCat=None, sourceCat=None, exposure=None):
 
   81         """Fit a TAN-SIP WCS from a list of reference object/source matches 
   85         matches : `list` of `lsst.afw.table.ReferenceMatch` 
   86             The following fields are read: 
   88             - match.first (reference object) coord 
   89             - match.second (source) centroid 
   91             The following fields are written: 
   93             - match.first (reference object) centroid, 
   94             - match.second (source) centroid 
   95             - match.distance (on sky separation, in radians) 
   97         initWcs : `lsst.afw.geom.SkyWcs` 
   99         bbox : `lsst.geom.Box2I` 
  100             the region over which the WCS will be valid (an lsst:afw::geom::Box2I); 
  101             if None or an empty box then computed from matches 
  102         refCat : `lsst.afw.table.SimpleCatalog` 
  103             reference object catalog, or None. 
  104             If provided then all centroids are updated with the new WCS, 
  105             otherwise only the centroids for ref objects in matches are updated. 
  106             Required fields are "centroid_x", "centroid_y", "coord_ra", and "coord_dec". 
  107         sourceCat : `lsst.afw.table.SourceCatalog` 
  108             source catalog, or None. 
  109             If provided then coords are updated with the new WCS; 
  110             otherwise only the coords for sources in matches are updated. 
  111             Required fields are "slot_Centroid_x", "slot_Centroid_y", and "coord_ra", and "coord_dec". 
  112         exposure : `lsst.afw.image.Exposure` 
  113             Ignored; present for consistency with FitSipDistortionTask. 
  117         result : `lsst.pipe.base.Struct` 
  118             with the following fields: 
  120             - ``wcs`` :  the fit WCS (`lsst.afw.geom.SkyWcs`) 
  121             - ``scatterOnSky`` :  median on-sky separation between reference 
  122               objects and sources in "matches" (`lsst.afw.geom.Angle`) 
  131         rejected = np.zeros(len(matches), dtype=bool)
 
  132         for rej 
in range(self.config.numRejIter):
 
  133             sipObject = self.
_fitWcs([mm 
for i, mm 
in enumerate(matches) 
if not rejected[i]], wcs)
 
  134             wcs = sipObject.getNewWcs()
 
  136             if rejected.sum() == len(rejected):
 
  137                 raise RuntimeError(
"All matches rejected in iteration %d" % (rej + 1,))
 
  139                 "Iteration {0} of astrometry fitting: rejected {1} outliers, " 
  140                 "out of {2} total matches.".
format(
 
  141                     rej, rejected.sum(), len(rejected)
 
  145                 print(
"Plotting fit after rejection iteration %d/%d" % (rej + 1, self.config.numRejIter))
 
  146                 self.
plotFit(matches, wcs, rejected)
 
  148         sipObject = self.
_fitWcs([mm 
for i, mm 
in enumerate(matches) 
if not rejected[i]], wcs)
 
  149         wcs = sipObject.getNewWcs()
 
  151             print(
"Plotting final fit")
 
  152             self.
plotFit(matches, wcs, rejected)
 
  154         if refCat 
is not None:
 
  155             self.log.
debug(
"Updating centroids in refCat")
 
  158             self.log.
warn(
"Updating reference object centroids in match list; refCat is None")
 
  161         if sourceCat 
is not None:
 
  162             self.log.
debug(
"Updating coords in sourceCat")
 
  165             self.log.
warn(
"Updating source coords in match list; sourceCat is None")
 
  168         self.log.
debug(
"Updating distance in match list")
 
  171         scatterOnSky = sipObject.getScatterOnSky()
 
  173         if scatterOnSky.asArcseconds() > self.config.maxScatterArcsec:
 
  174             raise pipeBase.TaskError(
 
  175                 "Fit failed: median scatter on sky = %0.3f arcsec > %0.3f config.maxScatterArcsec" %
 
  176                 (scatterOnSky.asArcseconds(), self.config.maxScatterArcsec))
 
  178         return pipeBase.Struct(
 
  180             scatterOnSky=scatterOnSky,
 
  184         """Generate a guess Wcs from the astrometric matches 
  186         We create a Wcs anchored at the center of the matches, with the scale 
  187         of the input Wcs.  This is necessary because matching returns only 
  188         matches with no estimated Wcs, and the input Wcs is a wild guess. 
  189         We're using the best of each: positions from the matches, and scale 
  194         matches : `list` of `lsst.afw.table.ReferenceMatch` 
  195             List of sources matched to references. 
  196         wcs : `lsst.afw.geom.SkyWcs` 
  201         newWcs : `lsst.afw.geom.SkyWcs` 
  202             Initial WCS guess from estimated crpix and crval. 
  208             crval += mm.first.getCoord().getVector()
 
  209         crpix /= len(matches)
 
  210         crval /= len(matches)
 
  213                                     cdMatrix=wcs.getCdMatrix())
 
  216     def _fitWcs(self, matches, wcs):
 
  217         """Fit a Wcs based on the matches and a guess Wcs. 
  221         matches : `list` of `lsst.afw.table.ReferenceMatch` 
  222             List of sources matched to references. 
  223         wcs : `lsst.afw.geom.SkyWcs` 
  228         sipObject : `lsst.meas.astrom.sip.CreateWcsWithSip` 
  231         for i 
in range(self.config.numIter):
 
  233             wcs = sipObject.getNewWcs()
 
  237         """Flag deviant matches 
  239         We return a boolean numpy array indicating whether the corresponding 
  240         match should be rejected.  The previous list of rejections is used 
  241         so we can calculate uncontaminated statistics. 
  245         matches : `list` of `lsst.afw.table.ReferenceMatch` 
  246             List of sources matched to references. 
  247         wcs : `lsst.afw.geom.SkyWcs` 
  249         rejected : array-like of `bool` 
  250             Array of matches rejected from the fit. Unused. 
  254         rejectedMatches : `ndarray` of type `bool` 
  255             Matched objects found to be outside of tolerance. 
  257         fit = [wcs.skyToPixel(m.first.getCoord()) 
for m 
in matches]
 
  258         dx = np.array([ff.getX() - mm.second.getCentroid().getX() 
for ff, mm 
in zip(fit, matches)])
 
  259         dy = np.array([ff.getY() - mm.second.getCentroid().getY() 
for ff, mm 
in zip(fit, matches)])
 
  260         good = np.logical_not(rejected)
 
  261         return (dx > self.config.rejSigma*dx[good].
std()) | (dy > self.config.rejSigma*dy[good].
std())
 
  266         We create four plots, for all combinations of (dx, dy) against 
  267         (x, y).  Good points are black, while rejected points are red. 
  271         matches : `list` of `lsst.afw.table.ReferenceMatch` 
  272             List of sources matched to references. 
  273         wcs : `lsst.afw.geom.SkyWcs` 
  275         rejected : array-like of `bool` 
  276             Array of matches rejected from the fit. 
  279             import matplotlib.pyplot 
as plt
 
  280         except ImportError 
as e:
 
  281             self.log.
warn(
"Unable to import matplotlib: %s", e)
 
  284         fit = [wcs.skyToPixel(m.first.getCoord()) 
for m 
in matches]
 
  285         x1 = np.array([ff.getX() 
for ff 
in fit])
 
  286         y1 = np.array([ff.getY() 
for ff 
in fit])
 
  287         x2 = np.array([m.second.getCentroid().getX() 
for m 
in matches])
 
  288         y2 = np.array([m.second.getCentroid().getY() 
for m 
in matches])
 
  293         good = np.logical_not(rejected)
 
  295         figure = plt.figure()
 
  296         axes = figure.add_subplot(2, 2, 1)
 
  297         axes.plot(x2[good], dx[good], 
'ko')
 
  298         axes.plot(x2[rejected], dx[rejected], 
'ro')
 
  300         axes.set_ylabel(
"dx")
 
  302         axes = figure.add_subplot(2, 2, 2)
 
  303         axes.plot(x2[good], dy[good], 
'ko')
 
  304         axes.plot(x2[rejected], dy[rejected], 
'ro')
 
  306         axes.set_ylabel(
"dy")
 
  308         axes = figure.add_subplot(2, 2, 3)
 
  309         axes.plot(y2[good], dx[good], 
'ko')
 
  310         axes.plot(y2[rejected], dx[rejected], 
'ro')
 
  312         axes.set_ylabel(
"dx")
 
  314         axes = figure.add_subplot(2, 2, 4)
 
  315         axes.plot(y2[good], dy[good], 
'ko')
 
  316         axes.plot(y2[rejected], dy[rejected], 
'ro')
 
  318         axes.set_ylabel(
"dy")