LSSTApplications  18.1.0
LSSTDataManagementBasePackage
ref_match.py
Go to the documentation of this file.
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 #
22 
23 __all__ = ['RefMatchConfig', 'RefMatchTask']
24 
25 import astropy.time
26 
27 import lsst.geom
28 from lsst.daf.base import DateTime
29 import lsst.afw.math as afwMath
30 import lsst.pex.config as pexConfig
31 import lsst.pipe.base as pipeBase
32 from lsst.meas.algorithms import ReferenceSourceSelectorTask
33 from lsst.meas.algorithms.sourceSelector import sourceSelectorRegistry
34 from .matchPessimisticB import MatchPessimisticBTask
35 from .display import displayAstrometry
36 from . import makeMatchStatistics
37 
38 
39 class 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  )
65 
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)
74 
75 
76 class RefMatchTask(pipeBase.Task):
77  """Match an input source catalog with objects from a reference catalog.
78 
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"
88 
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
95 
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")
100 
101  self.makeSubtask("matcher")
102  self.makeSubtask("sourceSelector")
103  self.makeSubtask("referenceSelector")
104 
105  def setRefObjLoader(self, refObjLoader):
106  """Sets the reference object loader for the task
107 
108  Parameters
109  ----------
110  refObjLoader
111  An instance of a reference object loader task or class
112  """
113  self.refObjLoader = refObjLoader
114 
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.
119 
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
126 
127  Returns
128  -------
129  result : `lsst.pipe.base.Struct`
130  Result struct with Components:
131 
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`)
138 
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__)
147 
148  expMd = self._getExposureMetadata(exposure)
149 
150  sourceSelection = self.sourceSelector.run(sourceCat)
151 
152  sourceFluxField = "slot_%sFlux_instFlux" % (self.config.sourceFluxType)
153 
154  loadRes = self.refObjLoader.loadPixelBox(
155  bbox=expMd.bbox,
156  wcs=expMd.wcs,
157  filterName=expMd.filterName,
158  photoCalib=expMd.photoCalib,
159  )
160 
161  refSelection = self.referenceSelector.run(loadRes.refCat)
162 
163  matchMeta = self.refObjLoader.getMetadataBox(
164  bbox=expMd.bbox,
165  wcs=expMd.wcs,
166  filterName=expMd.filterName,
167  photoCalib=expMd.photoCalib,
168  )
169 
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  )
178 
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  )
184 
185  if debug.display:
186  frame = int(debug.frame)
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  )
196 
197  return pipeBase.Struct(
198  refCat=loadRes.refCat,
199  refSelection=refSelection,
200  sourceSelection=sourceSelection,
201  matches=matchRes.matches,
202  matchMeta=matchMeta,
203  )
204 
205  def _computeMatchStatsOnSky(self, matchList):
206  """Compute on-sky radial distance statistics for a match list
207 
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
213 
214  Returns
215  -------
216  result : `lsst.pipe.base.Struct`
217  Result struct with components:
218 
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  )
233 
234  def _getExposureMetadata(self, exposure):
235  """Extract metadata from an exposure.
236 
237  Parameters
238  ----------
239  exposure : `lsst.afw.image.Exposure`
240 
241  Returns
242  -------
243  result : `lsst.pipe.base.Struct`
244  Result struct with components:
245 
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`)
251 
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")
262 
263  return pipeBase.Struct(
264  bbox=exposure.getBBox(),
265  wcs=exposureInfo.getWcs(),
266  photoCalib=exposureInfo.getPhotoCalib(),
267  filterName=filterName,
268  epoch=epoch,
269  )
def _computeMatchStatsOnSky(self, matchList)
Definition: ref_match.py:205
def _getExposureMetadata(self, exposure)
Definition: ref_match.py:234
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations...
def displayAstrometry(refCat=None, sourceCat=None, distortedCentroidKey=None, bbox=None, exposure=None, matches=None, frame=1, title="", pause=True)
Definition: display.py:34
def setRefObjLoader(self, refObjLoader)
Definition: ref_match.py:105
def loadAndMatch(self, exposure, sourceCat)
Definition: ref_match.py:116
def __init__(self, refObjLoader, kwargs)
Definition: ref_match.py:89