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()
111def _extractKeyValue(dataList, keys=None):
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",
268 """Select images using their Wcs and cuts on the PSF properties.
270 The PSF quality criteria are based on the size and ellipticity
271 residuals
from the adaptive second moments of the star
and the PSF.
274 - the median of the ellipticty residuals.
275 - the robust scatter of the size residuals (using the median absolute
277 - the robust scatter of the size residuals scaled by the square of
281 ConfigClass = PsfWcsSelectImagesConfig
282 _DefaultName = "PsfWcsSelectImages"
284 def run(self, wcsList, bboxList, coordList, visitSummary, dataIds=None, **kwargs):
285 """Return indices of provided lists that meet the selection criteria.
290 Specifying the WCS's of the input ccds to be selected.
292 Specifying the bounding boxes of the input ccds to be selected.
294 ICRS coordinates specifying boundary of the patch.
296 containing the PSF shape information for the input ccds to be
298 dataIds : iterable [`lsst.daf.butler.dataId`]
or `
None`, optional
299 An iterable object of dataIds which point to reference catalogs.
301 Additional keyword arguments.
305 goodPsf : `list` [`int`]
306 The indices of selected ccds.
308 goodWcs = super(PsfWcsSelectImagesTask, self).run(wcsList=wcsList, bboxList=bboxList,
309 coordList=coordList, dataIds=dataIds)
313 for i, dataId
in enumerate(dataIds):
316 if self.isValid(visitSummary, dataId[
"detector"]):
321 def isValid(self, visitSummary, detectorId):
322 """Should this ccd be selected based on its PSF shape information.
327 Exposure catalog with per-detector summary information.
336 row = visitSummary.find(detectorId)
339 self.log.warning(
"Removing detector %d because summary stats not available.", detectorId)
342 medianE = np.sqrt(row[
"psfStarDeltaE1Median"]**2. + row[
"psfStarDeltaE2Median"]**2.)
343 scatterSize = row[
"psfStarDeltaSizeScatter"]
344 scaledScatterSize = row[
"psfStarScaledDeltaSizeScatter"]
347 if self.config.maxEllipResidual
and medianE > self.config.maxEllipResidual:
348 self.log.info(
"Removing visit %d detector %d because median e residual too large: %f vs %f",
349 row[
"visit"], detectorId, medianE, self.config.maxEllipResidual)
351 elif self.config.maxSizeScatter
and scatterSize > self.config.maxSizeScatter:
352 self.log.info(
"Removing visit %d detector %d because size scatter too large: %f vs %f",
353 row[
"visit"], detectorId, scatterSize, self.config.maxSizeScatter)
355 elif self.config.maxScaledSizeScatter
and scaledScatterSize > self.config.maxScaledSizeScatter:
356 self.log.info(
"Removing visit %d detector %d because scaled size scatter too large: %f vs %f",
357 row[
"visit"], detectorId, scaledScatterSize, self.config.maxScaledSizeScatter)
363class BestSeeingSelectVisitsConnections(pipeBase.PipelineTaskConnections,
364 dimensions=(
"tract",
"patch",
"skymap",
"band",
"instrument"),
365 defaultTemplates={
"coaddName":
"goodSeeing"}):
366 skyMap = pipeBase.connectionTypes.Input(
367 doc=
"Input definition of geometry/bbox and projection/wcs for coadded exposures",
368 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
369 storageClass=
"SkyMap",
370 dimensions=(
"skymap",),
372 visitSummaries = pipeBase.connectionTypes.Input(
373 doc=
"Per-visit consolidated exposure metadata from ConsolidateVisitSummaryTask",
375 storageClass=
"ExposureCatalog",
376 dimensions=(
"instrument",
"visit",),
380 goodVisits = pipeBase.connectionTypes.Output(
381 doc=
"Selected visits to be coadded.",
382 name=
"{coaddName}Visits",
383 storageClass=
"StructuredDataDict",
384 dimensions=(
"instrument",
"tract",
"patch",
"skymap",
"band"),
388class BestSeeingSelectVisitsConfig(pipeBase.PipelineTaskConfig,
389 pipelineConnections=BestSeeingSelectVisitsConnections):
390 nVisitsMax = pexConfig.RangeField(
392 doc=
"Maximum number of visits to select",
396 maxPsfFwhm = pexConfig.Field(
398 doc=
"Maximum PSF FWHM (in arcseconds) to select",
402 minPsfFwhm = pexConfig.Field(
404 doc=
"Minimum PSF FWHM (in arcseconds) to select",
408 doConfirmOverlap = pexConfig.Field(
410 doc=
"Do remove visits that do not actually overlap the patch?",
413 minMJD = pexConfig.Field(
415 doc=
"Minimum visit MJD to select",
419 maxMJD = pexConfig.Field(
421 doc=
"Maximum visit MJD to select",
427class BestSeeingSelectVisitsTask(pipeBase.PipelineTask):
428 """Select up to a maximum number of the best-seeing visits.
430 Don't exceed the FWHM range specified by configs min(max)PsfFwhm.
431 This Task is a port of the Gen2 image-selector used
in the AP pipeline:
432 BestSeeingSelectImagesTask. This Task selects full visits based on the
433 average PSF of the entire visit.
436 ConfigClass = BestSeeingSelectVisitsConfig
437 _DefaultName = 'bestSeeingSelectVisits'
439 def runQuantum(self, butlerQC, inputRefs, outputRefs):
440 inputs = butlerQC.get(inputRefs)
441 quantumDataId = butlerQC.quantum.dataId
442 outputs = self.run(**inputs, dataId=quantumDataId)
443 butlerQC.put(outputs, outputRefs)
445 def run(self, visitSummaries, skyMap, dataId):
450 visitSummary : `list` [`lsst.pipe.base.connections.DeferredDatasetRef`]
451 List of `lsst.pipe.base.connections.DeferredDatasetRef` of
453 skyMap : `lsst.skyMap.SkyMap`
454 SkyMap for checking visits overlap patch.
455 dataId : `dict` of dataId keys
456 For retrieving patch info
for checking visits overlap patch.
460 result : `lsst.pipe.base.Struct`
461 Results
as a struct
with attributes:
464 A `dict`
with selected visit ids
as keys,
465 so that it can be be saved
as a StructuredDataDict.
466 StructuredDataList
's are currently limited.
468 if self.config.doConfirmOverlap:
469 patchPolygon = self.makePatchPolygon(skyMap, dataId)
471 inputVisits = [visitSummary.ref.dataId[
'visit']
for visitSummary
in visitSummaries]
474 for visit, visitSummary
in zip(inputVisits, visitSummaries):
476 visitSummary = visitSummary.get()
480 mjd = visitSummary[0].getVisitInfo().getDate().get(system=DateTime.MJD)
482 pixToArcseconds = [vs.getWcs().getPixelScale(vs.getBBox().getCenter()).asArcseconds()
483 for vs
in visitSummary
if vs.getWcs()]
485 psfSigmas = np.array([vs[
'psfSigma']
for vs
in visitSummary
if vs.getWcs()])
486 fwhm = np.nanmean(psfSigmas * pixToArcseconds) * np.sqrt(8.*np.log(2.))
488 if self.config.maxPsfFwhm
and fwhm > self.config.maxPsfFwhm:
490 if self.config.minPsfFwhm
and fwhm < self.config.minPsfFwhm:
492 if self.config.minMJD
and mjd < self.config.minMJD:
493 self.log.debug(
'MJD %f earlier than %.2f; rejecting', mjd, self.config.minMJD)
495 if self.config.maxMJD
and mjd > self.config.maxMJD:
496 self.log.debug(
'MJD %f later than %.2f; rejecting', mjd, self.config.maxMJD)
498 if self.config.doConfirmOverlap
and not self.doesIntersectPolygon(visitSummary, patchPolygon):
501 fwhmSizes.append(fwhm)
504 sortedVisits = [ind
for (_, ind)
in sorted(zip(fwhmSizes, visits))]
505 output = sortedVisits[:self.config.nVisitsMax]
506 self.log.info(
"%d images selected with FWHM range of %d--%d arcseconds",
507 len(output), fwhmSizes[visits.index(output[0])], fwhmSizes[visits.index(output[-1])])
510 goodVisits = {key:
True for key
in output}
511 return pipeBase.Struct(goodVisits=goodVisits)
513 def makePatchPolygon(self, skyMap, dataId):
514 """Return True if sky polygon overlaps visit.
519 Exposure catalog with per-detector geometry.
520 dataId : `dict` of dataId keys
521 For retrieving patch info.
525 result : `lsst.sphgeom.ConvexPolygon.convexHull`
526 Polygon of patch
's outer bbox.
528 wcs = skyMap[dataId['tract']].getWcs()
529 bbox = skyMap[dataId[
'tract']][dataId[
'patch']].getOuterBBox()
534 def doesIntersectPolygon(self, visitSummary, polygon):
535 """Return True if sky polygon overlaps visit.
540 Exposure catalog with per-detector geometry.
541 polygon :` lsst.sphgeom.ConvexPolygon.convexHull`
542 Polygon to check overlap.
546 doesIntersect : `bool`
547 True if the visit overlaps the polygon.
549 doesIntersect = False
550 for detectorSummary
in visitSummary:
551 if (np.all(np.isfinite(detectorSummary[
'raCorners']))
552 and np.all(np.isfinite(detectorSummary[
'decCorners']))):
555 zip(detectorSummary[
'raCorners'], detectorSummary[
'decCorners'])]
557 if detectorPolygon.intersects(polygon):
563class BestSeeingQuantileSelectVisitsConfig(pipeBase.PipelineTaskConfig,
564 pipelineConnections=BestSeeingSelectVisitsConnections):
565 qMin = pexConfig.RangeField(
566 doc=
"Lower bound of quantile range to select. Sorts visits by seeing from narrow to wide, "
567 "and select those in the interquantile range (qMin, qMax). Set qMin to 0 for Best Seeing. "
568 "This config should be changed from zero only for exploratory diffIm testing.",
574 qMax = pexConfig.RangeField(
575 doc=
"Upper bound of quantile range to select. Sorts visits by seeing from narrow to wide, "
576 "and select those in the interquantile range (qMin, qMax). Set qMax to 1 for Worst Seeing.",
582 nVisitsMin = pexConfig.Field(
583 doc=
"At least this number of visits selected and supercedes quantile. For example, if 10 visits "
584 "cover this patch, qMin=0.33, and nVisitsMin=5, the best 5 visits will be selected.",
588 doConfirmOverlap = pexConfig.Field(
590 doc=
"Do remove visits that do not actually overlap the patch?",
593 minMJD = pexConfig.Field(
595 doc=
"Minimum visit MJD to select",
599 maxMJD = pexConfig.Field(
601 doc=
"Maximum visit MJD to select",
607class BestSeeingQuantileSelectVisitsTask(BestSeeingSelectVisitsTask):
608 """Select a quantile of the best-seeing visits.
610 Selects the best (for example, third) full visits based on the average
611 PSF width
in the entire visit. It can also be used
for difference imaging
612 experiments that require templates
with the worst seeing visits.
613 For example, selecting the worst third can be acheived by
614 changing the config parameters qMin to 0.66
and qMax to 1.
616 ConfigClass = BestSeeingQuantileSelectVisitsConfig
617 _DefaultName = 'bestSeeingQuantileSelectVisits'
619 @utils.inheritDoc(BestSeeingSelectVisitsTask)
620 def run(self, visitSummaries, skyMap, dataId):
621 if self.config.doConfirmOverlap:
622 patchPolygon = self.makePatchPolygon(skyMap, dataId)
623 visits = np.array([visitSummary.ref.dataId[
'visit']
for visitSummary
in visitSummaries])
624 radius = np.empty(len(visits))
625 intersects = np.full(len(visits),
True)
626 for i, visitSummary
in enumerate(visitSummaries):
628 visitSummary = visitSummary.get()
630 psfSigma = np.nanmedian([vs[
'psfSigma']
for vs
in visitSummary])
632 if self.config.doConfirmOverlap:
633 intersects[i] = self.doesIntersectPolygon(visitSummary, patchPolygon)
634 if self.config.minMJD
or self.config.maxMJD:
637 mjd = visitSummary[0].getVisitInfo().getDate().get(system=DateTime.MJD)
638 aboveMin = mjd > self.config.minMJD
if self.config.minMJD
else True
639 belowMax = mjd < self.config.maxMJD
if self.config.maxMJD
else True
640 intersects[i] = intersects[i]
and aboveMin
and belowMax
642 sortedVisits = [v
for rad, v
in sorted(zip(radius[intersects], visits[intersects]))]
643 lowerBound =
min(int(np.round(self.config.qMin*len(visits[intersects]))),
644 max(0, len(visits[intersects]) - self.config.nVisitsMin))
645 upperBound =
max(int(np.round(self.config.qMax*len(visits[intersects]))), self.config.nVisitsMin)
648 goodVisits = {int(visit):
True for visit
in sortedVisits[lowerBound:upperBound]}
649 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.
def __init__(self, dataId, coordList)
def __init__(self, dataRef, wcs, bbox)
def 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