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).",
271 maxPsfApFluxDelta = pexConfig.Field(
272 doc=
"Maximum delta (max - min) of model PSF aperture flux (with aperture radius of "
273 "max(2, 3*psfSigma)) values evaluated on a grid on the unmasked detector pixels (based "
274 "on a normalized-to-one flux).",
279 maxPsfApCorrSigmaScaledDelta = pexConfig.Field(
280 doc=
"Maximum delta (max - min) of model PSF aperture correction values evaluated on a grid "
281 "on the unmasked detector pixels scaled (divided) by the measured model psfSigma.",
289 """Select images using their Wcs and cuts on the PSF properties.
291 The PSF quality criteria are based on the size and ellipticity
292 residuals from the adaptive second moments of the star and the PSF.
295 - the median of the ellipticty residuals.
296 - the robust scatter of the size residuals (using the median absolute
298 - the robust scatter of the size residuals scaled by the square of
302 ConfigClass = PsfWcsSelectImagesConfig
303 _DefaultName =
"PsfWcsSelectImages"
305 def run(self, wcsList, bboxList, coordList, visitSummary, dataIds=None, **kwargs):
306 """Return indices of provided lists that meet the selection criteria.
310 wcsList : `list` [`lsst.afw.geom.SkyWcs`]
311 Specifying the WCS's of the input ccds to be selected.
312 bboxList : `list` [`lsst.geom.Box2I`]
313 Specifying the bounding boxes of the input ccds to be selected.
314 coordList : `list` [`lsst.geom.SpherePoint`]
315 ICRS coordinates specifying boundary of the patch.
316 visitSummary : `list` [`lsst.afw.table.ExposureCatalog`]
317 containing the PSF shape information for the input ccds to be
319 dataIds : iterable [`lsst.daf.butler.dataId`] or `None`, optional
320 An iterable object of dataIds which point to reference catalogs.
322 Additional keyword arguments.
326 goodPsf : `list` [`int`]
327 The indices of selected ccds.
329 goodWcs = super(PsfWcsSelectImagesTask, self).run(wcsList=wcsList, bboxList=bboxList,
330 coordList=coordList, dataIds=dataIds)
334 for i, dataId
in enumerate(dataIds):
337 if self.isValid(visitSummary, dataId[
"detector"]):
342 def isValid(self, visitSummary, detectorId):
343 """Should this ccd be selected based on its PSF shape information.
347 visitSummary : `lsst.afw.table.ExposureCatalog`
348 Exposure catalog with per-detector summary information.
357 row = visitSummary.find(detectorId)
360 self.log.warning(
"Removing detector %d because summary stats not available.", detectorId)
363 medianE = np.sqrt(row[
"psfStarDeltaE1Median"]**2. + row[
"psfStarDeltaE2Median"]**2.)
364 scatterSize = row[
"psfStarDeltaSizeScatter"]
365 scaledScatterSize = row[
"psfStarScaledDeltaSizeScatter"]
366 psfTraceRadiusDelta = row[
"psfTraceRadiusDelta"]
367 psfApFluxDelta = row[
"psfApFluxDelta"]
368 psfApCorrSigmaScaledDelta = row[
"psfApCorrSigmaScaledDelta"]
371 if self.config.maxEllipResidual
and not (medianE <= self.config.maxEllipResidual):
372 self.log.info(
"Removing visit %d detector %d because median e residual too large: %f vs %f",
373 row[
"visit"], detectorId, medianE, self.config.maxEllipResidual)
375 elif self.config.maxSizeScatter
and not (scatterSize <= self.config.maxSizeScatter):
376 self.log.info(
"Removing visit %d detector %d because size scatter too large: %f vs %f",
377 row[
"visit"], detectorId, scatterSize, self.config.maxSizeScatter)
379 elif self.config.maxScaledSizeScatter
and not (scaledScatterSize <= self.config.maxScaledSizeScatter):
380 self.log.info(
"Removing visit %d detector %d because scaled size scatter too large: %f vs %f",
381 row[
"visit"], detectorId, scaledScatterSize, self.config.maxScaledSizeScatter)
384 self.config.maxPsfTraceRadiusDelta
is not None
385 and not (psfTraceRadiusDelta <= self.config.maxPsfTraceRadiusDelta)
388 "Removing visit %d detector %d because max-min delta of model PSF trace radius values "
389 "across the unmasked detector pixels is not finite or too large: %.3f vs %.3f (pixels)",
390 row[
"visit"], detectorId, psfTraceRadiusDelta, self.config.maxPsfTraceRadiusDelta
394 self.config.maxPsfApFluxDelta
is not None
395 and not (psfApFluxDelta <= self.config.maxPsfApFluxDelta)
398 "Removing visit %d detector %d because max-min delta of model PSF aperture flux values "
399 "across the unmasked detector pixels is not finite or too large: %.3f vs %.3f (pixels)",
400 row[
"visit"], detectorId, psfApFluxDelta, self.config.maxPsfApFluxDelta
404 self.config.maxPsfApCorrSigmaScaledDelta
is not None
405 and not (psfApCorrSigmaScaledDelta <= self.config.maxPsfApCorrSigmaScaledDelta)
408 "Removing visit %d detector %d because max-min delta of the model PSF apterture correction "
409 "values scaled (divided) by the psfSigma across the unmasked detector pixels is not "
410 "finite or too large: %.3f vs %.3f (fatcor)",
411 row[
"visit"], detectorId, psfApCorrSigmaScaledDelta, self.config.maxPsfApCorrSigmaScaledDelta
418class BestSeeingSelectVisitsConnections(pipeBase.PipelineTaskConnections,
419 dimensions=(
"tract",
"patch",
"skymap",
"band",
"instrument"),
420 defaultTemplates={
"coaddName":
"goodSeeing",
422 skyMap = pipeBase.connectionTypes.Input(
423 doc=
"Input definition of geometry/bbox and projection/wcs for coadded exposures",
424 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
425 storageClass=
"SkyMap",
426 dimensions=(
"skymap",),
428 visitSummaries = pipeBase.connectionTypes.Input(
429 doc=
"Per-visit consolidated exposure metadata",
430 name=
"finalVisitSummary",
431 storageClass=
"ExposureCatalog",
432 dimensions=(
"instrument",
"visit",),
436 goodVisits = pipeBase.connectionTypes.Output(
437 doc=
"Selected visits to be coadded.",
438 name=
"{coaddName}Visits",
439 storageClass=
"StructuredDataDict",
440 dimensions=(
"instrument",
"tract",
"patch",
"skymap",
"band"),
444class BestSeeingSelectVisitsConfig(pipeBase.PipelineTaskConfig,
445 pipelineConnections=BestSeeingSelectVisitsConnections):
446 nVisitsMax = pexConfig.RangeField(
448 doc=
"Maximum number of visits to select; use -1 to select all.",
452 maxPsfFwhm = pexConfig.Field(
454 doc=
"Maximum PSF FWHM (in arcseconds) to select",
458 minPsfFwhm = pexConfig.Field(
460 doc=
"Minimum PSF FWHM (in arcseconds) to select",
464 doConfirmOverlap = pexConfig.Field(
466 doc=
"Do remove visits that do not actually overlap the patch?",
469 minMJD = pexConfig.Field(
471 doc=
"Minimum visit MJD to select",
475 maxMJD = pexConfig.Field(
477 doc=
"Maximum visit MJD to select",
483class BestSeeingSelectVisitsTask(pipeBase.PipelineTask):
484 """Select up to a maximum number of the best-seeing visits.
486 Don't exceed the FWHM range specified by configs min(max)PsfFwhm.
487 This Task is a port of the Gen2 image-selector used in the AP pipeline:
488 BestSeeingSelectImagesTask. This Task selects full visits based on the
489 average PSF of the entire visit.
492 ConfigClass = BestSeeingSelectVisitsConfig
493 _DefaultName =
'bestSeeingSelectVisits'
495 def runQuantum(self, butlerQC, inputRefs, outputRefs):
496 inputs = butlerQC.get(inputRefs)
497 quantumDataId = butlerQC.quantum.dataId
498 outputs = self.run(**inputs, dataId=quantumDataId)
499 butlerQC.put(outputs, outputRefs)
501 def run(self, visitSummaries, skyMap, dataId):
506 visitSummary : `list` [`lsst.pipe.base.connections.DeferredDatasetRef`]
507 List of `lsst.pipe.base.connections.DeferredDatasetRef` of
508 visitSummary tables of type `lsst.afw.table.ExposureCatalog`.
509 skyMap : `lsst.skyMap.SkyMap`
510 SkyMap for checking visits overlap patch.
511 dataId : `dict` of dataId keys
512 For retrieving patch info for checking visits overlap patch.
516 result : `lsst.pipe.base.Struct`
517 Results as a struct with attributes:
520 A `dict` with selected visit ids as keys,
521 so that it can be be saved as a StructuredDataDict.
522 StructuredDataList's are currently limited.
524 if self.config.doConfirmOverlap:
525 patchPolygon = self.makePatchPolygon(skyMap, dataId)
527 inputVisits = [visitSummary.ref.dataId[
'visit']
for visitSummary
in visitSummaries]
530 for visit, visitSummary
in zip(inputVisits, visitSummaries):
532 visitSummary = visitSummary.get()
536 mjd = visitSummary[0].getVisitInfo().getDate().get(system=DateTime.MJD)
538 pixelScales = np.array([vs[
'pixelScale']
for vs
in visitSummary
if vs.getWcs()])
540 psfSigmas = np.array([vs[
'psfSigma']
for vs
in visitSummary
if vs.getWcs()])
541 fwhm = np.nanmean(psfSigmas * pixelScales) * np.sqrt(8.*np.log(2.))
543 if self.config.maxPsfFwhm
and fwhm > self.config.maxPsfFwhm:
545 if self.config.minPsfFwhm
and fwhm < self.config.minPsfFwhm:
547 if self.config.minMJD
and mjd < self.config.minMJD:
548 self.log.debug(
'MJD %f earlier than %.2f; rejecting', mjd, self.config.minMJD)
550 if self.config.maxMJD
and mjd > self.config.maxMJD:
551 self.log.debug(
'MJD %f later than %.2f; rejecting', mjd, self.config.maxMJD)
553 if self.config.doConfirmOverlap
and not self.doesIntersectPolygon(visitSummary, patchPolygon):
556 fwhmSizes.append(fwhm)
559 sortedVisits = [ind
for (_, ind)
in sorted(zip(fwhmSizes, visits))]
560 if self.config.nVisitsMax < 0:
561 output = sortedVisits
563 output = sortedVisits[:self.config.nVisitsMax]
566 self.log.info(
"All images rejected in BestSeeingSelectVisitsTask.")
567 raise pipeBase.NoWorkFound(f
"No good images found for {dataId}")
570 "%d images selected with FWHM range of %f--%f arcseconds",
572 fwhmSizes[visits.index(output[0])],
573 fwhmSizes[visits.index(output[-1])],
577 goodVisits = {key:
True for key
in output}
578 return pipeBase.Struct(goodVisits=goodVisits)
580 def makePatchPolygon(self, skyMap, dataId):
581 """Return True if sky polygon overlaps visit.
585 skyMap : `lsst.afw.table.ExposureCatalog`
586 Exposure catalog with per-detector geometry.
587 dataId : `dict` of dataId keys
588 For retrieving patch info.
592 result : `lsst.sphgeom.ConvexPolygon.convexHull`
593 Polygon of patch's outer bbox.
595 wcs = skyMap[dataId[
'tract']].getWcs()
596 bbox = skyMap[dataId[
'tract']][dataId[
'patch']].getOuterBBox()
601 def doesIntersectPolygon(self, visitSummary, polygon):
602 """Return True if sky polygon overlaps visit.
606 visitSummary : `lsst.afw.table.ExposureCatalog`
607 Exposure catalog with per-detector geometry.
608 polygon :` lsst.sphgeom.ConvexPolygon.convexHull`
609 Polygon to check overlap.
613 doesIntersect : `bool`
614 True if the visit overlaps the polygon.
616 doesIntersect =
False
617 for detectorSummary
in visitSummary:
618 if (np.all(np.isfinite(detectorSummary[
'raCorners']))
619 and np.all(np.isfinite(detectorSummary[
'decCorners']))):
622 zip(detectorSummary[
'raCorners'], detectorSummary[
'decCorners'])]
624 if detectorPolygon.intersects(polygon):
630class BestSeeingQuantileSelectVisitsConfig(pipeBase.PipelineTaskConfig,
631 pipelineConnections=BestSeeingSelectVisitsConnections):
632 qMin = pexConfig.RangeField(
633 doc=
"Lower bound of quantile range to select. Sorts visits by seeing from narrow to wide, "
634 "and select those in the interquantile range (qMin, qMax). Set qMin to 0 for Best Seeing. "
635 "This config should be changed from zero only for exploratory diffIm testing.",
641 qMax = pexConfig.RangeField(
642 doc=
"Upper bound of quantile range to select. Sorts visits by seeing from narrow to wide, "
643 "and select those in the interquantile range (qMin, qMax). Set qMax to 1 for Worst Seeing.",
649 nVisitsMin = pexConfig.Field(
650 doc=
"The minimum number of visits to select, if qMin and qMax alone would have "
651 "selected fewer. In regimes with many visits, at least this number of visits will be "
652 "selected, superceding quantile when necessary. "
653 "For example, if 10 visits cover this patch, qMin=0, qMax=0.33, and nVisitsMin=5, "
654 "the best 5 visits will be selected, even though 5 > 0.33*10. "
655 "In regimes with few visits, all available visits will be selected. "
656 "For example, if 2 visits cover this patch and nVisitsMin=12, "
657 "both visits will be selected regardless of qMin and qMax.",
661 doConfirmOverlap = pexConfig.Field(
663 doc=
"Do remove visits that do not actually overlap the patch?",
666 minMJD = pexConfig.Field(
668 doc=
"Minimum visit MJD to select",
672 maxMJD = pexConfig.Field(
674 doc=
"Maximum visit MJD to select",
680class BestSeeingQuantileSelectVisitsTask(BestSeeingSelectVisitsTask):
681 """Select a quantile of the best-seeing visits.
683 Selects the best (for example, third) full visits based on the average
684 PSF width in the entire visit. It can also be used for difference imaging
685 experiments that require templates with the worst seeing visits.
686 For example, selecting the worst third can be acheived by
687 changing the config parameters qMin to 0.66 and qMax to 1.
689 ConfigClass = BestSeeingQuantileSelectVisitsConfig
690 _DefaultName =
'bestSeeingQuantileSelectVisits'
692 def run(self, visitSummaries, skyMap, dataId):
693 if self.config.doConfirmOverlap:
694 patchPolygon = self.makePatchPolygon(skyMap, dataId)
695 visits = np.array([visitSummary.ref.dataId[
'visit']
for visitSummary
in visitSummaries])
696 radius = np.empty(len(visits))
697 intersects = np.full(len(visits),
True)
698 for i, visitSummary
in enumerate(visitSummaries):
700 visitSummary = visitSummary.get()
702 psfSigma = np.nanmedian([vs[
'psfSigma']
for vs
in visitSummary])
704 if self.config.doConfirmOverlap:
705 intersects[i] = self.doesIntersectPolygon(visitSummary, patchPolygon)
706 if self.config.minMJD
or self.config.maxMJD:
709 mjd = visitSummary[0].getVisitInfo().getDate().get(system=DateTime.MJD)
710 aboveMin = mjd > self.config.minMJD
if self.config.minMJD
else True
711 belowMax = mjd < self.config.maxMJD
if self.config.maxMJD
else True
712 intersects[i] = intersects[i]
and aboveMin
and belowMax
714 sortedVisits = [v
for rad, v
in sorted(zip(radius[intersects], visits[intersects]))]
715 lowerBound =
min(int(np.round(self.config.qMin*len(visits[intersects]))),
716 max(0, len(visits[intersects]) - self.config.nVisitsMin))
717 upperBound =
max(int(np.round(self.config.qMax*len(visits[intersects]))), self.config.nVisitsMin)
720 goodVisits = {int(visit):
True for visit
in sortedVisits[lowerBound:upperBound]}
721 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...
_extractKeyValue(dataList, keys=None)