22__all__ = [
"BaseSelectImagesTask",
"BaseExposureInfo",
"WcsSelectImagesTask",
"PsfWcsSelectImagesTask",
23 "DatabaseSelectImagesConfig",
"BestSeeingSelectVisitsTask",
24 "BestSeeingQuantileSelectVisitsTask"]
34from lsst.utils.timer
import timeMethod
38 """Base configuration for subclasses of BaseSelectImagesTask that use a
42 host = pexConfig.Field(
43 doc=
"Database server host name",
46 port = pexConfig.Field(
47 doc=
"Database server port",
50 database = pexConfig.Field(
51 doc=
"Name of database",
54 maxExposures = pexConfig.Field(
55 doc=
"maximum exposures to select; intended for debugging; ignored if None",
62 """Data about a selected exposure.
67 Data ID keys of exposure.
68 coordList : `list` [`lsst.afw.geom.SpherePoint`]
69 ICRS coordinates of the corners of the exposure
70 plus any others items that are desired.
74 super(BaseExposureInfo, self).
__init__(dataId=dataId, coordList=coordList)
78 """Base task for selecting images suitable for coaddition.
81 ConfigClass = pexConfig.Config
82 _DefaultName =
"selectImages"
85 def run(self, coordList):
86 """Select images suitable for coaddition in a particular region.
90 coordList : `list` [`lsst.geom.SpherePoint`] or `None`
91 List of coordinates defining region of interest; if `None`, then
92 select all images subclasses may add additional keyword arguments,
97 result : `pipeBase.Struct`
98 Results as a struct with attributes:
101 A list of exposure information objects (subclasses of
102 BaseExposureInfo), which have at least the following fields:
103 - dataId: Data ID dictionary (`dict`).
104 - coordList: ICRS coordinates of the corners of the exposure.
105 (`list` [`lsst.geom.SpherePoint`])
107 raise NotImplementedError()
111 """Extract the keys and values from a list of dataIds.
113 The input dataList is a list of objects that have 'dataId' members.
114 This allows it to be used for both a list of data references and a
115 list of ExposureInfo.
130 Raised if DataId keys are inconsistent.
132 assert len(dataList) > 0
134 keys = sorted(dataList[0].dataId.keys())
137 for data
in dataList:
138 thisKeys =
set(data.dataId.keys())
139 if thisKeys != keySet:
140 raise RuntimeError(
"DataId keys inconsistent: %s vs %s" % (keySet, thisKeys))
141 values.append(tuple(data.dataId[k]
for k
in keys))
146 """A container for data to be passed to the WcsSelectImagesTask.
152 wcs : `lsst.afw.geom.SkyWcs`
153 Coordinate system definition (wcs).
154 bbox : `lsst.geom.box.Box2I`
155 Integer bounding box for image.
159 super(SelectStruct, self).
__init__(dataRef=dataRef, wcs=wcs, bbox=bbox)
163 """Select images using their Wcs.
165 We use the "convexHull" method of lsst.sphgeom.ConvexPolygon to define
166 polygons on the celestial sphere, and test the polygon of the
167 patch for overlap with the polygon of the image.
169 We use "convexHull" instead of generating a ConvexPolygon
170 directly because the standard for the inputs to ConvexPolygon
171 are pretty high and we don't want to be responsible for reaching them.
174 def run(self, wcsList, bboxList, coordList, dataIds=None, **kwargs):
175 """Return indices of provided lists that meet the selection criteria.
179 wcsList : `list` [`lsst.afw.geom.SkyWcs`]
180 Specifying the WCS's of the input ccds to be selected.
181 bboxList : `list` [`lsst.geom.Box2I`]
182 Specifying the bounding boxes of the input ccds to be selected.
183 coordList : `list` [`lsst.geom.SpherePoint`]
184 ICRS coordinates specifying boundary of the patch.
185 dataIds : iterable [`lsst.daf.butler.dataId`] or `None`, optional
186 An iterable object of dataIds which point to reference catalogs.
188 Additional keyword arguments.
192 result : `list` [`int`]
193 The indices of selected ccds.
196 dataIds = [
None] * len(wcsList)
197 patchVertices = [coord.getVector()
for coord
in coordList]
200 for i, (imageWcs, imageBox, dataId)
in enumerate(zip(wcsList, bboxList, dataIds)):
202 self.log.info(
"De-selecting exposure %s: Exposure has no WCS.", dataId)
210 """Return corners or `None` if bad.
216 patchPoly : `Unknown`
220 imageCorners = [imageWcs.pixelToSky(pix)
for pix
in geom.Box2D(imageBox).getCorners()]
223 self.log.debug(
"WCS error in testing calexp %s (%s): deselecting", dataId, e)
227 if imagePoly
is None:
228 self.log.debug(
"Unable to create polygon from image %s: deselecting", dataId)
231 if patchPoly.intersects(imagePoly):
233 self.log.info(
"Selecting calexp %s", dataId)
240 dimensions=(
"tract",
"patch",
"skymap",
"instrument",
"visit"),
241 defaultTemplates={
"coaddName":
"deep"}):
245class PsfWcsSelectImagesConfig(pipeBase.PipelineTaskConfig,
246 pipelineConnections=PsfWcsSelectImagesConnections):
247 maxEllipResidual = pexConfig.Field(
248 doc=
"Maximum median ellipticity residual",
253 maxSizeScatter = pexConfig.Field(
254 doc=
"Maximum scatter in the size residuals",
258 maxScaledSizeScatter = pexConfig.Field(
259 doc=
"Maximum scatter in the size residuals, scaled by the median size",
264 maxPsfTraceRadiusDelta = pexConfig.Field(
265 doc=
"Maximum delta (max - min) of model PSF trace radius values evaluated on a grid on "
266 "the unmasked detector pixels (pixel).",
274 """Select images using their Wcs and cuts on the PSF properties.
276 The PSF quality criteria are based on the size and ellipticity
277 residuals from the adaptive second moments of the star and the PSF.
280 - the median of the ellipticty residuals.
281 - the robust scatter of the size residuals (using the median absolute
283 - the robust scatter of the size residuals scaled by the square of
287 ConfigClass = PsfWcsSelectImagesConfig
288 _DefaultName =
"PsfWcsSelectImages"
290 def run(self, wcsList, bboxList, coordList, visitSummary, dataIds=None, **kwargs):
291 """Return indices of provided lists that meet the selection criteria.
295 wcsList : `list` [`lsst.afw.geom.SkyWcs`]
296 Specifying the WCS's of the input ccds to be selected.
297 bboxList : `list` [`lsst.geom.Box2I`]
298 Specifying the bounding boxes of the input ccds to be selected.
299 coordList : `list` [`lsst.geom.SpherePoint`]
300 ICRS coordinates specifying boundary of the patch.
301 visitSummary : `list` [`lsst.afw.table.ExposureCatalog`]
302 containing the PSF shape information for the input ccds to be
304 dataIds : iterable [`lsst.daf.butler.dataId`] or `None`, optional
305 An iterable object of dataIds which point to reference catalogs.
307 Additional keyword arguments.
311 goodPsf : `list` [`int`]
312 The indices of selected ccds.
314 goodWcs = super(PsfWcsSelectImagesTask, self).run(wcsList=wcsList, bboxList=bboxList,
315 coordList=coordList, dataIds=dataIds)
319 for i, dataId
in enumerate(dataIds):
322 if self.isValid(visitSummary, dataId[
"detector"]):
327 def isValid(self, visitSummary, detectorId):
328 """Should this ccd be selected based on its PSF shape information.
332 visitSummary : `lsst.afw.table.ExposureCatalog`
333 Exposure catalog with per-detector summary information.
342 row = visitSummary.find(detectorId)
345 self.log.warning(
"Removing detector %d because summary stats not available.", detectorId)
348 medianE = np.sqrt(row[
"psfStarDeltaE1Median"]**2. + row[
"psfStarDeltaE2Median"]**2.)
349 scatterSize = row[
"psfStarDeltaSizeScatter"]
350 scaledScatterSize = row[
"psfStarScaledDeltaSizeScatter"]
351 psfTraceRadiusDelta = row[
"psfTraceRadiusDelta"]
354 if self.config.maxEllipResidual
and not (medianE <= self.config.maxEllipResidual):
355 self.log.info(
"Removing visit %d detector %d because median e residual too large: %f vs %f",
356 row[
"visit"], detectorId, medianE, self.config.maxEllipResidual)
358 elif self.config.maxSizeScatter
and not (scatterSize <= self.config.maxSizeScatter):
359 self.log.info(
"Removing visit %d detector %d because size scatter too large: %f vs %f",
360 row[
"visit"], detectorId, scatterSize, self.config.maxSizeScatter)
362 elif self.config.maxScaledSizeScatter
and not (scaledScatterSize <= self.config.maxScaledSizeScatter):
363 self.log.info(
"Removing visit %d detector %d because scaled size scatter too large: %f vs %f",
364 row[
"visit"], detectorId, scaledScatterSize, self.config.maxScaledSizeScatter)
367 self.config.maxPsfTraceRadiusDelta
is not None
368 and not (psfTraceRadiusDelta <= self.config.maxPsfTraceRadiusDelta)
371 "Removing visit %d detector %d because max-min delta of model PSF trace radius values "
372 "across the unmasked detector pixels is not finite or too large: %.3f vs %.3f (pixels)",
373 row[
"visit"], detectorId, psfTraceRadiusDelta, self.config.maxPsfTraceRadiusDelta
380class BestSeeingSelectVisitsConnections(pipeBase.PipelineTaskConnections,
381 dimensions=(
"tract",
"patch",
"skymap",
"band",
"instrument"),
382 defaultTemplates={
"coaddName":
"goodSeeing",
384 skyMap = pipeBase.connectionTypes.Input(
385 doc=
"Input definition of geometry/bbox and projection/wcs for coadded exposures",
386 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
387 storageClass=
"SkyMap",
388 dimensions=(
"skymap",),
390 visitSummaries = pipeBase.connectionTypes.Input(
391 doc=
"Per-visit consolidated exposure metadata",
392 name=
"finalVisitSummary",
393 storageClass=
"ExposureCatalog",
394 dimensions=(
"instrument",
"visit",),
398 goodVisits = pipeBase.connectionTypes.Output(
399 doc=
"Selected visits to be coadded.",
400 name=
"{coaddName}Visits",
401 storageClass=
"StructuredDataDict",
402 dimensions=(
"instrument",
"tract",
"patch",
"skymap",
"band"),
406class BestSeeingSelectVisitsConfig(pipeBase.PipelineTaskConfig,
407 pipelineConnections=BestSeeingSelectVisitsConnections):
408 nVisitsMax = pexConfig.RangeField(
410 doc=
"Maximum number of visits to select; use -1 to select all.",
414 maxPsfFwhm = pexConfig.Field(
416 doc=
"Maximum PSF FWHM (in arcseconds) to select",
420 minPsfFwhm = pexConfig.Field(
422 doc=
"Minimum PSF FWHM (in arcseconds) to select",
426 doConfirmOverlap = pexConfig.Field(
428 doc=
"Do remove visits that do not actually overlap the patch?",
431 minMJD = pexConfig.Field(
433 doc=
"Minimum visit MJD to select",
437 maxMJD = pexConfig.Field(
439 doc=
"Maximum visit MJD to select",
445class BestSeeingSelectVisitsTask(pipeBase.PipelineTask):
446 """Select up to a maximum number of the best-seeing visits.
448 Don't exceed the FWHM range specified by configs min(max)PsfFwhm.
449 This Task is a port of the Gen2 image-selector used in the AP pipeline:
450 BestSeeingSelectImagesTask. This Task selects full visits based on the
451 average PSF of the entire visit.
454 ConfigClass = BestSeeingSelectVisitsConfig
455 _DefaultName =
'bestSeeingSelectVisits'
457 def runQuantum(self, butlerQC, inputRefs, outputRefs):
458 inputs = butlerQC.get(inputRefs)
459 quantumDataId = butlerQC.quantum.dataId
460 outputs = self.run(**inputs, dataId=quantumDataId)
461 butlerQC.put(outputs, outputRefs)
463 def run(self, visitSummaries, skyMap, dataId):
468 visitSummary : `list` [`lsst.pipe.base.connections.DeferredDatasetRef`]
469 List of `lsst.pipe.base.connections.DeferredDatasetRef` of
470 visitSummary tables of type `lsst.afw.table.ExposureCatalog`.
471 skyMap : `lsst.skyMap.SkyMap`
472 SkyMap for checking visits overlap patch.
473 dataId : `dict` of dataId keys
474 For retrieving patch info for checking visits overlap patch.
478 result : `lsst.pipe.base.Struct`
479 Results as a struct with attributes:
482 A `dict` with selected visit ids as keys,
483 so that it can be be saved as a StructuredDataDict.
484 StructuredDataList's are currently limited.
486 if self.config.doConfirmOverlap:
487 patchPolygon = self.makePatchPolygon(skyMap, dataId)
489 inputVisits = [visitSummary.ref.dataId[
'visit']
for visitSummary
in visitSummaries]
492 for visit, visitSummary
in zip(inputVisits, visitSummaries):
494 visitSummary = visitSummary.get()
498 mjd = visitSummary[0].getVisitInfo().getDate().get(system=DateTime.MJD)
500 pixToArcseconds = [vs.getWcs().getPixelScale(vs.getBBox().getCenter()).asArcseconds()
501 for vs
in visitSummary
if vs.getWcs()]
503 psfSigmas = np.array([vs[
'psfSigma']
for vs
in visitSummary
if vs.getWcs()])
504 fwhm = np.nanmean(psfSigmas * pixToArcseconds) * np.sqrt(8.*np.log(2.))
506 if self.config.maxPsfFwhm
and fwhm > self.config.maxPsfFwhm:
508 if self.config.minPsfFwhm
and fwhm < self.config.minPsfFwhm:
510 if self.config.minMJD
and mjd < self.config.minMJD:
511 self.log.debug(
'MJD %f earlier than %.2f; rejecting', mjd, self.config.minMJD)
513 if self.config.maxMJD
and mjd > self.config.maxMJD:
514 self.log.debug(
'MJD %f later than %.2f; rejecting', mjd, self.config.maxMJD)
516 if self.config.doConfirmOverlap
and not self.doesIntersectPolygon(visitSummary, patchPolygon):
519 fwhmSizes.append(fwhm)
522 sortedVisits = [ind
for (_, ind)
in sorted(zip(fwhmSizes, visits))]
523 if self.config.nVisitsMax < 0:
524 output = sortedVisits
526 output = sortedVisits[:self.config.nVisitsMax]
529 self.log.info(
"All images rejected in BestSeeingSelectVisitsTask.")
530 raise pipeBase.NoWorkFound(f
"No good images found for {dataId}")
533 "%d images selected with FWHM range of %f--%f arcseconds",
535 fwhmSizes[visits.index(output[0])],
536 fwhmSizes[visits.index(output[-1])],
540 goodVisits = {key:
True for key
in output}
541 return pipeBase.Struct(goodVisits=goodVisits)
543 def makePatchPolygon(self, skyMap, dataId):
544 """Return True if sky polygon overlaps visit.
548 skyMap : `lsst.afw.table.ExposureCatalog`
549 Exposure catalog with per-detector geometry.
550 dataId : `dict` of dataId keys
551 For retrieving patch info.
555 result : `lsst.sphgeom.ConvexPolygon.convexHull`
556 Polygon of patch's outer bbox.
558 wcs = skyMap[dataId[
'tract']].getWcs()
559 bbox = skyMap[dataId[
'tract']][dataId[
'patch']].getOuterBBox()
564 def doesIntersectPolygon(self, visitSummary, polygon):
565 """Return True if sky polygon overlaps visit.
569 visitSummary : `lsst.afw.table.ExposureCatalog`
570 Exposure catalog with per-detector geometry.
571 polygon :` lsst.sphgeom.ConvexPolygon.convexHull`
572 Polygon to check overlap.
576 doesIntersect : `bool`
577 True if the visit overlaps the polygon.
579 doesIntersect =
False
580 for detectorSummary
in visitSummary:
581 if (np.all(np.isfinite(detectorSummary[
'raCorners']))
582 and np.all(np.isfinite(detectorSummary[
'decCorners']))):
585 zip(detectorSummary[
'raCorners'], detectorSummary[
'decCorners'])]
587 if detectorPolygon.intersects(polygon):
593class BestSeeingQuantileSelectVisitsConfig(pipeBase.PipelineTaskConfig,
594 pipelineConnections=BestSeeingSelectVisitsConnections):
595 qMin = pexConfig.RangeField(
596 doc=
"Lower bound of quantile range to select. Sorts visits by seeing from narrow to wide, "
597 "and select those in the interquantile range (qMin, qMax). Set qMin to 0 for Best Seeing. "
598 "This config should be changed from zero only for exploratory diffIm testing.",
604 qMax = pexConfig.RangeField(
605 doc=
"Upper bound of quantile range to select. Sorts visits by seeing from narrow to wide, "
606 "and select those in the interquantile range (qMin, qMax). Set qMax to 1 for Worst Seeing.",
612 nVisitsMin = pexConfig.Field(
613 doc=
"At least this number of visits selected and supercedes quantile. For example, if 10 visits "
614 "cover this patch, qMin=0.33, and nVisitsMin=5, the best 5 visits will be selected.",
618 doConfirmOverlap = pexConfig.Field(
620 doc=
"Do remove visits that do not actually overlap the patch?",
623 minMJD = pexConfig.Field(
625 doc=
"Minimum visit MJD to select",
629 maxMJD = pexConfig.Field(
631 doc=
"Maximum visit MJD to select",
637class BestSeeingQuantileSelectVisitsTask(BestSeeingSelectVisitsTask):
638 """Select a quantile of the best-seeing visits.
640 Selects the best (for example, third) full visits based on the average
641 PSF width in the entire visit. It can also be used for difference imaging
642 experiments that require templates with the worst seeing visits.
643 For example, selecting the worst third can be acheived by
644 changing the config parameters qMin to 0.66 and qMax to 1.
646 ConfigClass = BestSeeingQuantileSelectVisitsConfig
647 _DefaultName =
'bestSeeingQuantileSelectVisits'
649 def run(self, visitSummaries, skyMap, dataId):
650 if self.config.doConfirmOverlap:
651 patchPolygon = self.makePatchPolygon(skyMap, dataId)
652 visits = np.array([visitSummary.ref.dataId[
'visit']
for visitSummary
in visitSummaries])
653 radius = np.empty(len(visits))
654 intersects = np.full(len(visits),
True)
655 for i, visitSummary
in enumerate(visitSummaries):
657 visitSummary = visitSummary.get()
659 psfSigma = np.nanmedian([vs[
'psfSigma']
for vs
in visitSummary])
661 if self.config.doConfirmOverlap:
662 intersects[i] = self.doesIntersectPolygon(visitSummary, patchPolygon)
663 if self.config.minMJD
or self.config.maxMJD:
666 mjd = visitSummary[0].getVisitInfo().getDate().get(system=DateTime.MJD)
667 aboveMin = mjd > self.config.minMJD
if self.config.minMJD
else True
668 belowMax = mjd < self.config.maxMJD
if self.config.maxMJD
else True
669 intersects[i] = intersects[i]
and aboveMin
and belowMax
671 sortedVisits = [v
for rad, v
in sorted(zip(radius[intersects], visits[intersects]))]
672 lowerBound =
min(int(np.round(self.config.qMin*len(visits[intersects]))),
673 max(0, len(visits[intersects]) - self.config.nVisitsMin))
674 upperBound =
max(int(np.round(self.config.qMax*len(visits[intersects]))), self.config.nVisitsMin)
677 goodVisits = {int(visit):
True for visit
in sortedVisits[lowerBound:upperBound]}
678 return pipeBase.Struct(goodVisits=goodVisits)
A floating-point coordinate rectangle geometry.
Point in an unspecified spherical coordinate system.
Reports arguments outside the domain of an operation.
Reports errors that are due to events beyond the control of the program.
__init__(self, dataId, coordList)
__init__(self, dataRef, wcs, bbox)
run(self, wcsList, bboxList, coordList, dataIds=None, **kwargs)
getValidImageCorners(self, imageWcs, imageBox, patchPoly, dataId=None)
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...
daf::base::PropertySet * set
_extractKeyValue(dataList, keys=None)