22__all__ = [
"BaseSelectImagesTask",
"BaseExposureInfo",
"WcsSelectImagesTask",
"PsfWcsSelectImagesTask",
23 "DatabaseSelectImagesConfig",
"BestSeeingSelectVisitsTask",
24 "BestSeeingQuantileSelectVisitsTask"]
35from lsst.utils.timer
import timeMethod
39 """Base configuration for subclasses of BaseSelectImagesTask that use a
43 host = pexConfig.Field(
44 doc="Database server host name",
47 port = pexConfig.Field(
48 doc=
"Database server port",
51 database = pexConfig.Field(
52 doc=
"Name of database",
55 maxExposures = pexConfig.Field(
56 doc=
"maximum exposures to select; intended for debugging; ignored if None",
63 """Data about a selected exposure.
68 Data ID keys of exposure.
69 coordList : `list` [`lsst.afw.geom.SpherePoint`]
70 ICRS coordinates of the corners of the exposure
71 plus any others items that are desired.
75 super(BaseExposureInfo, self).
__init__(dataId=dataId, coordList=coordList)
79 """Base task for selecting images suitable for coaddition.
82 ConfigClass = pexConfig.Config
83 _DefaultName = "selectImages"
86 def run(self, coordList):
87 """Select images suitable for coaddition in a particular region.
92 List of coordinates defining region of interest;
if `
None`, then
93 select all images subclasses may add additional keyword arguments,
98 result : `pipeBase.Struct`
99 Results
as a struct
with attributes:
102 A list of exposure information objects (subclasses of
103 BaseExposureInfo), which have at least the following fields:
104 - dataId: Data ID dictionary (`dict`).
105 - coordList: ICRS coordinates of the corners of the exposure.
108 raise NotImplementedError()
112 """Extract the keys and values from a list of dataIds.
114 The input dataList is a list of objects that have
'dataId' members.
115 This allows it to be used
for both a list of data references
and a
116 list of ExposureInfo.
131 Raised
if DataId keys are inconsistent.
133 assert len(dataList) > 0
135 keys = sorted(dataList[0].dataId.keys())
138 for data
in dataList:
139 thisKeys =
set(data.dataId.keys())
140 if thisKeys != keySet:
141 raise RuntimeError(
"DataId keys inconsistent: %s vs %s" % (keySet, thisKeys))
142 values.append(tuple(data.dataId[k]
for k
in keys))
147 """A container for data to be passed to the WcsSelectImagesTask.
154 Coordinate system definition (wcs).
155 bbox : `lsst.geom.box.Box2I`
156 Integer bounding box for image.
160 super(SelectStruct, self).
__init__(dataRef=dataRef, wcs=wcs, bbox=bbox)
164 """Select images using their Wcs.
167 polygons on the celestial sphere,
and test the polygon of the
168 patch
for overlap
with the polygon of the image.
170 We use
"convexHull" instead of generating a ConvexPolygon
171 directly because the standard
for the inputs to ConvexPolygon
172 are pretty high
and we don
't want to be responsible for reaching them.
175 def run(self, wcsList, bboxList, coordList, dataIds=None, **kwargs):
176 """Return indices of provided lists that meet the selection criteria.
181 Specifying the WCS's of the input ccds to be selected.
183 Specifying the bounding boxes of the input ccds to be selected.
185 ICRS coordinates specifying boundary of the patch.
186 dataIds : iterable [`lsst.daf.butler.dataId`] or `
None`, optional
187 An iterable object of dataIds which point to reference catalogs.
189 Additional keyword arguments.
193 result : `list` [`int`]
194 The indices of selected ccds.
197 dataIds = [
None] * len(wcsList)
198 patchVertices = [coord.getVector()
for coord
in coordList]
201 for i, (imageWcs, imageBox, dataId)
in enumerate(zip(wcsList, bboxList, dataIds)):
203 self.log.info(
"De-selecting exposure %s: Exposure has no WCS.", dataId)
211 """Return corners or `None` if bad.
217 patchPoly : `Unknown`
221 imageCorners = [imageWcs.pixelToSky(pix)
for pix
in geom.Box2D(imageBox).getCorners()]
224 self.log.debug(
"WCS error in testing calexp %s (%s): deselecting", dataId, e)
228 if imagePoly
is None:
229 self.log.debug(
"Unable to create polygon from image %s: deselecting", dataId)
232 if patchPoly.intersects(imagePoly):
234 self.log.info(
"Selecting calexp %s", dataId)
241 dimensions=(
"tract",
"patch",
"skymap",
"instrument",
"visit"),
242 defaultTemplates={
"coaddName":
"deep"}):
246class PsfWcsSelectImagesConfig(pipeBase.PipelineTaskConfig,
247 pipelineConnections=PsfWcsSelectImagesConnections):
248 maxEllipResidual = pexConfig.Field(
249 doc=
"Maximum median ellipticity residual",
254 maxSizeScatter = pexConfig.Field(
255 doc=
"Maximum scatter in the size residuals",
259 maxScaledSizeScatter = pexConfig.Field(
260 doc=
"Maximum scatter in the size residuals, scaled by the median size",
265 maxPsfTraceRadiusDelta = pexConfig.Field(
266 doc=
"Maximum delta (max - min) of model PSF trace radius values evaluated on a grid on "
267 "the unmasked detector pixels (pixel).",
275 """Select images using their Wcs and cuts on the PSF properties.
277 The PSF quality criteria are based on the size and ellipticity
278 residuals
from the adaptive second moments of the star
and the PSF.
281 - the median of the ellipticty residuals.
282 - the robust scatter of the size residuals (using the median absolute
284 - the robust scatter of the size residuals scaled by the square of
288 ConfigClass = PsfWcsSelectImagesConfig
289 _DefaultName = "PsfWcsSelectImages"
291 def run(self, wcsList, bboxList, coordList, visitSummary, dataIds=None, **kwargs):
292 """Return indices of provided lists that meet the selection criteria.
297 Specifying the WCS's of the input ccds to be selected.
299 Specifying the bounding boxes of the input ccds to be selected.
301 ICRS coordinates specifying boundary of the patch.
303 containing the PSF shape information for the input ccds to be
305 dataIds : iterable [`lsst.daf.butler.dataId`]
or `
None`, optional
306 An iterable object of dataIds which point to reference catalogs.
308 Additional keyword arguments.
312 goodPsf : `list` [`int`]
313 The indices of selected ccds.
315 goodWcs = super(PsfWcsSelectImagesTask, self).run(wcsList=wcsList, bboxList=bboxList,
316 coordList=coordList, dataIds=dataIds)
320 for i, dataId
in enumerate(dataIds):
323 if self.isValid(visitSummary, dataId[
"detector"]):
328 def isValid(self, visitSummary, detectorId):
329 """Should this ccd be selected based on its PSF shape information.
334 Exposure catalog with per-detector summary information.
343 row = visitSummary.find(detectorId)
346 self.log.warning(
"Removing detector %d because summary stats not available.", detectorId)
349 medianE = np.sqrt(row[
"psfStarDeltaE1Median"]**2. + row[
"psfStarDeltaE2Median"]**2.)
350 scatterSize = row[
"psfStarDeltaSizeScatter"]
351 scaledScatterSize = row[
"psfStarScaledDeltaSizeScatter"]
352 psfTraceRadiusDelta = row[
"psfTraceRadiusDelta"]
355 if self.config.maxEllipResidual
and not (medianE <= self.config.maxEllipResidual):
356 self.log.info(
"Removing visit %d detector %d because median e residual too large: %f vs %f",
357 row[
"visit"], detectorId, medianE, self.config.maxEllipResidual)
359 elif self.config.maxSizeScatter
and not (scatterSize <= self.config.maxSizeScatter):
360 self.log.info(
"Removing visit %d detector %d because size scatter too large: %f vs %f",
361 row[
"visit"], detectorId, scatterSize, self.config.maxSizeScatter)
363 elif self.config.maxScaledSizeScatter
and not (scaledScatterSize <= self.config.maxScaledSizeScatter):
364 self.log.info(
"Removing visit %d detector %d because scaled size scatter too large: %f vs %f",
365 row[
"visit"], detectorId, scaledScatterSize, self.config.maxScaledSizeScatter)
368 self.config.maxPsfTraceRadiusDelta
is not None
369 and not (psfTraceRadiusDelta <= self.config.maxPsfTraceRadiusDelta)
372 "Removing visit %d detector %d because max-min delta of model PSF trace radius values "
373 "across the unmasked detector pixels is not finite or too large: %.3f vs %.3f (pixels)",
374 row[
"visit"], detectorId, psfTraceRadiusDelta, self.config.maxPsfTraceRadiusDelta
381class BestSeeingSelectVisitsConnections(pipeBase.PipelineTaskConnections,
382 dimensions=(
"tract",
"patch",
"skymap",
"band",
"instrument"),
383 defaultTemplates={
"coaddName":
"goodSeeing",
385 skyMap = pipeBase.connectionTypes.Input(
386 doc=
"Input definition of geometry/bbox and projection/wcs for coadded exposures",
387 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
388 storageClass=
"SkyMap",
389 dimensions=(
"skymap",),
391 visitSummaries = pipeBase.connectionTypes.Input(
392 doc=
"Per-visit consolidated exposure metadata",
393 name=
"finalVisitSummary",
394 storageClass=
"ExposureCatalog",
395 dimensions=(
"instrument",
"visit",),
399 goodVisits = pipeBase.connectionTypes.Output(
400 doc=
"Selected visits to be coadded.",
401 name=
"{coaddName}Visits",
402 storageClass=
"StructuredDataDict",
403 dimensions=(
"instrument",
"tract",
"patch",
"skymap",
"band"),
407class BestSeeingSelectVisitsConfig(pipeBase.PipelineTaskConfig,
408 pipelineConnections=BestSeeingSelectVisitsConnections):
409 nVisitsMax = pexConfig.RangeField(
411 doc=
"Maximum number of visits to select",
415 maxPsfFwhm = pexConfig.Field(
417 doc=
"Maximum PSF FWHM (in arcseconds) to select",
421 minPsfFwhm = pexConfig.Field(
423 doc=
"Minimum PSF FWHM (in arcseconds) to select",
427 doConfirmOverlap = pexConfig.Field(
429 doc=
"Do remove visits that do not actually overlap the patch?",
432 minMJD = pexConfig.Field(
434 doc=
"Minimum visit MJD to select",
438 maxMJD = pexConfig.Field(
440 doc=
"Maximum visit MJD to select",
446class BestSeeingSelectVisitsTask(pipeBase.PipelineTask):
447 """Select up to a maximum number of the best-seeing visits.
449 Don't exceed the FWHM range specified by configs min(max)PsfFwhm.
450 This Task is a port of the Gen2 image-selector used
in the AP pipeline:
451 BestSeeingSelectImagesTask. This Task selects full visits based on the
452 average PSF of the entire visit.
455 ConfigClass = BestSeeingSelectVisitsConfig
456 _DefaultName = 'bestSeeingSelectVisits'
458 def runQuantum(self, butlerQC, inputRefs, outputRefs):
459 inputs = butlerQC.get(inputRefs)
460 quantumDataId = butlerQC.quantum.dataId
461 outputs = self.run(**inputs, dataId=quantumDataId)
462 butlerQC.put(outputs, outputRefs)
464 def run(self, visitSummaries, skyMap, dataId):
469 visitSummary : `list` [`lsst.pipe.base.connections.DeferredDatasetRef`]
470 List of `lsst.pipe.base.connections.DeferredDatasetRef` of
472 skyMap : `lsst.skyMap.SkyMap`
473 SkyMap for checking visits overlap patch.
474 dataId : `dict` of dataId keys
475 For retrieving patch info
for checking visits overlap patch.
479 result : `lsst.pipe.base.Struct`
480 Results
as a struct
with attributes:
483 A `dict`
with selected visit ids
as keys,
484 so that it can be be saved
as a StructuredDataDict.
485 StructuredDataList
's are currently limited.
487 if self.config.doConfirmOverlap:
488 patchPolygon = self.makePatchPolygon(skyMap, dataId)
490 inputVisits = [visitSummary.ref.dataId[
'visit']
for visitSummary
in visitSummaries]
493 for visit, visitSummary
in zip(inputVisits, visitSummaries):
495 visitSummary = visitSummary.get()
499 mjd = visitSummary[0].getVisitInfo().getDate().get(system=DateTime.MJD)
501 pixToArcseconds = [vs.getWcs().getPixelScale(vs.getBBox().getCenter()).asArcseconds()
502 for vs
in visitSummary
if vs.getWcs()]
504 psfSigmas = np.array([vs[
'psfSigma']
for vs
in visitSummary
if vs.getWcs()])
505 fwhm = np.nanmean(psfSigmas * pixToArcseconds) * np.sqrt(8.*np.log(2.))
507 if self.config.maxPsfFwhm
and fwhm > self.config.maxPsfFwhm:
509 if self.config.minPsfFwhm
and fwhm < self.config.minPsfFwhm:
511 if self.config.minMJD
and mjd < self.config.minMJD:
512 self.log.debug(
'MJD %f earlier than %.2f; rejecting', mjd, self.config.minMJD)
514 if self.config.maxMJD
and mjd > self.config.maxMJD:
515 self.log.debug(
'MJD %f later than %.2f; rejecting', mjd, self.config.maxMJD)
517 if self.config.doConfirmOverlap
and not self.doesIntersectPolygon(visitSummary, patchPolygon):
520 fwhmSizes.append(fwhm)
523 sortedVisits = [ind
for (_, ind)
in sorted(zip(fwhmSizes, visits))]
524 output = sortedVisits[:self.config.nVisitsMax]
525 self.log.info(
"%d images selected with FWHM range of %d--%d arcseconds",
526 len(output), fwhmSizes[visits.index(output[0])], fwhmSizes[visits.index(output[-1])])
529 goodVisits = {key:
True for key
in output}
530 return pipeBase.Struct(goodVisits=goodVisits)
532 def makePatchPolygon(self, skyMap, dataId):
533 """Return True if sky polygon overlaps visit.
538 Exposure catalog with per-detector geometry.
539 dataId : `dict` of dataId keys
540 For retrieving patch info.
544 result : `lsst.sphgeom.ConvexPolygon.convexHull`
545 Polygon of patch
's outer bbox.
547 wcs = skyMap[dataId['tract']].getWcs()
548 bbox = skyMap[dataId[
'tract']][dataId[
'patch']].getOuterBBox()
553 def doesIntersectPolygon(self, visitSummary, polygon):
554 """Return True if sky polygon overlaps visit.
559 Exposure catalog with per-detector geometry.
560 polygon :` lsst.sphgeom.ConvexPolygon.convexHull`
561 Polygon to check overlap.
565 doesIntersect : `bool`
566 True if the visit overlaps the polygon.
568 doesIntersect = False
569 for detectorSummary
in visitSummary:
570 if (np.all(np.isfinite(detectorSummary[
'raCorners']))
571 and np.all(np.isfinite(detectorSummary[
'decCorners']))):
574 zip(detectorSummary[
'raCorners'], detectorSummary[
'decCorners'])]
576 if detectorPolygon.intersects(polygon):
582class BestSeeingQuantileSelectVisitsConfig(pipeBase.PipelineTaskConfig,
583 pipelineConnections=BestSeeingSelectVisitsConnections):
584 qMin = pexConfig.RangeField(
585 doc=
"Lower bound of quantile range to select. Sorts visits by seeing from narrow to wide, "
586 "and select those in the interquantile range (qMin, qMax). Set qMin to 0 for Best Seeing. "
587 "This config should be changed from zero only for exploratory diffIm testing.",
593 qMax = pexConfig.RangeField(
594 doc=
"Upper bound of quantile range to select. Sorts visits by seeing from narrow to wide, "
595 "and select those in the interquantile range (qMin, qMax). Set qMax to 1 for Worst Seeing.",
601 nVisitsMin = pexConfig.Field(
602 doc=
"At least this number of visits selected and supercedes quantile. For example, if 10 visits "
603 "cover this patch, qMin=0.33, and nVisitsMin=5, the best 5 visits will be selected.",
607 doConfirmOverlap = pexConfig.Field(
609 doc=
"Do remove visits that do not actually overlap the patch?",
612 minMJD = pexConfig.Field(
614 doc=
"Minimum visit MJD to select",
618 maxMJD = pexConfig.Field(
620 doc=
"Maximum visit MJD to select",
626class BestSeeingQuantileSelectVisitsTask(BestSeeingSelectVisitsTask):
627 """Select a quantile of the best-seeing visits.
629 Selects the best (for example, third) full visits based on the average
630 PSF width
in the entire visit. It can also be used
for difference imaging
631 experiments that require templates
with the worst seeing visits.
632 For example, selecting the worst third can be acheived by
633 changing the config parameters qMin to 0.66
and qMax to 1.
635 ConfigClass = BestSeeingQuantileSelectVisitsConfig
636 _DefaultName = 'bestSeeingQuantileSelectVisits'
638 @utils.inheritDoc(BestSeeingSelectVisitsTask)
639 def run(self, visitSummaries, skyMap, dataId):
640 if self.config.doConfirmOverlap:
641 patchPolygon = self.makePatchPolygon(skyMap, dataId)
642 visits = np.array([visitSummary.ref.dataId[
'visit']
for visitSummary
in visitSummaries])
643 radius = np.empty(len(visits))
644 intersects = np.full(len(visits),
True)
645 for i, visitSummary
in enumerate(visitSummaries):
647 visitSummary = visitSummary.get()
649 psfSigma = np.nanmedian([vs[
'psfSigma']
for vs
in visitSummary])
651 if self.config.doConfirmOverlap:
652 intersects[i] = self.doesIntersectPolygon(visitSummary, patchPolygon)
653 if self.config.minMJD
or self.config.maxMJD:
656 mjd = visitSummary[0].getVisitInfo().getDate().get(system=DateTime.MJD)
657 aboveMin = mjd > self.config.minMJD
if self.config.minMJD
else True
658 belowMax = mjd < self.config.maxMJD
if self.config.maxMJD
else True
659 intersects[i] = intersects[i]
and aboveMin
and belowMax
661 sortedVisits = [v
for rad, v
in sorted(zip(radius[intersects], visits[intersects]))]
662 lowerBound =
min(int(np.round(self.config.qMin*len(visits[intersects]))),
663 max(0, len(visits[intersects]) - self.config.nVisitsMin))
664 upperBound =
max(int(np.round(self.config.qMax*len(visits[intersects]))), self.config.nVisitsMin)
667 goodVisits = {int(visit):
True for visit
in sortedVisits[lowerBound:upperBound]}
668 return pipeBase.Struct(goodVisits=goodVisits)
A 2-dimensional celestial WCS that transform pixels to ICRS RA/Dec, using the LSST standard for pixel...
Custom catalog class for ExposureRecord/Table.
A floating-point coordinate rectangle geometry.
An integer coordinate rectangle.
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)
getValidImageCorners(self, imageWcs, imageBox, patchPoly, dataId=None)
ConvexPolygon is a closed convex polygon on the unit sphere.
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::PropertyList * list
daf::base::PropertySet * set
_extractKeyValue(dataList, keys=None)