Coverage for python/lsst/meas/astrom/ref_match.py: 37%
73 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-30 10:46 +0000
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-30 10:46 +0000
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 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 epoch=expMd.epoch,
169 )
171 matchRes = self.matcher.matchObjectsToSources(
172 refCat=refSelection.sourceCat,
173 sourceCat=sourceSelection.sourceCat,
174 wcs=expMd.wcs,
175 sourceFluxField=sourceFluxField,
176 refFluxField=loadRes.fluxField,
177 match_tolerance=None,
178 )
180 distStats = self._computeMatchStatsOnSky(matchRes.matches)
181 self.log.info(
182 "Found %d matches with scatter = %0.3f +- %0.3f arcsec; ",
183 len(matchRes.matches), distStats.distMean.asArcseconds(), distStats.distStdDev.asArcseconds()
184 )
186 if debug.display:
187 frame = int(debug.frame)
188 displayAstrometry(
189 refCat=refSelection.sourceCat,
190 sourceCat=sourceSelection.sourceCat,
191 matches=matchRes.matches,
192 exposure=exposure,
193 bbox=expMd.bbox,
194 frame=frame,
195 title="Matches",
196 )
198 return pipeBase.Struct(
199 refCat=loadRes.refCat,
200 refSelection=refSelection,
201 sourceSelection=sourceSelection,
202 matches=matchRes.matches,
203 matchMeta=matchMeta,
204 )
206 def _computeMatchStatsOnSky(self, matchList):
207 """Compute on-sky radial distance statistics for a match list
209 Parameters
210 ----------
211 matchList : `list` of `lsst.afw.table.ReferenceMatch`
212 list of matches between reference object and sources;
213 the distance field is the only field read and it must be set to distance in radians
215 Returns
216 -------
217 result : `lsst.pipe.base.Struct`
218 Result struct with components:
220 - ``distMean`` : clipped mean of on-sky radial separation (`float`)
221 - ``distStdDev`` : clipped standard deviation of on-sky radial
222 separation (`float`)
223 - ``maxMatchDist`` : distMean + self.config.matchDistanceSigma *
224 distStdDev (`float`)
225 """
226 distStatsInRadians = makeMatchStatistics(matchList, afwMath.MEANCLIP | afwMath.STDEVCLIP)
227 distMean = distStatsInRadians.getValue(afwMath.MEANCLIP)*lsst.geom.radians
228 distStdDev = distStatsInRadians.getValue(afwMath.STDEVCLIP)*lsst.geom.radians
229 return pipeBase.Struct(
230 distMean=distMean,
231 distStdDev=distStdDev,
232 maxMatchDist=distMean + self.config.matchDistanceSigma * distStdDev,
233 )
235 def _getExposureMetadata(self, exposure):
236 """Extract metadata from an exposure.
238 Parameters
239 ----------
240 exposure : `lsst.afw.image.Exposure`
242 Returns
243 -------
244 result : `lsst.pipe.base.Struct`
245 Result struct with components:
247 - ``bbox`` : parent bounding box (`lsst.geom.Box2I`)
248 - ``wcs`` : exposure WCS (`lsst.afw.geom.SkyWcs`)
249 - ``photoCalib`` : photometric calibration (`lsst.afw.image.PhotoCalib`)
250 - ``filterName`` : name of filter band (`str`)
251 - ``epoch`` : date of exposure (`astropy.time.Time`)
253 """
254 exposureInfo = exposure.getInfo()
255 filterLabel = exposureInfo.getFilter()
256 filterName = filterLabel.bandLabel if filterLabel is not None else 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 )