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