LSST Applications  21.0.0-147-g0e635eb1+1acddb5be5,22.0.0+052faf71bd,22.0.0+1ea9a8b2b2,22.0.0+6312710a6c,22.0.0+729191ecac,22.0.0+7589c3a021,22.0.0+9f079a9461,22.0.1-1-g7d6de66+b8044ec9de,22.0.1-1-g87000a6+536b1ee016,22.0.1-1-g8e32f31+6312710a6c,22.0.1-10-gd060f87+016f7cdc03,22.0.1-12-g9c3108e+df145f6f68,22.0.1-16-g314fa6d+c825727ab8,22.0.1-19-g93a5c75+d23f2fb6d8,22.0.1-19-gb93eaa13+aab3ef7709,22.0.1-2-g8ef0a89+b8044ec9de,22.0.1-2-g92698f7+9f079a9461,22.0.1-2-ga9b0f51+052faf71bd,22.0.1-2-gac51dbf+052faf71bd,22.0.1-2-gb66926d+6312710a6c,22.0.1-2-gcb770ba+09e3807989,22.0.1-20-g32debb5+b8044ec9de,22.0.1-23-gc2439a9a+fb0756638e,22.0.1-3-g496fd5d+09117f784f,22.0.1-3-g59f966b+1e6ba2c031,22.0.1-3-g849a1b8+f8b568069f,22.0.1-3-gaaec9c0+c5c846a8b1,22.0.1-32-g5ddfab5d3+60ce4897b0,22.0.1-4-g037fbe1+64e601228d,22.0.1-4-g8623105+b8044ec9de,22.0.1-5-g096abc9+d18c45d440,22.0.1-5-g15c806e+57f5c03693,22.0.1-7-gba73697+57f5c03693,master-g6e05de7fdc+c1283a92b8,master-g72cdda8301+729191ecac,w.2021.39
LSST Data Management Base Package
selectImages.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008, 2009, 2010 LSST Corporation.
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 <http://www.lsstcorp.org/LegalNotices/>.
21 #
22 import numpy as np
23 import lsst.sphgeom
24 import lsst.utils as utils
25 import lsst.pex.config as pexConfig
26 import lsst.pex.exceptions as pexExceptions
27 import lsst.geom as geom
28 import lsst.pipe.base as pipeBase
29 from lsst.skymap import BaseSkyMap
30 from lsst.daf.base import DateTime
31 
32 __all__ = ["BaseSelectImagesTask", "BaseExposureInfo", "WcsSelectImagesTask", "PsfWcsSelectImagesTask",
33  "DatabaseSelectImagesConfig", "BestSeeingWcsSelectImagesTask", "BestSeeingSelectVisitsTask",
34  "BestSeeingQuantileSelectVisitsTask"]
35 
36 
37 class DatabaseSelectImagesConfig(pexConfig.Config):
38  """Base configuration for subclasses of BaseSelectImagesTask that use a database"""
39  host = pexConfig.Field(
40  doc="Database server host name",
41  dtype=str,
42  )
43  port = pexConfig.Field(
44  doc="Database server port",
45  dtype=int,
46  )
47  database = pexConfig.Field(
48  doc="Name of database",
49  dtype=str,
50  )
51  maxExposures = pexConfig.Field(
52  doc="maximum exposures to select; intended for debugging; ignored if None",
53  dtype=int,
54  optional=True,
55  )
56 
57 
58 class BaseExposureInfo(pipeBase.Struct):
59  """Data about a selected exposure
60  """
61 
62  def __init__(self, dataId, coordList):
63  """Create exposure information that can be used to generate data references
64 
65  The object has the following fields:
66  - dataId: data ID of exposure (a dict)
67  - coordList: ICRS coordinates of the corners of the exposure (list of lsst.geom.SpherePoint)
68  plus any others items that are desired
69  """
70  super(BaseExposureInfo, self).__init__(dataId=dataId, coordList=coordList)
71 
72 
73 class BaseSelectImagesTask(pipeBase.Task):
74  """Base task for selecting images suitable for coaddition
75  """
76  ConfigClass = pexConfig.Config
77  _DefaultName = "selectImages"
78 
79  @pipeBase.timeMethod
80  def run(self, coordList):
81  """Select images suitable for coaddition in a particular region
82 
83  @param[in] coordList: list of coordinates defining region of interest; if None then select all images
84  subclasses may add additional keyword arguments, as required
85 
86  @return a pipeBase Struct containing:
87  - exposureInfoList: a list of exposure information objects (subclasses of BaseExposureInfo),
88  which have at least the following fields:
89  - dataId: data ID dictionary
90  - coordList: ICRS coordinates of the corners of the exposure (list of lsst.geom.SpherePoint)
91  """
92  raise NotImplementedError()
93 
94  def _runArgDictFromDataId(self, dataId):
95  """Extract keyword arguments for run (other than coordList) from a data ID
96 
97  @return keyword arguments for run (other than coordList), as a dict
98  """
99  raise NotImplementedError()
100 
101  def runDataRef(self, dataRef, coordList, makeDataRefList=True, selectDataList=[]):
102  """Run based on a data reference
103 
104  This delegates to run() and _runArgDictFromDataId() to do the actual
105  selection. In the event that the selectDataList is non-empty, this will
106  be used to further restrict the selection, providing the user with
107  additional control over the selection.
108 
109  @param[in] dataRef: data reference; must contain any extra keys needed by the subclass
110  @param[in] coordList: list of coordinates defining region of interest; if None, search the whole sky
111  @param[in] makeDataRefList: if True, return dataRefList
112  @param[in] selectDataList: List of SelectStruct with dataRefs to consider for selection
113  @return a pipeBase Struct containing:
114  - exposureInfoList: a list of objects derived from ExposureInfo
115  - dataRefList: a list of data references (None if makeDataRefList False)
116  """
117  runArgDict = self._runArgDictFromDataId_runArgDictFromDataId(dataRef.dataId)
118  exposureInfoList = self.runrun(coordList, **runArgDict).exposureInfoList
119 
120  if len(selectDataList) > 0 and len(exposureInfoList) > 0:
121  # Restrict the exposure selection further
122  ccdKeys, ccdValues = _extractKeyValue(exposureInfoList)
123  inKeys, inValues = _extractKeyValue([s.dataRef for s in selectDataList], keys=ccdKeys)
124  inValues = set(inValues)
125  newExposureInfoList = []
126  for info, ccdVal in zip(exposureInfoList, ccdValues):
127  if ccdVal in inValues:
128  newExposureInfoList.append(info)
129  else:
130  self.log.info("De-selecting exposure %s: not in selectDataList", info.dataId)
131  exposureInfoList = newExposureInfoList
132 
133  if makeDataRefList:
134  butler = dataRef.butlerSubset.butler
135  dataRefList = [butler.dataRef(datasetType="calexp",
136  dataId=expInfo.dataId,
137  ) for expInfo in exposureInfoList]
138  else:
139  dataRefList = None
140 
141  return pipeBase.Struct(
142  dataRefList=dataRefList,
143  exposureInfoList=exposureInfoList,
144  )
145 
146 
147 def _extractKeyValue(dataList, keys=None):
148  """Extract the keys and values from a list of dataIds
149 
150  The input dataList is a list of objects that have 'dataId' members.
151  This allows it to be used for both a list of data references and a
152  list of ExposureInfo
153  """
154  assert len(dataList) > 0
155  if keys is None:
156  keys = sorted(dataList[0].dataId.keys())
157  keySet = set(keys)
158  values = list()
159  for data in dataList:
160  thisKeys = set(data.dataId.keys())
161  if thisKeys != keySet:
162  raise RuntimeError("DataId keys inconsistent: %s vs %s" % (keySet, thisKeys))
163  values.append(tuple(data.dataId[k] for k in keys))
164  return keys, values
165 
166 
167 class SelectStruct(pipeBase.Struct):
168  """A container for data to be passed to the WcsSelectImagesTask"""
169 
170  def __init__(self, dataRef, wcs, bbox):
171  super(SelectStruct, self).__init__(dataRef=dataRef, wcs=wcs, bbox=bbox)
172 
173 
175  """Select images using their Wcs
176 
177  We use the "convexHull" method of lsst.sphgeom.ConvexPolygon to define
178  polygons on the celestial sphere, and test the polygon of the
179  patch for overlap with the polygon of the image.
180 
181  We use "convexHull" instead of generating a ConvexPolygon
182  directly because the standard for the inputs to ConvexPolygon
183  are pretty high and we don't want to be responsible for reaching them.
184  """
185 
186  def runDataRef(self, dataRef, coordList, makeDataRefList=True, selectDataList=[]):
187  """Select images in the selectDataList that overlap the patch
188 
189  This method is the old entry point for the Gen2 commandline tasks and drivers
190  Will be deprecated in v22.
191 
192  @param dataRef: Data reference for coadd/tempExp (with tract, patch)
193  @param coordList: List of ICRS coordinates (lsst.geom.SpherePoint) specifying boundary of patch
194  @param makeDataRefList: Construct a list of data references?
195  @param selectDataList: List of SelectStruct, to consider for selection
196  """
197  dataRefList = []
198  exposureInfoList = []
199 
200  patchVertices = [coord.getVector() for coord in coordList]
201  patchPoly = lsst.sphgeom.ConvexPolygon.convexHull(patchVertices)
202 
203  for data in selectDataList:
204  dataRef = data.dataRef
205  imageWcs = data.wcs
206  imageBox = data.bbox
207 
208  imageCorners = self.getValidImageCornersgetValidImageCorners(imageWcs, imageBox, patchPoly, dataId=None)
209  if imageCorners:
210  dataRefList.append(dataRef)
211  exposureInfoList.append(BaseExposureInfo(dataRef.dataId, imageCorners))
212 
213  return pipeBase.Struct(
214  dataRefList=dataRefList if makeDataRefList else None,
215  exposureInfoList=exposureInfoList,
216  )
217 
218  def run(self, wcsList, bboxList, coordList, dataIds=None, **kwargs):
219  """Return indices of provided lists that meet the selection criteria
220 
221  Parameters:
222  -----------
223  wcsList : `list` of `lsst.afw.geom.SkyWcs`
224  specifying the WCS's of the input ccds to be selected
225  bboxList : `list` of `lsst.geom.Box2I`
226  specifying the bounding boxes of the input ccds to be selected
227  coordList : `list` of `lsst.geom.SpherePoint`
228  ICRS coordinates specifying boundary of the patch.
229 
230  Returns:
231  --------
232  result: `list` of `int`
233  of indices of selected ccds
234  """
235  if dataIds is None:
236  dataIds = [None] * len(wcsList)
237  patchVertices = [coord.getVector() for coord in coordList]
238  patchPoly = lsst.sphgeom.ConvexPolygon.convexHull(patchVertices)
239  result = []
240  for i, (imageWcs, imageBox, dataId) in enumerate(zip(wcsList, bboxList, dataIds)):
241  imageCorners = self.getValidImageCornersgetValidImageCorners(imageWcs, imageBox, patchPoly, dataId)
242  if imageCorners:
243  result.append(i)
244  return result
245 
246  def getValidImageCorners(self, imageWcs, imageBox, patchPoly, dataId=None):
247  "Return corners or None if bad"
248  try:
249  imageCorners = [imageWcs.pixelToSky(pix) for pix in geom.Box2D(imageBox).getCorners()]
251  # Protecting ourselves from awful Wcs solutions in input images
252  self.log.debug("WCS error in testing calexp %s (%s): deselecting", dataId, e)
253  return
254 
255  imagePoly = lsst.sphgeom.ConvexPolygon.convexHull([coord.getVector() for coord in imageCorners])
256  if imagePoly is None:
257  self.log.debug("Unable to create polygon from image %s: deselecting", dataId)
258  return
259 
260  if patchPoly.intersects(imagePoly):
261  # "intersects" also covers "contains" or "is contained by"
262  self.log.info("Selecting calexp %s", dataId)
263  return imageCorners
264 
265 
266 def sigmaMad(array):
267  "Return median absolute deviation scaled to normally distributed data"
268  return 1.4826*np.median(np.abs(array - np.median(array)))
269 
270 
271 class PsfWcsSelectImagesConnections(pipeBase.PipelineTaskConnections,
272  dimensions=("tract", "patch", "skymap", "instrument", "visit"),
273  defaultTemplates={"coaddName": "deep"}):
274  pass
275 
276 
277 class PsfWcsSelectImagesConfig(pipeBase.PipelineTaskConfig,
278  pipelineConnections=PsfWcsSelectImagesConnections):
279  maxEllipResidual = pexConfig.Field(
280  doc="Maximum median ellipticity residual",
281  dtype=float,
282  default=0.007,
283  optional=True,
284  )
285  maxSizeScatter = pexConfig.Field(
286  doc="Maximum scatter in the size residuals",
287  dtype=float,
288  optional=True,
289  )
290  maxScaledSizeScatter = pexConfig.Field(
291  doc="Maximum scatter in the size residuals, scaled by the median size",
292  dtype=float,
293  default=0.009,
294  optional=True,
295  )
296  starSelection = pexConfig.Field(
297  doc="select star with this field",
298  dtype=str,
299  default='calib_psf_used'
300  )
301  starShape = pexConfig.Field(
302  doc="name of star shape",
303  dtype=str,
304  default='base_SdssShape'
305  )
306  psfShape = pexConfig.Field(
307  doc="name of psf shape",
308  dtype=str,
309  default='base_SdssShape_psf'
310  )
311 
312 
313 class PsfWcsSelectImagesTask(WcsSelectImagesTask):
314  """Select images using their Wcs and cuts on the PSF properties
315 
316  The PSF quality criteria are based on the size and ellipticity residuals from the
317  adaptive second moments of the star and the PSF.
318 
319  The criteria are:
320  - the median of the ellipticty residuals
321  - the robust scatter of the size residuals (using the median absolute deviation)
322  - the robust scatter of the size residuals scaled by the square of
323  the median size
324  """
325 
326  ConfigClass = PsfWcsSelectImagesConfig
327  _DefaultName = "PsfWcsSelectImages"
328 
329  def runDataRef(self, dataRef, coordList, makeDataRefList=True, selectDataList=[]):
330  """Select images in the selectDataList that overlap the patch and satisfy PSF quality critera.
331 
332  This method is the old entry point for the Gen2 commandline tasks and drivers
333  Will be deprecated in v22.
334 
335  @param dataRef: Data reference for coadd/tempExp (with tract, patch)
336  @param coordList: List of ICRS coordinates (lsst.geom.SpherePoint) specifying boundary of patch
337  @param makeDataRefList: Construct a list of data references?
338  @param selectDataList: List of SelectStruct, to consider for selection
339  """
340  result = super(PsfWcsSelectImagesTask, self).runDataRef(dataRef, coordList, makeDataRefList,
341  selectDataList)
342 
343  dataRefList = []
344  exposureInfoList = []
345  for dataRef, exposureInfo in zip(result.dataRefList, result.exposureInfoList):
346  butler = dataRef.butlerSubset.butler
347  srcCatalog = butler.get('src', dataRef.dataId)
348  valid = self.isValid(srcCatalog, dataRef.dataId)
349  if valid is False:
350  continue
351 
352  dataRefList.append(dataRef)
353  exposureInfoList.append(exposureInfo)
354 
355  return pipeBase.Struct(
356  dataRefList=dataRefList,
357  exposureInfoList=exposureInfoList,
358  )
359 
360  def run(self, wcsList, bboxList, coordList, srcList, dataIds=None, **kwargs):
361  """Return indices of provided lists that meet the selection criteria
362 
363  Parameters:
364  -----------
365  wcsList : `list` of `lsst.afw.geom.SkyWcs`
366  specifying the WCS's of the input ccds to be selected
367  bboxList : `list` of `lsst.geom.Box2I`
368  specifying the bounding boxes of the input ccds to be selected
369  coordList : `list` of `lsst.geom.SpherePoint`
370  ICRS coordinates specifying boundary of the patch.
371  srcList : `list` of `lsst.afw.table.SourceCatalog`
372  containing the PSF shape information for the input ccds to be selected
373 
374  Returns:
375  --------
376  goodPsf: `list` of `int`
377  of indices of selected ccds
378  """
379  goodWcs = super(PsfWcsSelectImagesTask, self).run(wcsList=wcsList, bboxList=bboxList,
380  coordList=coordList, dataIds=dataIds)
381 
382  goodPsf = []
383  if dataIds is None:
384  dataIds = [None] * len(srcList)
385  for i, (srcCatalog, dataId) in enumerate(zip(srcList, dataIds)):
386  if i not in goodWcs:
387  continue
388  if self.isValid(srcCatalog, dataId):
389  goodPsf.append(i)
390 
391  return goodPsf
392 
393  def isValid(self, srcCatalog, dataId=None):
394  """Should this ccd be selected based on its PSF shape information
395 
396  Parameters
397  ----------
398  srcCatalog : `lsst.afw.table.SourceCatalog`
399  dataId : `dict` of dataId keys, optional.
400  Used only for logging. Defaults to None.
401 
402  Returns
403  -------
404  valid : `bool`
405  True if selected.
406  """
407  mask = srcCatalog[self.config.starSelection]
408 
409  starXX = srcCatalog[self.config.starShape+'_xx'][mask]
410  starYY = srcCatalog[self.config.starShape+'_yy'][mask]
411  starXY = srcCatalog[self.config.starShape+'_xy'][mask]
412  psfXX = srcCatalog[self.config.psfShape+'_xx'][mask]
413  psfYY = srcCatalog[self.config.psfShape+'_yy'][mask]
414  psfXY = srcCatalog[self.config.psfShape+'_xy'][mask]
415 
416  starSize = np.power(starXX*starYY - starXY**2, 0.25)
417  starE1 = (starXX - starYY)/(starXX + starYY)
418  starE2 = 2*starXY/(starXX + starYY)
419  medianSize = np.median(starSize)
420 
421  psfSize = np.power(psfXX*psfYY - psfXY**2, 0.25)
422  psfE1 = (psfXX - psfYY)/(psfXX + psfYY)
423  psfE2 = 2*psfXY/(psfXX + psfYY)
424 
425  medianE1 = np.abs(np.median(starE1 - psfE1))
426  medianE2 = np.abs(np.median(starE2 - psfE2))
427  medianE = np.sqrt(medianE1**2 + medianE2**2)
428 
429  scatterSize = sigmaMad(starSize - psfSize)
430  scaledScatterSize = scatterSize/medianSize**2
431 
432  valid = True
433  if self.config.maxEllipResidual and medianE > self.config.maxEllipResidual:
434  self.log.info("Removing visit %s because median e residual too large: %f vs %f",
435  dataId, medianE, self.config.maxEllipResidual)
436  valid = False
437  elif self.config.maxSizeScatter and scatterSize > self.config.maxSizeScatter:
438  self.log.info("Removing visit %s because size scatter is too large: %f vs %f",
439  dataId, scatterSize, self.config.maxSizeScatter)
440  valid = False
441  elif self.config.maxScaledSizeScatter and scaledScatterSize > self.config.maxScaledSizeScatter:
442  self.log.info("Removing visit %s because scaled size scatter is too large: %f vs %f",
443  dataId, scaledScatterSize, self.config.maxScaledSizeScatter)
444  valid = False
445 
446  return valid
447 
448 
449 class BestSeeingWcsSelectImageConfig(WcsSelectImagesTask.ConfigClass):
450  """Base configuration for BestSeeingSelectImagesTask.
451  """
452  nImagesMax = pexConfig.RangeField(
453  dtype=int,
454  doc="Maximum number of images to select",
455  default=5,
456  min=0)
457  maxPsfFwhm = pexConfig.Field(
458  dtype=float,
459  doc="Maximum PSF FWHM (in arcseconds) to select",
460  default=1.5,
461  optional=True)
462  minPsfFwhm = pexConfig.Field(
463  dtype=float,
464  doc="Minimum PSF FWHM (in arcseconds) to select",
465  default=0.,
466  optional=True)
467 
468 
469 class BestSeeingWcsSelectImagesTask(WcsSelectImagesTask):
470  """Select up to a maximum number of the best-seeing images using their Wcs.
471  """
472  ConfigClass = BestSeeingWcsSelectImageConfig
473 
474  def runDataRef(self, dataRef, coordList, makeDataRefList=True,
475  selectDataList=None):
476  """Select the best-seeing images in the selectDataList that overlap the patch.
477 
478  This method is the old entry point for the Gen2 commandline tasks and drivers
479  Will be deprecated in v22.
480 
481  Parameters
482  ----------
483  dataRef : `lsst.daf.persistence.ButlerDataRef`
484  Data reference for coadd/tempExp (with tract, patch)
485  coordList : `list` of `lsst.geom.SpherePoint`
486  List of ICRS sky coordinates specifying boundary of patch
487  makeDataRefList : `boolean`, optional
488  Construct a list of data references?
489  selectDataList : `list` of `SelectStruct`
490  List of SelectStruct, to consider for selection
491 
492  Returns
493  -------
494  result : `lsst.pipe.base.Struct`
495  Result struct with components:
496  - ``exposureList``: the selected exposures
497  (`list` of `lsst.pipe.tasks.selectImages.BaseExposureInfo`).
498  - ``dataRefList``: the optional data references corresponding to
499  each element of ``exposureList``
500  (`list` of `lsst.daf.persistence.ButlerDataRef`, or `None`).
501  """
502  psfSizes = []
503  dataRefList = []
504  exposureInfoList = []
505 
506  if selectDataList is None:
507  selectDataList = []
508 
509  result = super().runDataRef(dataRef, coordList, makeDataRefList=True, selectDataList=selectDataList)
510 
511  for dataRef, exposureInfo in zip(result.dataRefList, result.exposureInfoList):
512  cal = dataRef.get("calexp", immediate=True)
513 
514  # if min/max PSF values are defined, remove images out of bounds
515  pixToArcseconds = cal.getWcs().getPixelScale().asArcseconds()
516  psfSize = cal.getPsf().computeShape().getDeterminantRadius()*pixToArcseconds
517  sizeFwhm = psfSize * np.sqrt(8.*np.log(2.))
518  if self.config.maxPsfFwhm and sizeFwhm > self.config.maxPsfFwhm:
519  continue
520  if self.config.minPsfFwhm and sizeFwhm < self.config.minPsfFwhm:
521  continue
522  psfSizes.append(sizeFwhm)
523  dataRefList.append(dataRef)
524  exposureInfoList.append(exposureInfo)
525 
526  if len(psfSizes) > self.config.nImagesMax:
527  sortedIndices = np.argsort(psfSizes)[:self.config.nImagesMax]
528  filteredDataRefList = [dataRefList[i] for i in sortedIndices]
529  filteredExposureInfoList = [exposureInfoList[i] for i in sortedIndices]
530  self.log.info("%d images selected with FWHM range of %f--%f arcseconds",
531  len(sortedIndices), psfSizes[sortedIndices[0]], psfSizes[sortedIndices[-1]])
532 
533  else:
534  if len(psfSizes) == 0:
535  self.log.warning("0 images selected.")
536  else:
537  self.log.debug("%d images selected with FWHM range of %d--%d arcseconds",
538  len(psfSizes), psfSizes[0], psfSizes[-1])
539  filteredDataRefList = dataRefList
540  filteredExposureInfoList = exposureInfoList
541 
542  return pipeBase.Struct(
543  dataRefList=filteredDataRefList if makeDataRefList else None,
544  exposureInfoList=filteredExposureInfoList,
545  )
546 
547 
548 class BestSeeingSelectVisitsConnections(pipeBase.PipelineTaskConnections,
549  dimensions=("tract", "patch", "skymap", "band", "instrument"),
550  defaultTemplates={"coaddName": "goodSeeing"}):
551  skyMap = pipeBase.connectionTypes.Input(
552  doc="Input definition of geometry/bbox and projection/wcs for coadded exposures",
553  name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
554  storageClass="SkyMap",
555  dimensions=("skymap",),
556  )
557  visitSummaries = pipeBase.connectionTypes.Input(
558  doc="Per-visit consolidated exposure metadata from ConsolidateVisitSummaryTask",
559  name="visitSummary",
560  storageClass="ExposureCatalog",
561  dimensions=("instrument", "visit",),
562  multiple=True,
563  deferLoad=True
564  )
565  goodVisits = pipeBase.connectionTypes.Output(
566  doc="Selected visits to be coadded.",
567  name="{coaddName}Visits",
568  storageClass="StructuredDataDict",
569  dimensions=("instrument", "tract", "patch", "skymap", "band"),
570  )
571 
572 
573 class BestSeeingSelectVisitsConfig(pipeBase.PipelineTaskConfig,
574  pipelineConnections=BestSeeingSelectVisitsConnections):
575  nVisitsMax = pexConfig.RangeField(
576  dtype=int,
577  doc="Maximum number of visits to select",
578  default=12,
579  min=0
580  )
581  maxPsfFwhm = pexConfig.Field(
582  dtype=float,
583  doc="Maximum PSF FWHM (in arcseconds) to select",
584  default=1.5,
585  optional=True
586  )
587  minPsfFwhm = pexConfig.Field(
588  dtype=float,
589  doc="Minimum PSF FWHM (in arcseconds) to select",
590  default=0.,
591  optional=True
592  )
593  doConfirmOverlap = pexConfig.Field(
594  dtype=bool,
595  doc="Do remove visits that do not actually overlap the patch?",
596  default=True,
597  )
598  minMJD = pexConfig.Field(
599  dtype=float,
600  doc="Minimum visit MJD to select",
601  default=None,
602  optional=True
603  )
604  maxMJD = pexConfig.Field(
605  dtype=float,
606  doc="Maximum visit MJD to select",
607  default=None,
608  optional=True
609  )
610 
611 
612 class BestSeeingSelectVisitsTask(pipeBase.PipelineTask):
613  """Select up to a maximum number of the best-seeing visits
614 
615  Don't exceed the FWHM range specified by configs min(max)PsfFwhm.
616  This Task is a port of the Gen2 image-selector used in the AP pipeline:
617  BestSeeingSelectImagesTask. This Task selects full visits based on the
618  average PSF of the entire visit.
619  """
620  ConfigClass = BestSeeingSelectVisitsConfig
621  _DefaultName = 'bestSeeingSelectVisits'
622 
623  def runQuantum(self, butlerQC, inputRefs, outputRefs):
624  inputs = butlerQC.get(inputRefs)
625  quantumDataId = butlerQC.quantum.dataId
626  outputs = self.run(**inputs, dataId=quantumDataId)
627  butlerQC.put(outputs, outputRefs)
628 
629  def run(self, visitSummaries, skyMap, dataId):
630  """Run task
631 
632  Parameters:
633  -----------
634  visitSummary : `list`
635  List of `lsst.pipe.base.connections.DeferredDatasetRef` of
636  visitSummary tables of type `lsst.afw.table.ExposureCatalog`
637  skyMap : `lsst.skyMap.SkyMap`
638  SkyMap for checking visits overlap patch
639  dataId : `dict` of dataId keys
640  For retrieving patch info for checking visits overlap patch
641 
642  Returns
643  -------
644  result : `lsst.pipe.base.Struct`
645  Result struct with components:
646 
647  - `goodVisits`: `dict` with selected visit ids as keys,
648  so that it can be be saved as a StructuredDataDict.
649  StructuredDataList's are currently limited.
650  """
651 
652  if self.config.doConfirmOverlap:
653  patchPolygon = self.makePatchPolygon(skyMap, dataId)
654 
655  inputVisits = [visitSummary.ref.dataId['visit'] for visitSummary in visitSummaries]
656  fwhmSizes = []
657  visits = []
658  for visit, visitSummary in zip(inputVisits, visitSummaries):
659  # read in one-by-one and only once. There may be hundreds
660  visitSummary = visitSummary.get()
661 
662  # mjd is guaranteed to be the same for every detector in the visitSummary.
663  mjd = visitSummary[0].getVisitInfo().getDate().get(system=DateTime.MJD)
664 
665  pixToArcseconds = [vs.getWcs().getPixelScale(vs.getBBox().getCenter()).asArcseconds()
666  for vs in visitSummary]
667  # psfSigma is PSF model determinant radius at chip center in pixels
668  psfSigmas = np.array([vs['psfSigma'] for vs in visitSummary])
669  fwhm = np.nanmean(psfSigmas * pixToArcseconds) * np.sqrt(8.*np.log(2.))
670 
671  if self.config.maxPsfFwhm and fwhm > self.config.maxPsfFwhm:
672  continue
673  if self.config.minPsfFwhm and fwhm < self.config.minPsfFwhm:
674  continue
675  if self.config.minMJD and mjd < self.config.minMJD:
676  self.log.debug('MJD %f earlier than %.2f; rejecting', mjd, self.config.minMJD)
677  continue
678  if self.config.maxMJD and mjd > self.config.maxMJD:
679  self.log.debug('MJD %f later than %.2f; rejecting', mjd, self.config.maxMJD)
680  continue
681  if self.config.doConfirmOverlap and not self.doesIntersectPolygon(visitSummary, patchPolygon):
682  continue
683 
684  fwhmSizes.append(fwhm)
685  visits.append(visit)
686 
687  sortedVisits = [ind for (_, ind) in sorted(zip(fwhmSizes, visits))]
688  output = sortedVisits[:self.config.nVisitsMax]
689  self.log.info("%d images selected with FWHM range of %d--%d arcseconds",
690  len(output), fwhmSizes[visits.index(output[0])], fwhmSizes[visits.index(output[-1])])
691 
692  # In order to store as a StructuredDataDict, convert list to dict
693  goodVisits = {key: True for key in output}
694  return pipeBase.Struct(goodVisits=goodVisits)
695 
696  def makePatchPolygon(self, skyMap, dataId):
697  """Return True if sky polygon overlaps visit
698 
699  Parameters:
700  -----------
701  skyMap : `lsst.afw.table.ExposureCatalog`
702  Exposure catalog with per-detector geometry
703  dataId : `dict` of dataId keys
704  For retrieving patch info
705 
706  Returns:
707  --------
708  result :` lsst.sphgeom.ConvexPolygon.convexHull`
709  Polygon of patch's outer bbox
710  """
711  wcs = skyMap[dataId['tract']].getWcs()
712  bbox = skyMap[dataId['tract']][dataId['patch']].getOuterBBox()
713  sphCorners = wcs.pixelToSky(lsst.geom.Box2D(bbox).getCorners())
714  result = lsst.sphgeom.ConvexPolygon.convexHull([coord.getVector() for coord in sphCorners])
715  return result
716 
717  def doesIntersectPolygon(self, visitSummary, polygon):
718  """Return True if sky polygon overlaps visit
719 
720  Parameters:
721  -----------
722  visitSummary : `lsst.afw.table.ExposureCatalog`
723  Exposure catalog with per-detector geometry
724  polygon :` lsst.sphgeom.ConvexPolygon.convexHull`
725  Polygon to check overlap
726 
727  Returns:
728  --------
729  doesIntersect: `bool`
730  Does the visit overlap the polygon
731  """
732  doesIntersect = False
733  for detectorSummary in visitSummary:
734  corners = [lsst.geom.SpherePoint(ra, decl, units=lsst.geom.degrees).getVector() for (ra, decl) in
735  zip(detectorSummary['raCorners'], detectorSummary['decCorners'])]
736  detectorPolygon = lsst.sphgeom.ConvexPolygon.convexHull(corners)
737  if detectorPolygon.intersects(polygon):
738  doesIntersect = True
739  break
740  return doesIntersect
741 
742 
743 class BestSeeingQuantileSelectVisitsConfig(pipeBase.PipelineTaskConfig,
744  pipelineConnections=BestSeeingSelectVisitsConnections):
745  qMin = pexConfig.RangeField(
746  doc="Lower bound of quantile range to select. Sorts visits by seeing from narrow to wide, "
747  "and select those in the interquantile range (qMin, qMax). Set qMin to 0 for Best Seeing. "
748  "This config should be changed from zero only for exploratory diffIm testing.",
749  dtype=float,
750  default=0,
751  min=0,
752  max=1,
753  )
754  qMax = pexConfig.RangeField(
755  doc="Upper bound of quantile range to select. Sorts visits by seeing from narrow to wide, "
756  "and select those in the interquantile range (qMin, qMax). Set qMax to 1 for Worst Seeing.",
757  dtype=float,
758  default=0.33,
759  min=0,
760  max=1,
761  )
762  nVisitsMin = pexConfig.Field(
763  doc="At least this number of visits selected and supercedes quantile. For example, if 10 visits "
764  "cover this patch, qMin=0.33, and nVisitsMin=5, the best 5 visits will be selected.",
765  dtype=int,
766  default=6,
767  )
768  doConfirmOverlap = pexConfig.Field(
769  dtype=bool,
770  doc="Do remove visits that do not actually overlap the patch?",
771  default=True,
772  )
773  minMJD = pexConfig.Field(
774  dtype=float,
775  doc="Minimum visit MJD to select",
776  default=None,
777  optional=True
778  )
779  maxMJD = pexConfig.Field(
780  dtype=float,
781  doc="Maximum visit MJD to select",
782  default=None,
783  optional=True
784  )
785 
786 
787 class BestSeeingQuantileSelectVisitsTask(BestSeeingSelectVisitsTask):
788  """Select a quantile of the best-seeing visits
789 
790  Selects the best (for example, third) full visits based on the average
791  PSF width in the entire visit. It can also be used for difference imaging
792  experiments that require templates with the worst seeing visits.
793  For example, selecting the worst third can be acheived by
794  changing the config parameters qMin to 0.66 and qMax to 1.
795  """
796  ConfigClass = BestSeeingQuantileSelectVisitsConfig
797  _DefaultName = 'bestSeeingQuantileSelectVisits'
798 
799  @utils.inheritDoc(BestSeeingSelectVisitsTask)
800  def run(self, visitSummaries, skyMap, dataId):
801  if self.config.doConfirmOverlap:
802  patchPolygon = self.makePatchPolygon(skyMap, dataId)
803  visits = np.array([visitSummary.ref.dataId['visit'] for visitSummary in visitSummaries])
804  radius = np.empty(len(visits))
805  intersects = np.full(len(visits), True)
806  for i, visitSummary in enumerate(visitSummaries):
807  # read in one-by-one and only once. There may be hundreds
808  visitSummary = visitSummary.get()
809  # psfSigma is PSF model determinant radius at chip center in pixels
810  psfSigma = np.nanmedian([vs['psfSigma'] for vs in visitSummary])
811  radius[i] = psfSigma
812  if self.config.doConfirmOverlap:
813  intersects[i] = self.doesIntersectPolygon(visitSummary, patchPolygon)
814  if self.config.minMJD or self.config.maxMJD:
815  # mjd is guaranteed to be the same for every detector in the visitSummary.
816  mjd = visitSummary[0].getVisitInfo().getDate().get(system=DateTime.MJD)
817  aboveMin = mjd > self.config.minMJD if self.config.minMJD else True
818  belowMax = mjd < self.config.maxMJD if self.config.maxMJD else True
819  intersects[i] = intersects[i] and aboveMin and belowMax
820 
821  sortedVisits = [v for rad, v in sorted(zip(radius[intersects], visits[intersects]))]
822  lowerBound = min(int(np.round(self.config.qMin*len(visits[intersects]))),
823  max(0, len(visits[intersects]) - self.config.nVisitsMin))
824  upperBound = max(int(np.round(self.config.qMax*len(visits[intersects]))), self.config.nVisitsMin)
825 
826  # In order to store as a StructuredDataDict, convert list to dict
827  goodVisits = {int(visit): True for visit in sortedVisits[lowerBound:upperBound]}
828  return pipeBase.Struct(goodVisits=goodVisits)
int min
int max
A floating-point coordinate rectangle geometry.
Definition: Box.h:413
Point in an unspecified spherical coordinate system.
Definition: SpherePoint.h:57
Reports arguments outside the domain of an operation.
Definition: Runtime.h:57
Reports errors that are due to events beyond the control of the program.
Definition: Runtime.h:104
def __init__(self, dataId, coordList)
Definition: selectImages.py:62
def runDataRef(self, dataRef, coordList, makeDataRefList=True, selectDataList=[])
def __init__(self, dataRef, wcs, bbox)
def getValidImageCorners(self, imageWcs, imageBox, patchPoly, dataId=None)
def runDataRef(self, dataRef, coordList, makeDataRefList=True, selectDataList=[])
def run(self, wcsList, bboxList, coordList, dataIds=None, **kwargs)
static ConvexPolygon convexHull(std::vector< UnitVector3d > const &points)
convexHull returns the convex hull of the given set of points if it exists and throws an exception ot...
Definition: ConvexPolygon.h:65
daf::base::PropertyList * list
Definition: fits.cc:913
daf::base::PropertySet * set
Definition: fits.cc:912
bool isValid
Definition: fits.cc:399
def run(self, coaddExposures, bbox, wcs)
Definition: getTemplate.py:603