1 from __future__
import absolute_import, division, print_function
9 from .loadAstrometryNetObjects
import LoadAstrometryNetObjectsTask
10 from .matchOptimisticB
import MatchOptimisticBTask
11 from .fitTanSipWcs
import FitTanSipWcsTask
12 from .display
import displayAstrometry
13 from .astromLib
import makeMatchStatistics
16 refObjLoader = pexConfig.ConfigurableField(
17 target = LoadAstrometryNetObjectsTask,
18 doc =
"reference object loader",
20 matcher = pexConfig.ConfigurableField(
21 target = MatchOptimisticBTask,
22 doc =
"reference object/source matcher",
24 wcsFitter = pexConfig.ConfigurableField(
25 target = FitTanSipWcsTask,
28 forceKnownWcs = pexConfig.Field(
30 doc=
"If True then load reference objects and match sources but do not fit a WCS; " +
31 " this simply controls whether 'run' calls 'solve' or 'loadAndMatch'",
34 maxIter = pexConfig.RangeField(
35 doc =
"maximum number of iterations of match sources and fit WCS" +
36 "ignored if not fitting a WCS",
41 matchDistanceSigma = pexConfig.RangeField(
42 doc =
"the maximum match distance is set to "
43 " mean_match_distance + matchDistanceSigma*std_dev_match_distance; " +
44 "ignored if not fitting a WCS",
49 minMatchDistanceArcSec = pexConfig.RangeField(
50 doc =
"the match distance below which further iteration is pointless (arcsec); "
51 "ignored if not fitting a WCS",
66 """!Match an input source catalog with objects from a reference catalog and solve for the WCS
68 @anchor AstrometryTask_
70 @section meas_astrom_astrometry_Contents Contents
72 - @ref meas_astrom_astrometry_Purpose
73 - @ref meas_astrom_astrometry_Initialize
74 - @ref meas_astrom_astrometry_IO
75 - @ref meas_astrom_astrometry_Config
76 - @ref meas_astrom_astrometry_Example
77 - @ref meas_astrom_astrometry_Debug
79 @section meas_astrom_astrometry_Purpose Description
81 Match input sourceCat with a reference catalog and solve for the Wcs
83 There are three steps, each performed by different subtasks:
84 - Find position reference stars that overlap the exposure
85 - Match sourceCat to position reference stars
86 - Fit a WCS based on the matches
88 @section meas_astrom_astrometry_Initialize Task initialisation
92 @section meas_astrom_astrometry_IO Invoking the Task
98 @section meas_astrom_astrometry_Config Configuration parameters
100 See @ref AstrometryConfig
102 @section meas_astrom_astrometry_Example A complete example of using AstrometryTask
104 See \ref meas_photocal_photocal_Example.
106 @section meas_astrom_astrometry_Debug Debug variables
108 The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink interface supports a
109 flag @c -d to import @b debug.py from your @c PYTHONPATH; see @ref baseDebug for more about
112 The available variables in AstrometryTask are:
114 <DT> @c display (bool)
115 <DD> If True display information at three stages: after finding reference objects,
116 after matching sources to reference objects, and after fitting the WCS; defaults to False
118 <DD> ds9 frame to use to display the reference objects; the next two frames are used
119 to display the match list and the results of the final WCS; defaults to 0
122 To investigate the @ref meas_astrom_astrometry_Debug, put something like
126 debug = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
127 if name == "lsst.meas.astrom.astrometry":
132 lsstDebug.Info = DebugInfo
134 into your debug.py file and run this task with the @c --debug flag.
136 ConfigClass = AstrometryConfig
137 _DefaultName =
"astrometricSolver"
140 """!Construct an AstrometryTask
142 @param[in] schema ignored; available for compatibility with an older astrometry task
143 @param[in] kwargs additional keyword arguments for pipe_base Task.\_\_init\_\_
145 pipeBase.Task.__init__(self, **kwargs)
146 self.makeSubtask(
"refObjLoader")
147 self.makeSubtask(
"matcher")
148 self.makeSubtask(
"wcsFitter")
151 def run(self, exposure, sourceCat):
152 """!Load reference objects, match sources and optionally fit a WCS
154 This is a thin layer around solve or loadAndMatch, depending on config.forceKnownWcs
156 @param[in,out] exposure exposure whose WCS is to be fit
157 The following are read only:
159 - calib (may be absent)
160 - filter (may be unset)
161 - detector (if wcs is pure tangent; may be absent)
162 The following are updated:
163 - wcs (the initial value is used as an initial guess, and is required)
164 @param[in] sourceCat catalog of sourceCat detected on the exposure (an lsst.afw.table.SourceCatalog)
165 @return an lsst.pipe.base.Struct with these fields:
166 - refCat reference object catalog of objects that overlap the exposure (with some margin)
167 (an lsst::afw::table::SimpleCatalog)
168 - matches list of reference object/source matches (an lsst.afw.table.ReferenceMatchVector)
169 - scatterOnSky median on-sky separation between reference objects and sources in "matches"
170 (an lsst.afw.geom.Angle), or None if config.forceKnownWcs True
171 - matchMeta metadata about the field (an lsst.daf.base.PropertyList)
173 if self.config.forceKnownWcs:
174 res = self.
loadAndMatch(exposure=exposure, sourceCat=sourceCat)
175 res.scatterOnSky =
None
177 res = self.
solve(exposure=exposure, sourceCat=sourceCat)
182 """!Load reference objects overlapping an exposure and match to sources detected on that exposure
184 @param[in] exposure exposure whose WCS is to be fit
185 @param[in] sourceCat catalog of sourceCat detected on the exposure (an lsst.afw.table.SourceCatalog)
187 @return an lsst.pipe.base.Struct with these fields:
188 - refCat reference object catalog of objects that overlap the exposure (with some margin)
189 (an lsst::afw::table::SimpleCatalog)
190 - matches list of reference object/source matches (an lsst.afw.table.ReferenceMatchVector)
191 - matchMeta metadata about the field (an lsst.daf.base.PropertyList)
193 @note ignores config.forceKnownWcs, config.maxIter, config.matchDistanceSigma
194 and config.minMatchDistanceArcSec
201 loadRes = self.refObjLoader.loadPixelBox(
204 filterName = expMd.filterName,
208 matchRes = self.matcher.matchObjectsToSources(
209 refCat = loadRes.refCat,
210 sourceCat = sourceCat,
212 refFluxField = loadRes.fluxField,
218 "Found %d matches with scatter = %0.3f +- %0.3f arcsec; " %
219 (len(matchRes.matches), distStats.distMean.asArcseconds(), distStats.distStdDev.asArcseconds())
223 frame = int(debug.frame)
225 refCat = loadRes.refCat,
226 sourceCat = sourceCat,
227 matches = matchRes.matches,
234 return pipeBase.Struct(
235 refCat = loadRes.refCat,
236 matches = matchRes.matches,
237 matchMeta = self.
_createMatchMetadata(bbox=expMd.bbox, wcs=expMd.wcs, filterName=expMd.filterName),
241 def solve(self, exposure, sourceCat):
242 """!Load reference objects overlapping an exposure, match to sources and fit a WCS
244 @return an lsst.pipe.base.Struct with these fields:
245 - refCat reference object catalog of objects that overlap the exposure (with some margin)
246 (an lsst::afw::table::SimpleCatalog)
247 - matches list of reference object/source matches (an lsst.afw.table.ReferenceMatchVector)
248 - scatterOnSky median on-sky separation between reference objects and sources in "matches"
249 (an lsst.afw.geom.Angle)
250 - matchMeta metadata about the field (an lsst.daf.base.PropertyList)
252 @note ignores config.forceKnownWcs
259 loadRes = self.refObjLoader.loadPixelBox(
262 filterName = expMd.filterName,
266 frame = int(debug.frame)
268 refCat = loadRes.refCat,
269 sourceCat = sourceCat,
273 title=
"Reference catalog",
279 for i
in range(self.config.maxIter):
283 refCat = loadRes.refCat,
284 sourceCat = sourceCat,
285 refFluxField = loadRes.fluxField,
289 maxMatchDist = maxMatchDist,
291 except Exception
as e:
294 self.log.info(
"Fit WCS iter %d failed; using previous iteration: %s" % (iterNum, e))
302 "Match and fit WCS iteration %d: found %d matches with scatter = %0.3f +- %0.3f arcsec; "
303 "max match distance = %0.3f arcsec" %
304 (iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
305 tryMatchDist.distStdDev.asArcseconds(), tryMatchDist.maxMatchDist.asArcseconds()))
306 if maxMatchDist
is not None:
307 if tryMatchDist.maxMatchDist >= maxMatchDist:
309 "Iteration %d had no better maxMatchDist; using previous iteration" % (iterNum,))
313 maxMatchDist = tryMatchDist.maxMatchDist
316 if tryMatchDist.maxMatchDist.asArcseconds() < self.config.minMatchDistanceArcSec:
318 "Max match distance = %0.3f arcsec < %0.3f = config.minMatchDistanceArcSec; "
319 "that's good enough" %
320 (tryMatchDist.maxMatchDist.asArcseconds(), self.config.minMatchDistanceArcSec))
324 "Matched and fit WCS in %d iterations; "
325 "found %d matches with scatter = %0.3f +- %0.3f arcsec" %
326 (iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
327 tryMatchDist.distStdDev.asArcseconds()))
329 exposure.setWcs(res.wcs)
331 return pipeBase.Struct(
332 refCat = loadRes.refCat,
333 matches = res.matches,
334 scatterOnSky = res.scatterOnSky,
335 matchMeta = self.
_createMatchMetadata(bbox=expMd.bbox, wcs=res.wcs, filterName=expMd.filterName)
339 """Compute on-sky radial distance statistics for a match list
341 @param[in] matchList list of matches between reference object and sources;
342 the distance field is the only field read and it must be set to distance in radians
344 @return a pipe_base Struct containing these fields:
345 - distMean clipped mean of on-sky radial separation
346 - distStdDev clipped standard deviation of on-sky radial separation
347 - maxMatchDist distMean + self.config.matchDistanceSigma*distStdDev
350 distMean = distStatsInRadians.getValue(afwMath.MEANCLIP)*afwGeom.radians
351 distStdDev = distStatsInRadians.getValue(afwMath.STDEVCLIP)*afwGeom.radians
352 return pipeBase.Struct(
354 distStdDev = distStdDev,
355 maxMatchDist = distMean + self.config.matchDistanceSigma*distStdDev,
358 """!Extract metadata from an exposure
360 @return an lsst.pipe.base.Struct containing the following exposure metadata:
361 - bbox: parent bounding box
362 - wcs: WCS (an lsst.afw.image.Wcs)
363 - calib calibration (an lsst.afw.image.Calib), or None if unknown
364 - filterName: name of filter, or None if unknown
366 exposureInfo = exposure.getInfo()
367 filterName = exposureInfo.getFilter().getName()
or None
368 if filterName ==
"_unknown_":
370 return pipeBase.Struct(
371 bbox = exposure.getBBox(),
373 calib = exposureInfo.getCalib()
if exposureInfo.hasCalib()
else None,
374 filterName = filterName,
378 def _matchAndFitWcs(self, refCat, sourceCat, refFluxField, bbox, wcs, maxMatchDist=None,
380 """!Match sources to reference objects and fit a WCS
382 @param[in] refCat catalog of reference objects
383 @param[in] sourceCat catalog of sourceCat detected on the exposure (an lsst.afw.table.SourceCatalog)
384 @param[in] refFluxField field of refCat to use for flux
385 @param[in] bbox bounding box of exposure (an lsst.afw.geom.Box2I)
386 @param[in] wcs initial guess for WCS of exposure (an lsst.afw.image.Wcs)
387 @param[in] maxMatchDist maximum on-sky distance between reference objects and sources
388 (an lsst.afw.geom.Angle); if None then use the matcher's default
389 @param[in] exposure exposure whose WCS is to be fit, or None; used only for the debug display
391 @return an lsst.pipe.base.Struct with these fields:
392 - matches list of reference object/source matches (an lsst.afw.table.ReferenceMatchVector)
393 - wcs the fit WCS (an lsst.afw.image.Wcs)
394 - scatterOnSky median on-sky separation between reference objects and sources in "matches"
395 (an lsst.afw.geom.Angle)
399 matchRes = self.matcher.matchObjectsToSources(
401 sourceCat = sourceCat,
403 refFluxField = refFluxField,
404 maxMatchDist = maxMatchDist,
406 self.log.logdebug(
"Found %s matches" % (len(matchRes.matches),))
408 frame = int(debug.frame)
411 sourceCat = matchRes.usableSourceCat,
412 matches = matchRes.matches,
419 self.log.logdebug(
"Fitting WCS")
420 fitRes = self.wcsFitter.fitWcs(
421 matches = matchRes.matches,
425 sourceCat = sourceCat,
428 scatterOnSky = fitRes.scatterOnSky
430 frame = int(debug.frame)
433 sourceCat = matchRes.usableSourceCat,
434 matches = matchRes.matches,
438 title=
"Fit TAN-SIP WCS",
441 return pipeBase.Struct(
442 matches = matchRes.matches,
444 scatterOnSky = scatterOnSky,
449 """Create matchMeta metadata required for regenerating the catalog
451 This is copied from Astrom and I'm not sure why it is needed.
453 @param bbox bounding box of exposure (an lsst.afw.geom.Box2I or Box2D)
454 @param wcs WCS of exposure
455 @param filterName Name of filter, used for magnitudes
456 @return metadata about the field (a daf_base PropertyList)
460 ctrPos = bboxd.getCenter()
461 ctrCoord = wcs.pixelToSky(ctrPos).toIcrs()
462 llCoord = wcs.pixelToSky(bboxd.getMin())
463 approxRadius = ctrCoord.angularSeparation(llCoord)
464 matchMeta.add(
'RA', ctrCoord.getRa().asDegrees(),
'field center in degrees')
465 matchMeta.add(
'DEC', ctrCoord.getDec().asDegrees(),
'field center in degrees')
466 matchMeta.add(
'RADIUS', approxRadius.asDegrees(),
'field radius in degrees, approximate')
467 matchMeta.add(
'SMATCHV', 1,
'SourceMatchVector version number')
468 if filterName
is not None:
469 matchMeta.add(
'FILTER', filterName,
'filter name for tagalong data')
afw::math::Statistics makeMatchStatistics(std::vector< MatchT > const &matchList, int const flags, afw::math::StatisticsControl const &sctrl=afw::math::StatisticsControl())
def _matchAndFitWcs
Match sources to reference objects and fit a WCS.
Class for storing ordered metadata with comments.
def _getExposureMetadata
Extract metadata from an exposure.
def __init__
Construct an AstrometryTask.
def getDistortedWcs
Get a WCS from an exposureInfo, with distortion terms if possible.
def run
Load reference objects, match sources and optionally fit a WCS.
def _computeMatchStatsOnSky
def solve
Load reference objects overlapping an exposure, match to sources and fit a WCS.
A floating-point coordinate rectangle geometry.
Match an input source catalog with objects from a reference catalog and solve for the WCS...
def loadAndMatch
Load reference objects overlapping an exposure and match to sources detected on that exposure...