Coverage for python / lsst / meas / astrom / ref_match.py: 35%
64 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-24 08:29 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-24 08:29 +0000
1# This file is part of meas_astrom.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22__all__ = ['RefMatchConfig', 'RefMatchTask']
24import lsst.geom
25import lsst.afw.math as afwMath
26import lsst.pex.config as pexConfig
27import lsst.pipe.base as pipeBase
28from lsst.meas.algorithms import ReferenceSourceSelectorTask
29from lsst.meas.algorithms.sourceSelector import sourceSelectorRegistry
30from lsst.utils.timer import timeMethod
31from .matchPessimisticB import MatchPessimisticBTask
32from .display import displayAstrometry
33from . import makeMatchStatistics
36class RefMatchConfig(pexConfig.Config):
37 matcher = pexConfig.ConfigurableField(
38 target=MatchPessimisticBTask,
39 doc="reference object/source matcher",
40 )
41 matchDistanceSigma = pexConfig.RangeField(
42 doc="the maximum match distance is set to "
43 " mean_match_distance + matchDistanceSigma*std_dev_match_distance; "
44 "ignored if not fitting a WCS",
45 dtype=float,
46 default=2,
47 min=0,
48 )
49 sourceSelector = sourceSelectorRegistry.makeField(
50 doc="How to select sources for cross-matching.",
51 default="science",
52 )
53 referenceSelector = pexConfig.ConfigurableField(
54 target=ReferenceSourceSelectorTask,
55 doc="How to select reference objects for cross-matching."
56 )
57 sourceFluxType = pexConfig.Field(
58 dtype=str,
59 doc="Source flux type to use in source selection.",
60 default='Psf'
61 )
63 def setDefaults(self):
64 # Configured to match the deprecated "matcher" selector:
65 # SN > 40, some bad flags, valid centroids.
66 self.sourceSelector["science"].doSignalToNoise = True
67 self.sourceSelector["science"].signalToNoise.minimum = 40
68 self.sourceSelector["science"].signalToNoise.fluxField = f"slot_{self.sourceFluxType}Flux_instFlux"
69 self.sourceSelector["science"].signalToNoise.errField = f"slot_{self.sourceFluxType}Flux_instFluxErr"
70 self.sourceSelector["science"].doFlags = True
71 self.sourceSelector["science"].flags.bad = ["base_PixelFlags_flag_edge",
72 "base_PixelFlags_flag_nodata",
73 "base_PixelFlags_flag_interpolatedCenter",
74 "base_PixelFlags_flag_saturated",
75 "base_SdssCentroid_flag",
76 ]
79class RefMatchTask(pipeBase.Task):
80 """Match an input source catalog with objects from a reference catalog.
82 Parameters
83 ----------
84 refObjLoader : `lsst.meas.algorithms.ReferenceLoader`
85 A reference object loader object; gen3 pipeline tasks will pass `None`
86 and call `setRefObjLoader` in `runQuantum`.
87 **kwargs
88 Additional keyword arguments for pipe_base `lsst.pipe.base.Task`.
89 """
90 ConfigClass = RefMatchConfig
91 _DefaultName = "calibrationBaseClass"
93 def __init__(self, refObjLoader=None, **kwargs):
94 pipeBase.Task.__init__(self, **kwargs)
95 if refObjLoader:
96 self.refObjLoader = refObjLoader
97 else:
98 self.refObjLoader = None
100 if self.config.sourceSelector.name == 'matcher':
101 if self.config.sourceSelector['matcher'].sourceFluxType != self.config.sourceFluxType:
102 raise RuntimeError("The sourceFluxType in the sourceSelector['matcher'] must match "
103 "the configured sourceFluxType")
105 self.makeSubtask("matcher")
106 self.makeSubtask("sourceSelector")
107 self.makeSubtask("referenceSelector")
109 def setRefObjLoader(self, refObjLoader):
110 """Sets the reference object loader for the task.
112 Parameters
113 ----------
114 refObjLoader
115 An instance of a reference object loader task or class.
116 """
117 self.refObjLoader = refObjLoader
119 @timeMethod
120 def loadAndMatch(self, exposure, sourceCat):
121 """Load reference objects overlapping an exposure and match to sources
122 detected on that exposure.
124 Parameters
125 ----------
126 exposure : `lsst.afw.image.Exposure`
127 exposure that the sources overlap
128 sourceCat : `lsst.afw.table.SourceCatalog.`
129 catalog of sources detected on the exposure
131 Returns
132 -------
133 result : `lsst.pipe.base.Struct`
134 Result struct with Components:
136 - ``refCat`` : reference object catalog of objects that overlap the
137 exposure (`lsst.afw.table.SimpleCatalog`)
138 - ``matches`` : Matched sources and references
139 (`list` of `lsst.afw.table.ReferenceMatch`)
140 - ``matchMeta`` : metadata needed to unpersist matches
141 (`lsst.daf.base.PropertyList`)
143 Notes
144 -----
145 ignores config.matchDistanceSigma
146 """
147 if self.refObjLoader is None:
148 raise RuntimeError("Running matcher task with no refObjLoader set in __ini__ or setRefObjLoader")
149 import lsstDebug
150 debug = lsstDebug.Info(__name__)
152 epoch = exposure.visitInfo.date.toAstropy()
154 sourceSelection = self.sourceSelector.run(sourceCat)
156 sourceFluxField = "slot_%sFlux_instFlux" % (self.config.sourceFluxType)
158 loadRes = self.refObjLoader.loadPixelBox(
159 bbox=exposure.getBBox(),
160 wcs=exposure.wcs,
161 filterName=exposure.filter.bandLabel,
162 epoch=epoch,
163 )
165 refSelection = self.referenceSelector.run(loadRes.refCat, exposure=exposure)
167 matchMeta = self.refObjLoader.getMetadataBox(
168 bbox=exposure.getBBox(),
169 wcs=exposure.wcs,
170 filterName=exposure.filter.bandLabel,
171 epoch=epoch,
172 )
174 matchRes = self.matcher.matchObjectsToSources(
175 refCat=refSelection.sourceCat,
176 sourceCat=sourceSelection.sourceCat,
177 wcs=exposure.wcs,
178 sourceFluxField=sourceFluxField,
179 refFluxField=loadRes.fluxField,
180 matchTolerance=None,
181 bbox=exposure.getBBox(),
182 )
184 distStats = self._computeMatchStatsOnSky(matchRes.matches)
185 self.log.info(
186 "Found %d matches with scatter = %0.3f +- %0.3f arcsec; ",
187 len(matchRes.matches), distStats.distMean.asArcseconds(), distStats.distStdDev.asArcseconds()
188 )
190 if debug.display:
191 frame = int(debug.frame)
192 displayAstrometry(
193 refCat=refSelection.sourceCat,
194 sourceCat=sourceSelection.sourceCat,
195 matches=matchRes.matches,
196 exposure=exposure,
197 bbox=exposure.getBBox(),
198 frame=frame,
199 title="Matches",
200 )
202 return pipeBase.Struct(
203 refCat=loadRes.refCat,
204 refSelection=refSelection,
205 sourceSelection=sourceSelection,
206 matches=matchRes.matches,
207 matchMeta=matchMeta,
208 )
210 def _computeMatchStatsOnSky(self, matchList):
211 """Compute on-sky radial distance statistics for a match list
213 Parameters
214 ----------
215 matchList : `list` of `lsst.afw.table.ReferenceMatch`
216 list of matches between reference object and sources;
217 the distance field is the only field read and it must be set to distance in radians
219 Returns
220 -------
221 result : `lsst.pipe.base.Struct`
222 Result struct with components:
224 - ``distMean`` : clipped mean of on-sky radial separation (`float`)
225 - ``distStdDev`` : clipped standard deviation of on-sky radial
226 separation (`float`)
227 - ``maxMatchDist`` : distMean + self.config.matchDistanceSigma *
228 distStdDev (`float`)
229 """
230 distStatsInRadians = makeMatchStatistics(matchList, afwMath.MEANCLIP | afwMath.STDEVCLIP)
231 distMean = distStatsInRadians.getValue(afwMath.MEANCLIP)*lsst.geom.radians
232 distStdDev = distStatsInRadians.getValue(afwMath.STDEVCLIP)*lsst.geom.radians
233 return pipeBase.Struct(
234 distMean=distMean,
235 distStdDev=distStdDev,
236 maxMatchDist=distMean + self.config.matchDistanceSigma * distStdDev,
237 )