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