22__all__ = [
"BaseSelectImagesTask",
"BaseExposureInfo",
"WcsSelectImagesTask",
"PsfWcsSelectImagesTask",
23 "DatabaseSelectImagesConfig",
"BestSeeingSelectVisitsTask",
24 "BestSeeingQuantileSelectVisitsTask"]
31import lsst.pipe.base
as pipeBase
34from lsst.utils.timer
import timeMethod
38 """Check if the visitSummary meets minimum values in visitSummaryMinValues.
44 visitSummary : `lsst.afw.table.ExposureCatalog`
45 Exposure catalog with per-detector summary information.
46 visitSummaryMinValues : `dict`
47 Dictionary with column names as keys and minimum allowed values as values.
48 logger : `lsst.log.Logger`
49 Logger to log debug and warning messages.
54 True if all columns in visitSummary meet the minimum values specified
55 in visitSummaryMinValues, False otherwise.
57 for columnName, minThreshold
in visitSummaryMinValues.items():
59 values = np.asarray([vs[columnName]
for vs
in visitSummary
if vs.getWcs()])
60 meanValue = np.nanmean(values)
61 if meanValue < minThreshold:
62 if logger
is not None:
63 logger.info(f
'Rejecting visit {visit}: mean {columnName} ({meanValue:.3f}) '
64 f
'is below the minimum threshold ({minThreshold:.3f}).')
70 """Base configuration for subclasses of BaseSelectImagesTask that use a
74 host = pexConfig.Field(
75 doc=
"Database server host name",
78 port = pexConfig.Field(
79 doc=
"Database server port",
82 database = pexConfig.Field(
83 doc=
"Name of database",
86 maxExposures = pexConfig.Field(
87 doc=
"maximum exposures to select; intended for debugging; ignored if None",
94 """Data about a selected exposure.
99 Data ID keys of exposure.
100 coordList : `list` [`lsst.afw.geom.SpherePoint`]
101 ICRS coordinates of the corners of the exposure
102 plus any others items that are desired.
106 super(BaseExposureInfo, self).
__init__(dataId=dataId, coordList=coordList)
110 """Base task for selecting images suitable for coaddition.
113 ConfigClass = pexConfig.Config
114 _DefaultName =
"selectImages"
118 """Select images suitable for coaddition in a particular region.
122 coordList : `list` [`lsst.geom.SpherePoint`] or `None`
123 List of coordinates defining region of interest; if `None`, then
124 select all images subclasses may add additional keyword arguments,
129 result : `pipeBase.Struct`
130 Results as a struct with attributes:
133 A list of exposure information objects (subclasses of
134 BaseExposureInfo), which have at least the following fields:
135 - dataId: Data ID dictionary (`dict`).
136 - coordList: ICRS coordinates of the corners of the exposure.
137 (`list` [`lsst.geom.SpherePoint`])
139 raise NotImplementedError()
143 """Extract the keys and values from a list of dataIds.
145 The input dataList is a list of objects that have 'dataId' members.
146 This allows it to be used for both a list of data references and a
147 list of ExposureInfo.
162 Raised if DataId keys are inconsistent.
164 assert len(dataList) > 0
166 keys = sorted(dataList[0].dataId.keys())
169 for data
in dataList:
170 thisKeys = set(data.dataId.keys())
171 if thisKeys != keySet:
172 raise RuntimeError(
"DataId keys inconsistent: %s vs %s" % (keySet, thisKeys))
173 values.append(tuple(data.dataId[k]
for k
in keys))
178 """A container for data to be passed to the WcsSelectImagesTask.
184 wcs : `lsst.afw.geom.SkyWcs`
185 Coordinate system definition (wcs).
186 bbox : `lsst.geom.box.Box2I`
187 Integer bounding box for image.
191 super(SelectStruct, self).
__init__(dataRef=dataRef, wcs=wcs, bbox=bbox)
195 excludeDetectors = pexConfig.ListField(
198 doc=
"Detectors to exclude from selection.",
204 """Select images using their Wcs.
206 We use the "convexHull" method of lsst.sphgeom.ConvexPolygon to define
207 polygons on the celestial sphere, and test the polygon of the
208 patch for overlap with the polygon of the image.
210 We use "convexHull" instead of generating a ConvexPolygon
211 directly because the standard for the inputs to ConvexPolygon
212 are pretty high and we don't want to be responsible for reaching them.
215 ConfigClass = WcsSelectImagesConfig
216 _DefaultName =
"WcsSelectImages"
218 def run(self, wcsList, bboxList, coordList, dataIds=None, **kwargs):
219 """Return indices of provided lists that meet the selection criteria.
223 wcsList : `list` [`lsst.afw.geom.SkyWcs`]
224 Specifying the WCS's of the input ccds to be selected.
225 bboxList : `list` [`lsst.geom.Box2I`]
226 Specifying the bounding boxes of the input ccds to be selected.
227 coordList : `list` [`lsst.geom.SpherePoint`]
228 ICRS coordinates specifying boundary of the patch.
229 dataIds : iterable [`lsst.daf.butler.dataId`] or `None`, optional
230 An iterable object of dataIds which point to reference catalogs.
232 Additional keyword arguments.
236 result : `list` [`int`]
237 The indices of selected ccds.
240 dataIds = [
None] * len(wcsList)
241 patchVertices = [coord.getVector()
for coord
in coordList]
244 for i, (imageWcs, imageBox, dataId)
in enumerate(zip(wcsList, bboxList, dataIds)):
245 if dataId
and (
"detector" in dataId)
and (dataId[
"detector"]
in self.config.excludeDetectors):
246 self.log.info(
"De-selecting exposure %s because detector %s is excluded from processing",
247 dataId, dataId[
"detector"])
248 elif imageWcs
is None:
249 self.log.info(
"De-selecting exposure %s: Exposure has no WCS.", dataId)
257 """Return corners or `None` if bad.
263 patchPoly : `Unknown`
267 imageCorners = [imageWcs.pixelToSky(pix)
for pix
in geom.Box2D(imageBox).getCorners()]
268 except (pexExceptions.DomainError, pexExceptions.RuntimeError)
as e:
270 self.log.debug(
"WCS error in testing calexp %s (%s): deselecting", dataId, e)
274 if imagePoly
is None:
275 self.log.debug(
"Unable to create polygon from image %s: deselecting", dataId)
278 if patchPoly.intersects(imagePoly):
280 self.log.info(
"Selecting calexp %s", dataId)
287 dimensions=(
"tract",
"patch",
"skymap",
"instrument",
"visit"),
288 defaultTemplates={
"coaddName":
"deep"}):
292class PsfWcsSelectImagesConfig(pipeBase.PipelineTaskConfig,
293 pipelineConnections=PsfWcsSelectImagesConnections):
294 maxPsfFwhm = pexConfig.Field(
295 doc=
"Maximum PSF FWHM (in arcseconds) to select. This can be used in conjunction with the "
296 "per-visit maxPsfFwhm in BestSeeingSelectVisitsTask to ensure that no outlier CCD has a "
297 "PSF FWHM larger than this value.",
302 maxEllipResidual = pexConfig.Field(
303 doc=
"Maximum median ellipticity residual",
308 maxSizeScatter = pexConfig.Field(
309 doc=
"Maximum scatter in the size residuals",
313 maxScaledSizeScatter = pexConfig.Field(
314 doc=
"Maximum scatter in the size residuals, scaled by the median size",
319 maxPsfTraceRadiusDelta = pexConfig.Field(
320 doc=
"Maximum delta (max - min) of model PSF trace radius values evaluated on a grid on "
321 "the unmasked detector pixels (pixel).",
326 maxPsfApFluxDelta = pexConfig.Field(
327 doc=
"Maximum delta (max - min) of model PSF aperture flux (with aperture radius of "
328 "max(2, 3*psfSigma)) values evaluated on a grid on the unmasked detector pixels (based "
329 "on a normalized-to-one flux).",
334 maxPsfApCorrSigmaScaledDelta = pexConfig.Field(
335 doc=
"Maximum delta (max - min) of model PSF aperture correction values evaluated on a grid "
336 "on the unmasked detector pixels scaled (divided) by the measured model psfSigma.",
341 minNPsfStarPerBand = pexConfig.DictField(
353 doc=
"Minimum number of PSF stars for the final PSF model to be considered "
354 "well-constrained and suitible for inclusion in the coadd. This number should "
355 "take into consideration the spatial order used for the PSF model. If the current "
356 "band for the exposure is not included as a key in this dict, the value associated "
357 "with the \"fallback\" key will be used.",
359 maxStarEPerBand = pexConfig.DictField(
371 doc=
"Maximum median of the ellipticity (sqrt(starE1**2.0 + starE2**2.0)) "
372 "distribution of the star sources used in the PSF model. If the current band "
373 "for the exposure is not included as a key in this dict, the value associated "
374 "with the \"fallback\" key will be used.",
376 maxStarUnNormalizedEPerBand = pexConfig.DictField(
388 doc=
"Maximum median of the unnormalized ellipticity "
389 "(sqrt((starXX - starYY)**2.0 + (2.0*starXY)**2.0)) distribution of the star "
390 "sources used in the PSF model. If the current band for the exposure is not "
391 "included as a key in this dict, the value associated with the \"fallback\" key "
394 excludeDetectors = pexConfig.ListField(
397 doc=
"Detectors to exclude from selection.",
403 if "fallback" not in self.minNPsfStarPerBand:
404 msg = (
"Must include a \"fallback\" key in the config.minNPsfStarPerBand config dict. "
405 f
"It is currently: {self.minNPsfStarPerBand}.")
406 raise pexConfig.FieldValidationError(self.__class__.minNPsfStarPerBand, self, msg)
407 if "fallback" not in self.maxStarEPerBand:
408 msg = (
"Must include a \"fallback\" key in the config.maxStarEPerBand config dict. "
409 f
"It is currently: {self.maxStarEPerBand}.")
410 raise pexConfig.FieldValidationError(self.__class__.maxStarEPerBand, self, msg)
411 if "fallback" not in self.maxStarUnNormalizedEPerBand:
412 msg = (
"Must include a \"fallback\" key in the config.maxStarUnNormalizedEPerBand "
413 f
"config dict. It is currently: {self.maxStarUnNormalizedEPerBand}.")
414 raise pexConfig.FieldValidationError(self.__class__.maxStarUnNormalizedEPerBand, self, msg)
418 """Select images using their Wcs and cuts on the PSF properties.
420 The PSF quality criteria are based on the size and ellipticity
421 residuals from the adaptive second moments of the star and the PSF.
424 - the median of the ellipticty residuals.
425 - the robust scatter of the size residuals (using the median absolute
427 - the robust scatter of the size residuals scaled by the square of
431 ConfigClass = PsfWcsSelectImagesConfig
432 _DefaultName =
"PsfWcsSelectImages"
434 def run(self, wcsList, bboxList, coordList, visitSummary, dataIds=None, **kwargs):
435 """Return indices of provided lists that meet the selection criteria.
439 wcsList : `list` [`lsst.afw.geom.SkyWcs`]
440 Specifying the WCS's of the input ccds to be selected.
441 bboxList : `list` [`lsst.geom.Box2I`]
442 Specifying the bounding boxes of the input ccds to be selected.
443 coordList : `list` [`lsst.geom.SpherePoint`]
444 ICRS coordinates specifying boundary of the patch.
445 visitSummary : `list` [`lsst.afw.table.ExposureCatalog`]
446 containing the PSF shape information for the input ccds to be
448 dataIds : iterable [`lsst.daf.butler.dataId`] or `None`, optional
449 An iterable object of dataIds which point to reference catalogs.
451 Additional keyword arguments.
455 goodPsf : `list` [`int`]
456 The indices of selected ccds.
458 goodWcs = super(PsfWcsSelectImagesTask, self).run(wcsList=wcsList, bboxList=bboxList,
459 coordList=coordList, dataIds=dataIds)
463 for i, dataId
in enumerate(dataIds):
466 if self.
isValid(visitSummary, dataId[
"detector"]):
472 """Should this ccd be selected based on its PSF shape information.
476 visitSummary : `lsst.afw.table.ExposureCatalog`
477 Exposure catalog with per-detector summary information.
486 row = visitSummary.find(detectorId)
489 self.log.warning(
"Removing detector %d because summary stats not available.", detectorId)
493 if band
in self.config.minNPsfStarPerBand:
494 minNPsfStar = self.config.minNPsfStarPerBand[band]
496 minNPsfStar = self.config.minNPsfStarPerBand[
"fallback"]
497 if band
in self.config.maxStarEPerBand:
498 maxStarE = self.config.maxStarEPerBand[band]
500 maxStarE = self.config.maxStarEPerBand[
"fallback"]
501 if band
in self.config.maxStarUnNormalizedEPerBand:
502 maxStarUnNormalizedE = self.config.maxStarUnNormalizedEPerBand[band]
504 maxStarUnNormalizedE = self.config.maxStarUnNormalizedEPerBand[
"fallback"]
505 nPsfStar = row[
"nPsfStar"]
506 medianE = np.sqrt(row[
"psfStarDeltaE1Median"]**2. + row[
"psfStarDeltaE2Median"]**2.)
507 scatterSize = row[
"psfStarDeltaSizeScatter"]
508 scaledScatterSize = row[
"psfStarScaledDeltaSizeScatter"]
509 psfTraceRadiusDelta = row[
"psfTraceRadiusDelta"]
510 psfApFluxDelta = row[
"psfApFluxDelta"]
511 psfApCorrSigmaScaledDelta = row[
"psfApCorrSigmaScaledDelta"]
512 starEMedian = row[
"starEMedian"]
513 starUnNormalizedEMedian = row[
"starUnNormalizedEMedian"]
520 if self.config.maxPsfFwhm:
521 pixelScale = row[
'pixelScale']
522 psfSigma = row[
'psfSigma']
523 fwhm = psfSigma * pixelScale * np.sqrt(8.*np.log(2.))
524 if not (fwhm <= self.config.maxPsfFwhm):
525 self.log.info(
"Removing visit %d detector %d because FWHM too large: %f vs %f",
526 row[
"visit"], detectorId, fwhm, self.config.maxPsfFwhm)
530 if self.config.maxEllipResidual
and not (medianE <= self.config.maxEllipResidual):
531 self.log.info(
"Removing visit %d detector %d because median e residual too large: %f vs %f",
532 row[
"visit"], detectorId, medianE, self.config.maxEllipResidual)
534 elif self.config.maxSizeScatter
and not (scatterSize <= self.config.maxSizeScatter):
535 self.log.info(
"Removing visit %d detector %d because size scatter too large: %f vs %f",
536 row[
"visit"], detectorId, scatterSize, self.config.maxSizeScatter)
538 elif self.config.maxScaledSizeScatter
and not (scaledScatterSize <= self.config.maxScaledSizeScatter):
539 self.log.info(
"Removing visit %d detector %d because scaled size scatter too large: %f vs %f",
540 row[
"visit"], detectorId, scaledScatterSize, self.config.maxScaledSizeScatter)
543 self.config.maxPsfTraceRadiusDelta
is not None
544 and not (psfTraceRadiusDelta <= self.config.maxPsfTraceRadiusDelta)
547 "Removing visit %d detector %d because max-min delta of model PSF trace radius values "
548 "across the unmasked detector pixels is not finite or too large: %.3f vs %.3f (pixels)",
549 row[
"visit"], detectorId, psfTraceRadiusDelta, self.config.maxPsfTraceRadiusDelta
553 self.config.maxPsfApFluxDelta
is not None
554 and not (psfApFluxDelta <= self.config.maxPsfApFluxDelta)
557 "Removing visit %d detector %d because max-min delta of model PSF aperture flux values "
558 "across the unmasked detector pixels is not finite or too large: %.3f vs %.3f (pixels)",
559 row[
"visit"], detectorId, psfApFluxDelta, self.config.maxPsfApFluxDelta
563 self.config.maxPsfApCorrSigmaScaledDelta
is not None
564 and not (psfApCorrSigmaScaledDelta <= self.config.maxPsfApCorrSigmaScaledDelta)
567 "Removing visit %d detector %d because max-min delta of the model PSF aperture correction "
568 "values scaled (divided) by the psfSigma across the unmasked detector pixels is not "
569 "finite or too large: %.3f vs %.3f (factor)",
570 row[
"visit"], detectorId, psfApCorrSigmaScaledDelta, self.config.maxPsfApCorrSigmaScaledDelta
573 elif minNPsfStar
and (nPsfStar < minNPsfStar):
575 "Removing visit %d detector %d because the PSF model had too few stars: %d vs %d",
576 row[
"visit"], detectorId, nPsfStar, minNPsfStar
579 elif maxStarE
and not (starEMedian <= maxStarE):
581 "Removing visit %d detector %d because the median star ellipticity is too large: "
583 row[
"visit"], detectorId, starEMedian, maxStarE
586 elif maxStarUnNormalizedE
and not (starUnNormalizedEMedian <= maxStarUnNormalizedE):
588 "Removing visit %d detector %d because the median star unnormalized ellipticity "
589 "is too large: %.2f vs %.2f",
590 row[
"visit"], detectorId, starUnNormalizedEMedian, maxStarUnNormalizedE
598 dimensions=(
"tract",
"patch",
"skymap",
"band",
"instrument"),
599 defaultTemplates={
"coaddName":
"goodSeeing",
601 skyMap = pipeBase.connectionTypes.Input(
602 doc=
"Input definition of geometry/bbox and projection/wcs for coadded exposures",
603 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
604 storageClass=
"SkyMap",
605 dimensions=(
"skymap",),
607 visitSummaries = pipeBase.connectionTypes.Input(
608 doc=
"Per-visit consolidated exposure metadata",
609 name=
"finalVisitSummary",
610 storageClass=
"ExposureCatalog",
611 dimensions=(
"instrument",
"visit",),
615 goodVisits = pipeBase.connectionTypes.Output(
616 doc=
"Selected visits to be coadded.",
617 name=
"{coaddName}Visits",
618 storageClass=
"StructuredDataDict",
619 dimensions=(
"instrument",
"tract",
"patch",
"skymap",
"band"),
623class BestSeeingSelectVisitsConfig(pipeBase.PipelineTaskConfig,
624 pipelineConnections=BestSeeingSelectVisitsConnections):
625 nVisitsMax = pexConfig.RangeField(
627 doc=
"Maximum number of visits to select; use -1 to select all.",
631 maxPsfFwhm = pexConfig.Field(
633 doc=
"Maximum PSF FWHM (in arcseconds) to select",
637 minPsfFwhm = pexConfig.Field(
639 doc=
"Minimum PSF FWHM (in arcseconds) to select",
643 doConfirmOverlap = pexConfig.Field(
645 doc=
"Do remove visits that do not actually overlap the patch?",
648 minMJD = pexConfig.Field(
650 doc=
"Minimum visit MJD to select",
654 maxMJD = pexConfig.Field(
656 doc=
"Maximum visit MJD to select",
660 visitSummaryMinValues = pexConfig.DictField(
663 doc=(
"Optional thresholds for visit selection. Keys are visit_summary column names"
664 "(e.g. 'effTimeZeroPointScale'), and values are minimum allowed values."),
670class BestSeeingSelectVisitsTask(pipeBase.PipelineTask):
671 """Select up to a maximum number of the best-seeing visits.
673 Don't exceed the FWHM range specified by configs min(max)PsfFwhm.
674 This Task is a port of the Gen2 image-selector used in the AP pipeline:
675 BestSeeingSelectImagesTask. This Task selects full visits based on the
676 average PSF of the entire visit.
679 ConfigClass = BestSeeingSelectVisitsConfig
680 _DefaultName =
'bestSeeingSelectVisits'
682 def runQuantum(self, butlerQC, inputRefs, outputRefs):
683 inputs = butlerQC.get(inputRefs)
684 quantumDataId = butlerQC.quantum.dataId
685 outputs = self.run(**inputs, dataId=quantumDataId)
686 butlerQC.put(outputs, outputRefs)
688 def run(self, visitSummaries, skyMap, dataId):
693 visitSummary : `list` [`lsst.pipe.base.connections.DeferredDatasetRef`]
694 List of `lsst.pipe.base.connections.DeferredDatasetRef` of
695 visitSummary tables of type `lsst.afw.table.ExposureCatalog`.
696 skyMap : `lsst.skyMap.SkyMap`
697 SkyMap for checking visits overlap patch.
698 dataId : `dict` of dataId keys
699 For retrieving patch info for checking visits overlap patch.
703 result : `lsst.pipe.base.Struct`
704 Results as a struct with attributes:
707 A `dict` with selected visit ids as keys,
708 so that it can be be saved as a StructuredDataDict.
709 StructuredDataList's are currently limited.
711 if self.config.doConfirmOverlap:
712 patchPolygon = self.makePatchPolygon(skyMap, dataId)
714 inputVisits = [visitSummary.ref.dataId[
'visit']
for visitSummary
in visitSummaries]
717 for visit, visitSummary
in zip(inputVisits, visitSummaries):
719 visitSummary = visitSummary.get()
723 mjd = visitSummary[0].getVisitInfo().getDate().get(system=DateTime.MJD)
725 pixelScales = np.array([vs[
'pixelScale']
for vs
in visitSummary
if vs.getWcs()])
727 psfSigmas = np.array([vs[
'psfSigma']
for vs
in visitSummary
if vs.getWcs()])
728 fwhm = np.nanmean(psfSigmas * pixelScales) * np.sqrt(8.*np.log(2.))
730 if self.config.maxPsfFwhm
and fwhm > self.config.maxPsfFwhm:
732 if self.config.minPsfFwhm
and fwhm < self.config.minPsfFwhm:
734 if self.config.minMJD
and mjd < self.config.minMJD:
735 self.log.debug(
'MJD %f earlier than %.2f; rejecting', mjd, self.config.minMJD)
737 if self.config.maxMJD
and mjd > self.config.maxMJD:
738 self.log.debug(
'MJD %f later than %.2f; rejecting', mjd, self.config.maxMJD)
740 if self.config.doConfirmOverlap
and not self.doesIntersectPolygon(visitSummary, patchPolygon):
742 if (self.config.visitSummaryMinValues
747 fwhmSizes.append(fwhm)
750 sortedVisits = [ind
for (_, ind)
in sorted(zip(fwhmSizes, visits))]
751 if self.config.nVisitsMax < 0:
752 output = sortedVisits
754 output = sortedVisits[:self.config.nVisitsMax]
757 self.log.info(
"All images rejected in BestSeeingSelectVisitsTask.")
758 raise pipeBase.NoWorkFound(f
"No good images found for {dataId}")
761 "%d images selected with FWHM range of %f--%f arcseconds",
763 fwhmSizes[visits.index(output[0])],
764 fwhmSizes[visits.index(output[-1])],
768 goodVisits = {key:
True for key
in output}
769 return pipeBase.Struct(goodVisits=goodVisits)
771 def makePatchPolygon(self, skyMap, dataId):
772 """Return True if sky polygon overlaps visit.
776 skyMap : `lsst.afw.table.ExposureCatalog`
777 Exposure catalog with per-detector geometry.
778 dataId : `dict` of dataId keys
779 For retrieving patch info.
783 result : `lsst.sphgeom.ConvexPolygon.convexHull`
784 Polygon of patch's outer bbox.
786 wcs = skyMap[dataId[
'tract']].getWcs()
787 bbox = skyMap[dataId[
'tract']][dataId[
'patch']].getOuterBBox()
792 def doesIntersectPolygon(self, visitSummary, polygon):
793 """Return True if sky polygon overlaps visit.
797 visitSummary : `lsst.afw.table.ExposureCatalog`
798 Exposure catalog with per-detector geometry.
799 polygon :` lsst.sphgeom.ConvexPolygon.convexHull`
800 Polygon to check overlap.
804 doesIntersect : `bool`
805 True if the visit overlaps the polygon.
807 doesIntersect =
False
808 for detectorSummary
in visitSummary:
809 if (np.all(np.isfinite(detectorSummary[
'raCorners']))
810 and np.all(np.isfinite(detectorSummary[
'decCorners']))):
813 zip(detectorSummary[
'raCorners'], detectorSummary[
'decCorners'])]
815 if detectorPolygon.intersects(polygon):
821class BestSeeingQuantileSelectVisitsConfig(pipeBase.PipelineTaskConfig,
822 pipelineConnections=BestSeeingSelectVisitsConnections):
823 qMin = pexConfig.RangeField(
824 doc=
"Lower bound of quantile range to select. Sorts visits by seeing from narrow to wide, "
825 "and select those in the interquantile range (qMin, qMax). Set qMin to 0 for Best Seeing. "
826 "This config should be changed from zero only for exploratory diffIm testing.",
832 qMax = pexConfig.RangeField(
833 doc=
"Upper bound of quantile range to select. Sorts visits by seeing from narrow to wide, "
834 "and select those in the interquantile range (qMin, qMax). Set qMax to 1 for Worst Seeing.",
840 maxPsfFwhm = pexConfig.Field(
842 doc=
"Maximum PSF FWHM (in arcseconds) to select. Any visits with larger FWHM will be excluded "
843 "before quantile selection.",
847 nVisitsMin = pexConfig.Field(
848 doc=
"The minimum number of visits to select, if qMin and qMax alone would have "
849 "selected fewer. In regimes with many visits, at least this number of visits will be "
850 "selected, superceding quantile when necessary. "
851 "For example, if 10 visits cover this patch, qMin=0, qMax=0.33, and nVisitsMin=5, "
852 "the best 5 visits will be selected, even though 5 > 0.33*10. "
853 "In regimes with few visits, all available visits will be selected. "
854 "For example, if 2 visits cover this patch and nVisitsMin=12, "
855 "both visits will be selected regardless of qMin and qMax.",
859 doConfirmOverlap = pexConfig.Field(
861 doc=
"Do remove visits that do not actually overlap the patch?",
864 minMJD = pexConfig.Field(
866 doc=
"Minimum visit MJD to select",
870 maxMJD = pexConfig.Field(
872 doc=
"Maximum visit MJD to select",
876 visitSummaryMinValues = pexConfig.DictField(
879 doc=(
"Optional thresholds for visit selection. Keys are visit_summary column names"
880 "(e.g. 'effTimeZeroPointScale'), and values are minimum allowed values."),
886class BestSeeingQuantileSelectVisitsTask(BestSeeingSelectVisitsTask):
887 """Select a quantile of the best-seeing visits.
889 Selects the best (for example, third) full visits based on the average
890 PSF width in the entire visit. It can also be used for difference imaging
891 experiments that require templates with the worst seeing visits.
892 For example, selecting the worst third can be acheived by
893 changing the config parameters qMin to 0.66 and qMax to 1.
895 ConfigClass = BestSeeingQuantileSelectVisitsConfig
896 _DefaultName =
'bestSeeingQuantileSelectVisits'
898 def run(self, visitSummaries, skyMap, dataId):
899 if self.config.doConfirmOverlap:
900 patchPolygon = self.makePatchPolygon(skyMap, dataId)
901 visits = np.array([visitSummary.ref.dataId[
'visit']
for visitSummary
in visitSummaries])
902 radius = np.empty(len(visits))
903 intersects = np.full(len(visits),
True)
904 for i, visitSummary
in enumerate(visitSummaries):
906 visitSummary = visitSummary.get()
908 psfSigmas = np.array([vs[
'psfSigma']
for vs
in visitSummary
if vs.getWcs()])
909 pixelScales = np.array([vs[
'pixelScale']
for vs
in visitSummary
if vs.getWcs()])
910 fwhm = np.nanmedian(psfSigmas * pixelScales) * np.sqrt(8.*np.log(2.))
912 if self.config.doConfirmOverlap:
913 intersects[i] = self.doesIntersectPolygon(visitSummary, patchPolygon)
914 if self.config.minMJD
or self.config.maxMJD:
917 mjd = visitSummary[0].getVisitInfo().getDate().get(system=DateTime.MJD)
918 aboveMin = mjd > self.config.minMJD
if self.config.minMJD
else True
919 belowMax = mjd < self.config.maxMJD
if self.config.maxMJD
else True
920 intersects[i] = intersects[i]
and aboveMin
and belowMax
921 if self.config.maxPsfFwhm
and fwhm > self.config.maxPsfFwhm:
922 intersects[i] =
False
923 self.log.info(f
"Rejecting visit {visits[i]}: with FWHM {fwhm:.2f} > {self.config.maxPsfFwhm}")
924 if (self.config.visitSummaryMinValues
926 self.config.visitSummaryMinValues, self.log)):
927 intersects[i] =
False
929 sortedVisits = [v
for rad, v
in sorted(zip(radius[intersects], visits[intersects]))]
930 lowerBound = min(int(np.round(self.config.qMin*len(visits[intersects]))),
931 max(0, len(visits[intersects]) - self.config.nVisitsMin))
932 upperBound = max(int(np.round(self.config.qMax*len(visits[intersects]))), self.config.nVisitsMin)
935 goodVisits = {int(visit):
True for visit
in sortedVisits[lowerBound:upperBound]}
937 if len(goodVisits) == 0:
938 self.log.info(
"All visits rejected")
940 raise pipeBase.NoWorkFound(f
"No good visits found for {dataId}")
942 selectedRadii = sorted(radius[intersects])[lowerBound:upperBound]
943 self.log.info(f
"Selected {len(goodVisits)} visits with FWHM range of "
944 f
"{selectedRadii[0]:.2f}--{selectedRadii[-1]:.2f} arcseconds.")
945 return pipeBase.Struct(goodVisits=goodVisits)
__init__(self, dataId, coordList)
run(self, wcsList, bboxList, coordList, visitSummary, dataIds=None, **kwargs)
isValid(self, visitSummary, detectorId)
__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)
_extractKeyValue(dataList, keys=None)
_meetsVisitSummaryMinValues(visit, visitSummary, visitSummaryMinValues, logger=None)