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 exluded 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 currenly: {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 currenly: {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 currenly: {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"]
516 if self.config.maxEllipResidual
and not (medianE <= self.config.maxEllipResidual):
517 self.log.info(
"Removing visit %d detector %d because median e residual too large: %f vs %f",
518 row[
"visit"], detectorId, medianE, self.config.maxEllipResidual)
520 elif self.config.maxSizeScatter
and not (scatterSize <= self.config.maxSizeScatter):
521 self.log.info(
"Removing visit %d detector %d because size scatter too large: %f vs %f",
522 row[
"visit"], detectorId, scatterSize, self.config.maxSizeScatter)
524 elif self.config.maxScaledSizeScatter
and not (scaledScatterSize <= self.config.maxScaledSizeScatter):
525 self.log.info(
"Removing visit %d detector %d because scaled size scatter too large: %f vs %f",
526 row[
"visit"], detectorId, scaledScatterSize, self.config.maxScaledSizeScatter)
529 self.config.maxPsfTraceRadiusDelta
is not None
530 and not (psfTraceRadiusDelta <= self.config.maxPsfTraceRadiusDelta)
533 "Removing visit %d detector %d because max-min delta of model PSF trace radius values "
534 "across the unmasked detector pixels is not finite or too large: %.3f vs %.3f (pixels)",
535 row[
"visit"], detectorId, psfTraceRadiusDelta, self.config.maxPsfTraceRadiusDelta
539 self.config.maxPsfApFluxDelta
is not None
540 and not (psfApFluxDelta <= self.config.maxPsfApFluxDelta)
543 "Removing visit %d detector %d because max-min delta of model PSF aperture flux values "
544 "across the unmasked detector pixels is not finite or too large: %.3f vs %.3f (pixels)",
545 row[
"visit"], detectorId, psfApFluxDelta, self.config.maxPsfApFluxDelta
549 self.config.maxPsfApCorrSigmaScaledDelta
is not None
550 and not (psfApCorrSigmaScaledDelta <= self.config.maxPsfApCorrSigmaScaledDelta)
553 "Removing visit %d detector %d because max-min delta of the model PSF apterture correction "
554 "values scaled (divided) by the psfSigma across the unmasked detector pixels is not "
555 "finite or too large: %.3f vs %.3f (fatcor)",
556 row[
"visit"], detectorId, psfApCorrSigmaScaledDelta, self.config.maxPsfApCorrSigmaScaledDelta
559 elif minNPsfStar
and (nPsfStar < minNPsfStar):
561 "Removing visit %d detector %d because the PSF model had too few stars: %d vs %d",
562 row[
"visit"], detectorId, nPsfStar, minNPsfStar
565 elif self.config.maxPsfFwhm:
566 pixelScale = row[
'pixelScale']
567 psfSigma = row[
'psfSigma']
568 fwhm = psfSigma * pixelScale * np.sqrt(8.*np.log(2.))
569 if not (fwhm <= self.config.maxPsfFwhm):
570 self.log.info(
"Removing visit %d detector %d because FWHM too large: %f vs %f",
571 row[
"visit"], detectorId, fwhm, self.config.maxPsfFwhm)
573 elif maxStarE
and not (starEMedian <= maxStarE):
575 "Removing visit %d detector %d because the median star ellipticity is too large: %d vs %d",
576 row[
"visit"], detectorId, starEMedian, maxStarE
579 elif maxStarUnNormalizedE
and not (starUnNormalizedEMedian <= maxStarUnNormalizedE):
581 "Removing visit %d detector %d because the median star unnormalized ellipticity "
582 "is too large: %d vs %d",
583 row[
"visit"], detectorId, starUnNormalizedEMedian, maxStarUnNormalizedE
591 dimensions=(
"tract",
"patch",
"skymap",
"band",
"instrument"),
592 defaultTemplates={
"coaddName":
"goodSeeing",
594 skyMap = pipeBase.connectionTypes.Input(
595 doc=
"Input definition of geometry/bbox and projection/wcs for coadded exposures",
596 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
597 storageClass=
"SkyMap",
598 dimensions=(
"skymap",),
600 visitSummaries = pipeBase.connectionTypes.Input(
601 doc=
"Per-visit consolidated exposure metadata",
602 name=
"finalVisitSummary",
603 storageClass=
"ExposureCatalog",
604 dimensions=(
"instrument",
"visit",),
608 goodVisits = pipeBase.connectionTypes.Output(
609 doc=
"Selected visits to be coadded.",
610 name=
"{coaddName}Visits",
611 storageClass=
"StructuredDataDict",
612 dimensions=(
"instrument",
"tract",
"patch",
"skymap",
"band"),
616class BestSeeingSelectVisitsConfig(pipeBase.PipelineTaskConfig,
617 pipelineConnections=BestSeeingSelectVisitsConnections):
618 nVisitsMax = pexConfig.RangeField(
620 doc=
"Maximum number of visits to select; use -1 to select all.",
624 maxPsfFwhm = pexConfig.Field(
626 doc=
"Maximum PSF FWHM (in arcseconds) to select",
630 minPsfFwhm = pexConfig.Field(
632 doc=
"Minimum PSF FWHM (in arcseconds) to select",
636 doConfirmOverlap = pexConfig.Field(
638 doc=
"Do remove visits that do not actually overlap the patch?",
641 minMJD = pexConfig.Field(
643 doc=
"Minimum visit MJD to select",
647 maxMJD = pexConfig.Field(
649 doc=
"Maximum visit MJD to select",
653 visitSummaryMinValues = pexConfig.DictField(
656 doc=(
"Optional thresholds for visit selection. Keys are visit_summary column names"
657 "(e.g. 'effTimeZeroPointScale'), and values are minimum allowed values."),
663class BestSeeingSelectVisitsTask(pipeBase.PipelineTask):
664 """Select up to a maximum number of the best-seeing visits.
666 Don't exceed the FWHM range specified by configs min(max)PsfFwhm.
667 This Task is a port of the Gen2 image-selector used in the AP pipeline:
668 BestSeeingSelectImagesTask. This Task selects full visits based on the
669 average PSF of the entire visit.
672 ConfigClass = BestSeeingSelectVisitsConfig
673 _DefaultName =
'bestSeeingSelectVisits'
675 def runQuantum(self, butlerQC, inputRefs, outputRefs):
676 inputs = butlerQC.get(inputRefs)
677 quantumDataId = butlerQC.quantum.dataId
678 outputs = self.run(**inputs, dataId=quantumDataId)
679 butlerQC.put(outputs, outputRefs)
681 def run(self, visitSummaries, skyMap, dataId):
686 visitSummary : `list` [`lsst.pipe.base.connections.DeferredDatasetRef`]
687 List of `lsst.pipe.base.connections.DeferredDatasetRef` of
688 visitSummary tables of type `lsst.afw.table.ExposureCatalog`.
689 skyMap : `lsst.skyMap.SkyMap`
690 SkyMap for checking visits overlap patch.
691 dataId : `dict` of dataId keys
692 For retrieving patch info for checking visits overlap patch.
696 result : `lsst.pipe.base.Struct`
697 Results as a struct with attributes:
700 A `dict` with selected visit ids as keys,
701 so that it can be be saved as a StructuredDataDict.
702 StructuredDataList's are currently limited.
704 if self.config.doConfirmOverlap:
705 patchPolygon = self.makePatchPolygon(skyMap, dataId)
707 inputVisits = [visitSummary.ref.dataId[
'visit']
for visitSummary
in visitSummaries]
710 for visit, visitSummary
in zip(inputVisits, visitSummaries):
712 visitSummary = visitSummary.get()
716 mjd = visitSummary[0].getVisitInfo().getDate().get(system=DateTime.MJD)
718 pixelScales = np.array([vs[
'pixelScale']
for vs
in visitSummary
if vs.getWcs()])
720 psfSigmas = np.array([vs[
'psfSigma']
for vs
in visitSummary
if vs.getWcs()])
721 fwhm = np.nanmean(psfSigmas * pixelScales) * np.sqrt(8.*np.log(2.))
723 if self.config.maxPsfFwhm
and fwhm > self.config.maxPsfFwhm:
725 if self.config.minPsfFwhm
and fwhm < self.config.minPsfFwhm:
727 if self.config.minMJD
and mjd < self.config.minMJD:
728 self.log.debug(
'MJD %f earlier than %.2f; rejecting', mjd, self.config.minMJD)
730 if self.config.maxMJD
and mjd > self.config.maxMJD:
731 self.log.debug(
'MJD %f later than %.2f; rejecting', mjd, self.config.maxMJD)
733 if self.config.doConfirmOverlap
and not self.doesIntersectPolygon(visitSummary, patchPolygon):
735 if (self.config.visitSummaryMinValues
740 fwhmSizes.append(fwhm)
743 sortedVisits = [ind
for (_, ind)
in sorted(zip(fwhmSizes, visits))]
744 if self.config.nVisitsMax < 0:
745 output = sortedVisits
747 output = sortedVisits[:self.config.nVisitsMax]
750 self.log.info(
"All images rejected in BestSeeingSelectVisitsTask.")
751 raise pipeBase.NoWorkFound(f
"No good images found for {dataId}")
754 "%d images selected with FWHM range of %f--%f arcseconds",
756 fwhmSizes[visits.index(output[0])],
757 fwhmSizes[visits.index(output[-1])],
761 goodVisits = {key:
True for key
in output}
762 return pipeBase.Struct(goodVisits=goodVisits)
764 def makePatchPolygon(self, skyMap, dataId):
765 """Return True if sky polygon overlaps visit.
769 skyMap : `lsst.afw.table.ExposureCatalog`
770 Exposure catalog with per-detector geometry.
771 dataId : `dict` of dataId keys
772 For retrieving patch info.
776 result : `lsst.sphgeom.ConvexPolygon.convexHull`
777 Polygon of patch's outer bbox.
779 wcs = skyMap[dataId[
'tract']].getWcs()
780 bbox = skyMap[dataId[
'tract']][dataId[
'patch']].getOuterBBox()
785 def doesIntersectPolygon(self, visitSummary, polygon):
786 """Return True if sky polygon overlaps visit.
790 visitSummary : `lsst.afw.table.ExposureCatalog`
791 Exposure catalog with per-detector geometry.
792 polygon :` lsst.sphgeom.ConvexPolygon.convexHull`
793 Polygon to check overlap.
797 doesIntersect : `bool`
798 True if the visit overlaps the polygon.
800 doesIntersect =
False
801 for detectorSummary
in visitSummary:
802 if (np.all(np.isfinite(detectorSummary[
'raCorners']))
803 and np.all(np.isfinite(detectorSummary[
'decCorners']))):
806 zip(detectorSummary[
'raCorners'], detectorSummary[
'decCorners'])]
808 if detectorPolygon.intersects(polygon):
814class BestSeeingQuantileSelectVisitsConfig(pipeBase.PipelineTaskConfig,
815 pipelineConnections=BestSeeingSelectVisitsConnections):
816 qMin = pexConfig.RangeField(
817 doc=
"Lower bound of quantile range to select. Sorts visits by seeing from narrow to wide, "
818 "and select those in the interquantile range (qMin, qMax). Set qMin to 0 for Best Seeing. "
819 "This config should be changed from zero only for exploratory diffIm testing.",
825 qMax = pexConfig.RangeField(
826 doc=
"Upper bound of quantile range to select. Sorts visits by seeing from narrow to wide, "
827 "and select those in the interquantile range (qMin, qMax). Set qMax to 1 for Worst Seeing.",
833 nVisitsMin = pexConfig.Field(
834 doc=
"The minimum number of visits to select, if qMin and qMax alone would have "
835 "selected fewer. In regimes with many visits, at least this number of visits will be "
836 "selected, superceding quantile when necessary. "
837 "For example, if 10 visits cover this patch, qMin=0, qMax=0.33, and nVisitsMin=5, "
838 "the best 5 visits will be selected, even though 5 > 0.33*10. "
839 "In regimes with few visits, all available visits will be selected. "
840 "For example, if 2 visits cover this patch and nVisitsMin=12, "
841 "both visits will be selected regardless of qMin and qMax.",
845 doConfirmOverlap = pexConfig.Field(
847 doc=
"Do remove visits that do not actually overlap the patch?",
850 minMJD = pexConfig.Field(
852 doc=
"Minimum visit MJD to select",
856 maxMJD = pexConfig.Field(
858 doc=
"Maximum visit MJD to select",
862 visitSummaryMinValues = pexConfig.DictField(
865 doc=(
"Optional thresholds for visit selection. Keys are visit_summary column names"
866 "(e.g. 'effTimeZeroPointScale'), and values are minimum allowed values."),
872class BestSeeingQuantileSelectVisitsTask(BestSeeingSelectVisitsTask):
873 """Select a quantile of the best-seeing visits.
875 Selects the best (for example, third) full visits based on the average
876 PSF width in the entire visit. It can also be used for difference imaging
877 experiments that require templates with the worst seeing visits.
878 For example, selecting the worst third can be acheived by
879 changing the config parameters qMin to 0.66 and qMax to 1.
881 ConfigClass = BestSeeingQuantileSelectVisitsConfig
882 _DefaultName =
'bestSeeingQuantileSelectVisits'
884 def run(self, visitSummaries, skyMap, dataId):
885 if self.config.doConfirmOverlap:
886 patchPolygon = self.makePatchPolygon(skyMap, dataId)
887 visits = np.array([visitSummary.ref.dataId[
'visit']
for visitSummary
in visitSummaries])
888 radius = np.empty(len(visits))
889 intersects = np.full(len(visits),
True)
890 for i, visitSummary
in enumerate(visitSummaries):
892 visitSummary = visitSummary.get()
894 psfSigma = np.nanmedian([vs[
'psfSigma']
for vs
in visitSummary])
896 if self.config.doConfirmOverlap:
897 intersects[i] = self.doesIntersectPolygon(visitSummary, patchPolygon)
898 if self.config.minMJD
or self.config.maxMJD:
901 mjd = visitSummary[0].getVisitInfo().getDate().get(system=DateTime.MJD)
902 aboveMin = mjd > self.config.minMJD
if self.config.minMJD
else True
903 belowMax = mjd < self.config.maxMJD
if self.config.maxMJD
else True
904 intersects[i] = intersects[i]
and aboveMin
and belowMax
905 if (self.config.visitSummaryMinValues
907 self.config.visitSummaryMinValues, self.log)):
908 intersects[i] =
False
910 sortedVisits = [v
for rad, v
in sorted(zip(radius[intersects], visits[intersects]))]
911 lowerBound = min(int(np.round(self.config.qMin*len(visits[intersects]))),
912 max(0, len(visits[intersects]) - self.config.nVisitsMin))
913 upperBound = max(int(np.round(self.config.qMax*len(visits[intersects]))), self.config.nVisitsMin)
916 goodVisits = {int(visit):
True for visit
in sortedVisits[lowerBound:upperBound]}
917 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)