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

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 epoch=expMd.epoch,
160 )
162 refSelection = self.referenceSelector.run(loadRes.refCat)
164 matchMeta = self.refObjLoader.getMetadataBox(
165 bbox=expMd.bbox,
166 wcs=expMd.wcs,
167 filterName=expMd.filterName,
168 photoCalib=expMd.photoCalib,
169 epoch=expMd.epoch,
170 )
172 matchRes = self.matcher.matchObjectsToSources(
173 refCat=refSelection.sourceCat,
174 sourceCat=sourceSelection.sourceCat,
175 wcs=expMd.wcs,
176 sourceFluxField=sourceFluxField,
177 refFluxField=loadRes.fluxField,
178 match_tolerance=None,
179 )
181 distStats = self._computeMatchStatsOnSky(matchRes.matches)
182 self.log.info(
183 "Found %d matches with scatter = %0.3f +- %0.3f arcsec; " %
184 (len(matchRes.matches), distStats.distMean.asArcseconds(), distStats.distStdDev.asArcseconds())
185 )
187 if debug.display:
188 frame = int(debug.frame)
189 displayAstrometry(
190 refCat=refSelection.sourceCat,
191 sourceCat=sourceSelection.sourceCat,
192 matches=matchRes.matches,
193 exposure=exposure,
194 bbox=expMd.bbox,
195 frame=frame,
196 title="Matches",
197 )
199 return pipeBase.Struct(
200 refCat=loadRes.refCat,
201 refSelection=refSelection,
202 sourceSelection=sourceSelection,
203 matches=matchRes.matches,
204 matchMeta=matchMeta,
205 )
207 def _computeMatchStatsOnSky(self, matchList):
208 """Compute on-sky radial distance statistics for a match list
210 Parameters
211 ----------
212 matchList : `list` of `lsst.afw.table.ReferenceMatch`
213 list of matches between reference object and sources;
214 the distance field is the only field read and it must be set to distance in radians
216 Returns
217 -------
218 result : `lsst.pipe.base.Struct`
219 Result struct with components:
221 - ``distMean`` : clipped mean of on-sky radial separation (`float`)
222 - ``distStdDev`` : clipped standard deviation of on-sky radial
223 separation (`float`)
224 - ``maxMatchDist`` : distMean + self.config.matchDistanceSigma *
225 distStdDev (`float`)
226 """
227 distStatsInRadians = makeMatchStatistics(matchList, afwMath.MEANCLIP | afwMath.STDEVCLIP)
228 distMean = distStatsInRadians.getValue(afwMath.MEANCLIP)*lsst.geom.radians
229 distStdDev = distStatsInRadians.getValue(afwMath.STDEVCLIP)*lsst.geom.radians
230 return pipeBase.Struct(
231 distMean=distMean,
232 distStdDev=distStdDev,
233 maxMatchDist=distMean + self.config.matchDistanceSigma * distStdDev,
234 )
236 def _getExposureMetadata(self, exposure):
237 """Extract metadata from an exposure.
239 Parameters
240 ----------
241 exposure : `lsst.afw.image.Exposure`
243 Returns
244 -------
245 result : `lsst.pipe.base.Struct`
246 Result struct with components:
248 - ``bbox`` : parent bounding box (`lsst.geom.Box2I`)
249 - ``wcs`` : exposure WCS (`lsst.afw.geom.SkyWcs`)
250 - ``photoCalib`` : photometric calibration (`lsst.afw.image.PhotoCalib`)
251 - ``filterName`` : name of filter band (`str`)
252 - ``epoch`` : date of exposure (`astropy.time.Time`)
254 """
255 exposureInfo = exposure.getInfo()
256 filterLabel = exposureInfo.getFilterLabel()
257 filterName = filterLabel.bandLabel if filterLabel is not None else None
258 epoch = None
259 if exposure.getInfo().hasVisitInfo():
260 epochTaiMjd = exposure.getInfo().getVisitInfo().getDate().get(system=DateTime.MJD,
261 scale=DateTime.TAI)
262 epoch = astropy.time.Time(epochTaiMjd, scale="tai", format="mjd")
264 return pipeBase.Struct(
265 bbox=exposure.getBBox(),
266 wcs=exposureInfo.getWcs(),
267 photoCalib=exposureInfo.getPhotoCalib(),
268 filterName=filterName,
269 epoch=epoch,
270 )