Coverage for python/lsst/meas/astrom/astrometry.py : 18%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#
2# LSST Data Management System
3# Copyright 2008-2016 AURA/LSST.
4#
5# This product includes software developed by the
6# LSST Project (http://www.lsst.org/).
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the LSST License Statement and
19# the GNU General Public License along with this program. If not,
20# see <https://www.lsstcorp.org/LegalNotices/>.
21#
23__all__ = ["AstrometryConfig", "AstrometryTask"]
26import lsst.pex.config as pexConfig
27import lsst.pipe.base as pipeBase
28from .ref_match import RefMatchTask, RefMatchConfig
29from .fitTanSipWcs import FitTanSipWcsTask
30from .display import displayAstrometry
33class AstrometryConfig(RefMatchConfig):
34 """Config for AstrometryTask.
35 """
36 wcsFitter = pexConfig.ConfigurableField(
37 target=FitTanSipWcsTask,
38 doc="WCS fitter",
39 )
40 forceKnownWcs = pexConfig.Field(
41 dtype=bool,
42 doc="If True then load reference objects and match sources but do not fit a WCS; "
43 "this simply controls whether 'run' calls 'solve' or 'loadAndMatch'",
44 default=False,
45 )
46 maxIter = pexConfig.RangeField(
47 doc="maximum number of iterations of match sources and fit WCS"
48 "ignored if not fitting a WCS",
49 dtype=int,
50 default=3,
51 min=1,
52 )
53 minMatchDistanceArcSec = pexConfig.RangeField(
54 doc="the match distance below which further iteration is pointless (arcsec); "
55 "ignored if not fitting a WCS",
56 dtype=float,
57 default=0.001,
58 min=0,
59 )
61 def setDefaults(self):
62 # Override the default source selector for astrometry tasks
63 self.sourceFluxType = "Ap"
65 self.sourceSelector.name = "matcher"
66 self.sourceSelector["matcher"].sourceFluxType = self.sourceFluxType
68 # Note that if the matcher is MatchOptimisticBTask, then the
69 # default should be self.sourceSelector['matcher'].excludePixelFlags = False
70 # However, there is no way to do this automatically.
73class AstrometryTask(RefMatchTask):
74 """Match an input source catalog with objects from a reference catalog and
75 solve for the WCS.
77 This task is broken into two main subasks: matching and WCS fitting which
78 are very interactive. The matching here can be considered in part a first
79 pass WCS fitter due to the fitter's sensitivity to outliers.
81 Parameters
82 ----------
83 refObjLoader : `lsst.meas.algorithms.ReferenceLoader`
84 A reference object loader object
85 schema : `lsst.afw.table.Schema`
86 Used to set "calib_astrometry_used" flag in output source catalog.
87 **kwargs
88 additional keyword arguments for pipe_base
89 `lsst.pipe.base.Task.__init__`
90 """
91 ConfigClass = AstrometryConfig
92 _DefaultName = "astrometricSolver"
94 def __init__(self, refObjLoader, schema=None, **kwargs):
95 RefMatchTask.__init__(self, refObjLoader, **kwargs)
97 if schema is not None:
98 self.usedKey = schema.addField("calib_astrometry_used", type="Flag",
99 doc="set if source was used in astrometric calibration")
100 else:
101 self.usedKey = None
103 self.makeSubtask("wcsFitter")
105 @pipeBase.timeMethod
106 def run(self, sourceCat, exposure):
107 """Load reference objects, match sources and optionally fit a WCS.
109 This is a thin layer around solve or loadAndMatch, depending on
110 config.forceKnownWcs.
112 Parameters
113 ----------
114 exposure : `lsst.afw.image.Exposure`
115 exposure whose WCS is to be fit
116 The following are read only:
118 - bbox
119 - photoCalib (may be absent)
120 - filter (may be unset)
121 - detector (if wcs is pure tangent; may be absent)
123 The following are updated:
125 - wcs (the initial value is used as an initial guess, and is
126 required)
128 sourceCat : `lsst.afw.table.SourceCatalog`
129 catalog of sources detected on the exposure
131 Returns
132 -------
133 result : `lsst.pipe.base.Struct`
134 with these fields:
136 - ``refCat`` : reference object catalog of objects that overlap the
137 exposure (with some margin) (`lsst.afw.table.SimpleCatalog`).
138 - ``matches`` : astrometric matches
139 (`list` of `lsst.afw.table.ReferenceMatch`).
140 - ``scatterOnSky`` : median on-sky separation between reference
141 objects and sources in "matches"
142 (`lsst.afw.geom.Angle`) or `None` if config.forceKnownWcs True
143 - ``matchMeta`` : metadata needed to unpersist matches
144 (`lsst.daf.base.PropertyList`)
145 """
146 if self.refObjLoader is None:
147 raise RuntimeError("Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
148 if self.config.forceKnownWcs:
149 res = self.loadAndMatch(exposure=exposure, sourceCat=sourceCat)
150 res.scatterOnSky = None
151 else:
152 res = self.solve(exposure=exposure, sourceCat=sourceCat)
153 return res
155 @pipeBase.timeMethod
156 def solve(self, exposure, sourceCat):
157 """Load reference objects overlapping an exposure, match to sources and
158 fit a WCS
160 Returns
161 -------
162 result : `lsst.pipe.base.Struct`
163 Result struct with components:
165 - ``refCat`` : reference object catalog of objects that overlap the
166 exposure (with some margin) (`lsst::afw::table::SimpleCatalog`).
167 - ``matches`` : astrometric matches
168 (`list` of `lsst.afw.table.ReferenceMatch`).
169 - ``scatterOnSky`` : median on-sky separation between reference
170 objects and sources in "matches" (`lsst.geom.Angle`)
171 - ``matchMeta`` : metadata needed to unpersist matches
172 (`lsst.daf.base.PropertyList`)
174 Notes
175 -----
176 ignores config.forceKnownWcs
177 """
178 if self.refObjLoader is None:
179 raise RuntimeError("Running matcher task with no refObjLoader set in __init__ or setRefObjLoader")
180 import lsstDebug
181 debug = lsstDebug.Info(__name__)
183 expMd = self._getExposureMetadata(exposure)
185 sourceSelection = self.sourceSelector.run(sourceCat)
187 self.log.info("Purged %d sources, leaving %d good sources" %
188 (len(sourceCat) - len(sourceSelection.sourceCat),
189 len(sourceSelection.sourceCat)))
191 loadRes = self.refObjLoader.loadPixelBox(
192 bbox=expMd.bbox,
193 wcs=expMd.wcs,
194 filterName=expMd.filterName,
195 photoCalib=expMd.photoCalib,
196 epoch=expMd.epoch,
197 )
199 refSelection = self.referenceSelector.run(loadRes.refCat)
201 matchMeta = self.refObjLoader.getMetadataBox(
202 bbox=expMd.bbox,
203 wcs=expMd.wcs,
204 filterName=expMd.filterName,
205 photoCalib=expMd.photoCalib,
206 epoch=expMd.epoch,
207 )
209 if debug.display:
210 frame = int(debug.frame)
211 displayAstrometry(
212 refCat=refSelection.sourceCat,
213 sourceCat=sourceSelection.sourceCat,
214 exposure=exposure,
215 bbox=expMd.bbox,
216 frame=frame,
217 title="Reference catalog",
218 )
220 res = None
221 wcs = expMd.wcs
222 match_tolerance = None
223 for i in range(self.config.maxIter):
224 iterNum = i + 1
225 try:
226 tryRes = self._matchAndFitWcs(
227 refCat=refSelection.sourceCat,
228 sourceCat=sourceCat,
229 goodSourceCat=sourceSelection.sourceCat,
230 refFluxField=loadRes.fluxField,
231 bbox=expMd.bbox,
232 wcs=wcs,
233 exposure=exposure,
234 match_tolerance=match_tolerance,
235 )
236 except Exception as e:
237 # if we have had a succeessful iteration then use that; otherwise fail
238 if i > 0:
239 self.log.info("Fit WCS iter %d failed; using previous iteration: %s" % (iterNum, e))
240 iterNum -= 1
241 break
242 else:
243 raise
245 match_tolerance = tryRes.match_tolerance
246 tryMatchDist = self._computeMatchStatsOnSky(tryRes.matches)
247 self.log.debug(
248 "Match and fit WCS iteration %d: found %d matches with scatter = %0.3f +- %0.3f arcsec; "
249 "max match distance = %0.3f arcsec",
250 iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
251 tryMatchDist.distStdDev.asArcseconds(), tryMatchDist.maxMatchDist.asArcseconds())
253 maxMatchDist = tryMatchDist.maxMatchDist
254 res = tryRes
255 wcs = res.wcs
256 if maxMatchDist.asArcseconds() < self.config.minMatchDistanceArcSec:
257 self.log.debug(
258 "Max match distance = %0.3f arcsec < %0.3f = config.minMatchDistanceArcSec; "
259 "that's good enough",
260 maxMatchDist.asArcseconds(), self.config.minMatchDistanceArcSec)
261 break
262 match_tolerance.maxMatchDist = maxMatchDist
264 self.log.info(
265 "Matched and fit WCS in %d iterations; "
266 "found %d matches with scatter = %0.3f +- %0.3f arcsec" %
267 (iterNum, len(tryRes.matches), tryMatchDist.distMean.asArcseconds(),
268 tryMatchDist.distStdDev.asArcseconds()))
269 for m in res.matches:
270 if self.usedKey:
271 m.second.set(self.usedKey, True)
272 exposure.setWcs(res.wcs)
274 return pipeBase.Struct(
275 refCat=refSelection.sourceCat,
276 matches=res.matches,
277 scatterOnSky=res.scatterOnSky,
278 matchMeta=matchMeta,
279 )
281 @pipeBase.timeMethod
282 def _matchAndFitWcs(self, refCat, sourceCat, goodSourceCat, refFluxField, bbox, wcs, match_tolerance,
283 exposure=None):
284 """Match sources to reference objects and fit a WCS.
286 Parameters
287 ----------
288 refCat : `lsst.afw.table.SimpleCatalog`
289 catalog of reference objects
290 sourceCat : `lsst.afw.table.SourceCatalog`
291 catalog of sources detected on the exposure
292 goodSourceCat : `lsst.afw.table.SourceCatalog`
293 catalog of down-selected good sources detected on the exposure
294 refFluxField : 'str'
295 field of refCat to use for flux
296 bbox : `lsst.geom.Box2I`
297 bounding box of exposure
298 wcs : `lsst.afw.geom.SkyWcs`
299 initial guess for WCS of exposure
300 match_tolerance : `lsst.meas.astrom.MatchTolerance`
301 a MatchTolerance object (or None) specifying
302 internal tolerances to the matcher. See the MatchTolerance
303 definition in the respective matcher for the class definition.
304 exposure : `lsst.afw.image.Exposure`
305 exposure whose WCS is to be fit, or None; used only for the debug
306 display.
308 Returns
309 -------
310 result : `lsst.pipe.base.Struct`
311 Result struct with components:
313 - ``matches``: astrometric matches
314 (`list` of `lsst.afw.table.ReferenceMatch`).
315 - ``wcs``: the fit WCS (lsst.afw.geom.SkyWcs).
316 - ``scatterOnSky`` : median on-sky separation between reference
317 objects and sources in "matches" (`lsst.afw.geom.Angle`).
318 """
319 import lsstDebug
320 debug = lsstDebug.Info(__name__)
322 sourceFluxField = "slot_%sFlux_instFlux" % (self.config.sourceFluxType)
324 matchRes = self.matcher.matchObjectsToSources(
325 refCat=refCat,
326 sourceCat=goodSourceCat,
327 wcs=wcs,
328 sourceFluxField=sourceFluxField,
329 refFluxField=refFluxField,
330 match_tolerance=match_tolerance,
331 )
332 self.log.debug("Found %s matches", len(matchRes.matches))
333 if debug.display:
334 frame = int(debug.frame)
335 displayAstrometry(
336 refCat=refCat,
337 sourceCat=matchRes.usableSourceCat,
338 matches=matchRes.matches,
339 exposure=exposure,
340 bbox=bbox,
341 frame=frame + 1,
342 title="Initial WCS",
343 )
345 self.log.debug("Fitting WCS")
346 fitRes = self.wcsFitter.fitWcs(
347 matches=matchRes.matches,
348 initWcs=wcs,
349 bbox=bbox,
350 refCat=refCat,
351 sourceCat=sourceCat,
352 exposure=exposure,
353 )
354 fitWcs = fitRes.wcs
355 scatterOnSky = fitRes.scatterOnSky
356 if debug.display:
357 frame = int(debug.frame)
358 displayAstrometry(
359 refCat=refCat,
360 sourceCat=matchRes.usableSourceCat,
361 matches=matchRes.matches,
362 exposure=exposure,
363 bbox=bbox,
364 frame=frame + 2,
365 title="Fit TAN-SIP WCS",
366 )
368 return pipeBase.Struct(
369 matches=matchRes.matches,
370 wcs=fitWcs,
371 scatterOnSky=scatterOnSky,
372 match_tolerance=matchRes.match_tolerance,
373 )