Coverage for python/lsst/meas/astrom/ref_match.py : 31%

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__ = ['RefMatchConfig', 'RefMatchTask']
25import astropy.time
27import lsst.geom
28from lsst.daf.base import DateTime
29import lsst.afw.math as afwMath
30import lsst.pex.config as pexConfig
31import lsst.pipe.base as pipeBase
32from lsst.meas.algorithms import ReferenceSourceSelectorTask
33from lsst.meas.algorithms.sourceSelector import sourceSelectorRegistry
34from .matchPessimisticB import MatchPessimisticBTask
35from .display import displayAstrometry
36from . import makeMatchStatistics
39class RefMatchConfig(pexConfig.Config):
40 matcher = pexConfig.ConfigurableField(
41 target=MatchPessimisticBTask,
42 doc="reference object/source matcher",
43 )
44 matchDistanceSigma = pexConfig.RangeField(
45 doc="the maximum match distance is set to "
46 " mean_match_distance + matchDistanceSigma*std_dev_match_distance; "
47 "ignored if not fitting a WCS",
48 dtype=float,
49 default=2,
50 min=0,
51 )
52 sourceSelector = sourceSelectorRegistry.makeField(
53 doc="How to select sources for cross-matching.",
54 default="science",
55 )
56 referenceSelector = pexConfig.ConfigurableField(
57 target=ReferenceSourceSelectorTask,
58 doc="How to select reference objects for cross-matching."
59 )
60 sourceFluxType = pexConfig.Field(
61 dtype=str,
62 doc="Source flux type to use in source selection.",
63 default='Calib'
64 )
66 def setDefaults(self):
67 self.sourceSelector.name = "science"
68 self.sourceSelector['science'].fluxLimit.fluxField = \
69 'slot_%sFlux_instFlux' % (self.sourceFluxType)
70 self.sourceSelector['science'].signalToNoise.fluxField = \
71 'slot_%sFlux_instFlux' % (self.sourceFluxType)
72 self.sourceSelector['science'].signalToNoise.errField = \
73 'slot_%sFlux_instFluxErr' % (self.sourceFluxType)
76class RefMatchTask(pipeBase.Task):
77 """Match an input source catalog with objects from a reference catalog.
79 Parameters
80 ----------
81 refObjLoader : `lsst.meas.algorithms.ReferenceLoader`
82 A reference object loader object
83 **kwargs
84 additional keyword arguments for pipe_base `lsst.pipe.base.Task`
85 """
86 ConfigClass = RefMatchConfig
87 _DefaultName = "calibrationBaseClass"
89 def __init__(self, refObjLoader, **kwargs):
90 pipeBase.Task.__init__(self, **kwargs)
91 if refObjLoader:
92 self.refObjLoader = refObjLoader
93 else:
94 self.refObjLoader = None
96 if self.config.sourceSelector.name == 'matcher':
97 if self.config.sourceSelector['matcher'].sourceFluxType != self.config.sourceFluxType:
98 raise RuntimeError("The sourceFluxType in the sourceSelector['matcher'] must match "
99 "the configured sourceFluxType")
101 self.makeSubtask("matcher")
102 self.makeSubtask("sourceSelector")
103 self.makeSubtask("referenceSelector")
105 def setRefObjLoader(self, refObjLoader):
106 """Sets the reference object loader for the task
108 Parameters
109 ----------
110 refObjLoader
111 An instance of a reference object loader task or class
112 """
113 self.refObjLoader = refObjLoader
115 @pipeBase.timeMethod
116 def loadAndMatch(self, exposure, sourceCat):
117 """Load reference objects overlapping an exposure and match to sources
118 detected on that exposure.
120 Parameters
121 ----------
122 exposure : `lsst.afw.image.Exposure`
123 exposure that the sources overlap
124 sourceCat : `lsst.afw.table.SourceCatalog.`
125 catalog of sources detected on the exposure
127 Returns
128 -------
129 result : `lsst.pipe.base.Struct`
130 Result struct with Components:
132 - ``refCat`` : reference object catalog of objects that overlap the
133 exposure (`lsst.afw.table.SimpleCatalog`)
134 - ``matches`` : Matched sources and references
135 (`list` of `lsst.afw.table.ReferenceMatch`)
136 - ``matchMeta`` : metadata needed to unpersist matches
137 (`lsst.daf.base.PropertyList`)
139 Notes
140 -----
141 ignores config.matchDistanceSigma
142 """
143 if self.refObjLoader is None:
144 raise RuntimeError("Running matcher task with no refObjLoader set in __ini__ or setRefObjLoader")
145 import lsstDebug
146 debug = lsstDebug.Info(__name__)
148 expMd = self._getExposureMetadata(exposure)
150 sourceSelection = self.sourceSelector.run(sourceCat)
152 sourceFluxField = "slot_%sFlux_instFlux" % (self.config.sourceFluxType)
154 loadRes = self.refObjLoader.loadPixelBox(
155 bbox=expMd.bbox,
156 wcs=expMd.wcs,
157 filterName=expMd.filterName,
158 photoCalib=expMd.photoCalib,
159 )
161 refSelection = self.referenceSelector.run(loadRes.refCat)
163 matchMeta = self.refObjLoader.getMetadataBox(
164 bbox=expMd.bbox,
165 wcs=expMd.wcs,
166 filterName=expMd.filterName,
167 photoCalib=expMd.photoCalib,
168 )
170 matchRes = self.matcher.matchObjectsToSources(
171 refCat=refSelection.sourceCat,
172 sourceCat=sourceSelection.sourceCat,
173 wcs=expMd.wcs,
174 sourceFluxField=sourceFluxField,
175 refFluxField=loadRes.fluxField,
176 match_tolerance=None,
177 )
179 distStats = self._computeMatchStatsOnSky(matchRes.matches)
180 self.log.info(
181 "Found %d matches with scatter = %0.3f +- %0.3f arcsec; " %
182 (len(matchRes.matches), distStats.distMean.asArcseconds(), distStats.distStdDev.asArcseconds())
183 )
185 if debug.display:
186 frame = int(debug.frame)
187 displayAstrometry(
188 refCat=refSelection.sourceCat,
189 sourceCat=sourceSelection.sourceCat,
190 matches=matchRes.matches,
191 exposure=exposure,
192 bbox=expMd.bbox,
193 frame=frame,
194 title="Matches",
195 )
197 return pipeBase.Struct(
198 refCat=loadRes.refCat,
199 refSelection=refSelection,
200 sourceSelection=sourceSelection,
201 matches=matchRes.matches,
202 matchMeta=matchMeta,
203 )
205 def _computeMatchStatsOnSky(self, matchList):
206 """Compute on-sky radial distance statistics for a match list
208 Parameters
209 ----------
210 matchList : `list` of `lsst.afw.table.ReferenceMatch`
211 list of matches between reference object and sources;
212 the distance field is the only field read and it must be set to distance in radians
214 Returns
215 -------
216 result : `lsst.pipe.base.Struct`
217 Result struct with components:
219 - ``distMean`` : clipped mean of on-sky radial separation (`float`)
220 - ``distStdDev`` : clipped standard deviation of on-sky radial
221 separation (`float`)
222 - ``maxMatchDist`` : distMean + self.config.matchDistanceSigma *
223 distStdDev (`float`)
224 """
225 distStatsInRadians = makeMatchStatistics(matchList, afwMath.MEANCLIP | afwMath.STDEVCLIP)
226 distMean = distStatsInRadians.getValue(afwMath.MEANCLIP)*lsst.geom.radians
227 distStdDev = distStatsInRadians.getValue(afwMath.STDEVCLIP)*lsst.geom.radians
228 return pipeBase.Struct(
229 distMean=distMean,
230 distStdDev=distStdDev,
231 maxMatchDist=distMean + self.config.matchDistanceSigma * distStdDev,
232 )
234 def _getExposureMetadata(self, exposure):
235 """Extract metadata from an exposure.
237 Parameters
238 ----------
239 exposure : `lsst.afw.image.Exposure`
241 Returns
242 -------
243 result : `lsst.pipe.base.Struct`
244 Result struct with components:
246 - ``bbox`` : parent bounding box (`lsst.geom.Box2I`)
247 - ``wcs`` : exposure WCS (`lsst.afw.geom.SkyWcs`)
248 - ``photoCalib`` : photometric calibration (`lsst.afw.image.PhotoCalib`)
249 - ``filterName`` : name of filter (`str`)
250 - ``epoch`` : date of exposure (`astropy.time.Time`)
252 """
253 exposureInfo = exposure.getInfo()
254 filterName = exposureInfo.getFilter().getName() or None
255 if filterName == "_unknown_":
256 filterName = None
257 epoch = None
258 if exposure.getInfo().hasVisitInfo():
259 epochTaiMjd = exposure.getInfo().getVisitInfo().getDate().get(system=DateTime.MJD,
260 scale=DateTime.TAI)
261 epoch = astropy.time.Time(epochTaiMjd, scale="tai", format="mjd")
263 return pipeBase.Struct(
264 bbox=exposure.getBBox(),
265 wcs=exposureInfo.getWcs(),
266 photoCalib=exposureInfo.getPhotoCalib(),
267 filterName=filterName,
268 epoch=epoch,
269 )