24import lsst.utils
as utils
31from lsst.utils.timer
import timeMethod
33__all__ = [
"BaseSelectImagesTask",
"BaseExposureInfo",
"WcsSelectImagesTask",
"PsfWcsSelectImagesTask",
34 "DatabaseSelectImagesConfig",
"BestSeeingWcsSelectImagesTask",
"BestSeeingSelectVisitsTask",
35 "BestSeeingQuantileSelectVisitsTask"]
39 """Base configuration for subclasses of BaseSelectImagesTask that use a database"""
40 host = pexConfig.Field(
41 doc=
"Database server host name",
44 port = pexConfig.Field(
45 doc=
"Database server port",
48 database = pexConfig.Field(
49 doc=
"Name of database",
52 maxExposures = pexConfig.Field(
53 doc=
"maximum exposures to select; intended for debugging; ignored if None",
60 """Data about a selected exposure
64 """Create exposure information that can be used to generate data references
66 The object has the following fields:
67 - dataId: data ID of exposure (a dict)
69 plus any others items that are desired
71 super(BaseExposureInfo, self).__init__(dataId=dataId, coordList=coordList)
75 """Base task for selecting images suitable for coaddition
77 ConfigClass = pexConfig.Config
78 _DefaultName = "selectImages"
81 def run(self, coordList):
82 """Select images suitable for coaddition in a particular region
84 @param[
in] coordList: list of coordinates defining region of interest;
if None then select all images
85 subclasses may add additional keyword arguments,
as required
87 @return a pipeBase Struct containing:
88 - exposureInfoList: a list of exposure information objects (subclasses of BaseExposureInfo),
89 which have at least the following fields:
90 - dataId: data ID dictionary
93 raise NotImplementedError()
95 def _runArgDictFromDataId(self, dataId):
96 """Extract keyword arguments for run (other than coordList) from a data ID
98 @return keyword arguments
for run (other than coordList),
as a dict
100 raise NotImplementedError()
102 def runDataRef(self, dataRef, coordList, makeDataRefList=True, selectDataList=[]):
103 """Run based on a data reference
105 This delegates to run() and _runArgDictFromDataId() to do the actual
106 selection. In the event that the selectDataList
is non-empty, this will
107 be used to further restrict the selection, providing the user
with
108 additional control over the selection.
110 @param[
in] dataRef: data reference; must contain any extra keys needed by the subclass
111 @param[
in] coordList: list of coordinates defining region of interest;
if None, search the whole sky
112 @param[
in] makeDataRefList:
if True,
return dataRefList
113 @param[
in] selectDataList: List of SelectStruct
with dataRefs to consider
for selection
114 @return a pipeBase Struct containing:
115 - exposureInfoList: a list of objects derived
from ExposureInfo
116 - dataRefList: a list of data references (
None if makeDataRefList
False)
119 exposureInfoList = self.runrun(coordList, **runArgDict).exposureInfoList
121 if len(selectDataList) > 0
and len(exposureInfoList) > 0:
123 ccdKeys, ccdValues = _extractKeyValue(exposureInfoList)
124 inKeys, inValues = _extractKeyValue([s.dataRef
for s
in selectDataList], keys=ccdKeys)
125 inValues = set(inValues)
126 newExposureInfoList = []
127 for info, ccdVal
in zip(exposureInfoList, ccdValues):
128 if ccdVal
in inValues:
129 newExposureInfoList.append(info)
131 self.log.info(
"De-selecting exposure %s: not in selectDataList", info.dataId)
132 exposureInfoList = newExposureInfoList
135 butler = dataRef.butlerSubset.butler
136 dataRefList = [butler.dataRef(datasetType=
"calexp",
137 dataId=expInfo.dataId,
138 )
for expInfo
in exposureInfoList]
142 return pipeBase.Struct(
143 dataRefList=dataRefList,
144 exposureInfoList=exposureInfoList,
148def _extractKeyValue(dataList, keys=None):
149 """Extract the keys and values from a list of dataIds
151 The input dataList is a list of objects that have
'dataId' members.
152 This allows it to be used
for both a list of data references
and a
155 assert len(dataList) > 0
157 keys = sorted(dataList[0].dataId.keys())
160 for data
in dataList:
161 thisKeys = set(data.dataId.keys())
162 if thisKeys != keySet:
163 raise RuntimeError(
"DataId keys inconsistent: %s vs %s" % (keySet, thisKeys))
164 values.append(tuple(data.dataId[k]
for k
in keys))
169 """A container for data to be passed to the WcsSelectImagesTask"""
172 super(SelectStruct, self).
__init__(dataRef=dataRef, wcs=wcs, bbox=bbox)
176 """Select images using their Wcs
179 polygons on the celestial sphere,
and test the polygon of the
180 patch
for overlap
with the polygon of the image.
182 We use
"convexHull" instead of generating a ConvexPolygon
183 directly because the standard
for the inputs to ConvexPolygon
184 are pretty high
and we don
't want to be responsible for reaching them.
187 def runDataRef(self, dataRef, coordList, makeDataRefList=True, selectDataList=[]):
188 """Select images in the selectDataList that overlap the patch
190 This method is the old entry point
for the Gen2 commandline tasks
and drivers
191 Will be deprecated
in v22.
193 @param dataRef: Data reference
for coadd/tempExp (
with tract, patch)
195 @param makeDataRefList: Construct a list of data references?
196 @param selectDataList: List of SelectStruct, to consider
for selection
199 exposureInfoList = []
201 patchVertices = [coord.getVector() for coord
in coordList]
204 for data
in selectDataList:
205 dataRef = data.dataRef
209 imageCorners = self.
getValidImageCornersgetValidImageCorners(imageWcs, imageBox, patchPoly, dataId=
None)
211 dataRefList.append(dataRef)
214 return pipeBase.Struct(
215 dataRefList=dataRefList
if makeDataRefList
else None,
216 exposureInfoList=exposureInfoList,
219 def run(self, wcsList, bboxList, coordList, dataIds=None, **kwargs):
220 """Return indices of provided lists that meet the selection criteria
225 specifying the WCS's of the input ccds to be selected
227 specifying the bounding boxes of the input ccds to be selected
229 ICRS coordinates specifying boundary of the patch.
233 result: `list` of `int`
234 of indices of selected ccds
237 dataIds = [
None] * len(wcsList)
238 patchVertices = [coord.getVector()
for coord
in coordList]
241 for i, (imageWcs, imageBox, dataId)
in enumerate(zip(wcsList, bboxList, dataIds)):
242 imageCorners = self.
getValidImageCornersgetValidImageCorners(imageWcs, imageBox, patchPoly, dataId)
248 "Return corners or None if bad"
250 imageCorners = [imageWcs.pixelToSky(pix)
for pix
in geom.Box2D(imageBox).getCorners()]
251 except (pexExceptions.DomainError, pexExceptions.RuntimeError)
as e:
253 self.log.debug(
"WCS error in testing calexp %s (%s): deselecting", dataId, e)
257 if imagePoly
is None:
258 self.log.debug(
"Unable to create polygon from image %s: deselecting", dataId)
261 if patchPoly.intersects(imagePoly):
263 self.log.info(
"Selecting calexp %s", dataId)
268 "Return median absolute deviation scaled to normally distributed data"
269 return 1.4826*np.median(np.abs(array - np.median(array)))
273 dimensions=(
"tract",
"patch",
"skymap",
"instrument",
"visit"),
274 defaultTemplates={
"coaddName":
"deep"}):
278class PsfWcsSelectImagesConfig(pipeBase.PipelineTaskConfig,
279 pipelineConnections=PsfWcsSelectImagesConnections):
280 maxEllipResidual = pexConfig.Field(
281 doc=
"Maximum median ellipticity residual",
286 maxSizeScatter = pexConfig.Field(
287 doc=
"Maximum scatter in the size residuals",
291 maxScaledSizeScatter = pexConfig.Field(
292 doc=
"Maximum scatter in the size residuals, scaled by the median size",
297 starSelection = pexConfig.Field(
298 doc=
"select star with this field",
300 default=
'calib_psf_used',
301 deprecated=(
'This field has been moved to ComputeExposureSummaryStatsTask and '
302 'will be removed after v24.')
304 starShape = pexConfig.Field(
305 doc=
"name of star shape",
307 default=
'base_SdssShape',
308 deprecated=(
'This field has been moved to ComputeExposureSummaryStatsTask and '
309 'will be removed after v24.')
311 psfShape = pexConfig.Field(
312 doc=
"name of psf shape",
314 default=
'base_SdssShape_psf',
315 deprecated=(
'This field has been moved to ComputeExposureSummaryStatsTask and '
316 'will be removed after v24.')
318 doLegacyStarSelectionComputation = pexConfig.Field(
319 doc=
"Perform the legacy star selection computations (for backwards compatibility)",
322 deprecated=(
"This field is here for backwards compatibility and will be "
323 "removed after v24.")
328 """Select images using their Wcs and cuts on the PSF properties
330 The PSF quality criteria are based on the size and ellipticity residuals
from the
331 adaptive second moments of the star
and the PSF.
334 - the median of the ellipticty residuals
335 - the robust scatter of the size residuals (using the median absolute deviation)
336 - the robust scatter of the size residuals scaled by the square of
340 ConfigClass = PsfWcsSelectImagesConfig
341 _DefaultName = "PsfWcsSelectImages"
343 def runDataRef(self, dataRef, coordList, makeDataRefList=True, selectDataList=[]):
344 """Select images in the selectDataList that overlap the patch and satisfy PSF quality critera.
346 This method is the old entry point
for the Gen2 commandline tasks
and drivers
347 Will be deprecated
in v22.
349 @param dataRef: Data reference
for coadd/tempExp (
with tract, patch)
351 @param makeDataRefList: Construct a list of data references?
352 @param selectDataList: List of SelectStruct, to consider
for selection
354 result = super(PsfWcsSelectImagesTask, self).runDataRef(dataRef, coordList, makeDataRefList,
358 exposureInfoList = []
359 for dataRef, exposureInfo
in zip(result.dataRefList, result.exposureInfoList):
360 butler = dataRef.butlerSubset.butler
361 srcCatalog = butler.get(
'src', dataRef.dataId)
362 valid = self.isValidLegacy(srcCatalog, dataRef.dataId)
366 dataRefList.append(dataRef)
367 exposureInfoList.append(exposureInfo)
369 return pipeBase.Struct(
370 dataRefList=dataRefList,
371 exposureInfoList=exposureInfoList,
374 def run(self, wcsList, bboxList, coordList, visitSummary, dataIds=None, srcList=None, **kwargs):
375 """Return indices of provided lists that meet the selection criteria
380 specifying the WCS's of the input ccds to be selected
382 specifying the bounding boxes of the input ccds to be selected
384 ICRS coordinates specifying boundary of the patch.
386 containing the PSF shape information for the input ccds to be selected.
388 containing the PSF shape information
for the input ccds to be selected.
389 This
is only used
if ``config.doLegacyStarSelectionComputation``
is
394 goodPsf: `list` of `int`
395 of indices of selected ccds
397 goodWcs = super(PsfWcsSelectImagesTask, self).run(wcsList=wcsList, bboxList=bboxList,
398 coordList=coordList, dataIds=dataIds)
402 if not self.config.doLegacyStarSelectionComputation:
404 if 'nPsfStar' not in visitSummary[0].schema.getNames():
405 raise RuntimeError(
"Old calexps detected. "
406 "Please set config.doLegacyStarSelectionComputation=True for "
407 "backwards compatibility.")
409 for i, dataId
in enumerate(dataIds):
412 if self.isValid(visitSummary, dataId[
"detector"]):
416 dataIds = [
None] * len(srcList)
417 for i, (srcCatalog, dataId)
in enumerate(zip(srcList, dataIds)):
420 if self.isValidLegacy(srcCatalog, dataId):
425 def isValid(self, visitSummary, detectorId):
426 """Should this ccd be selected based on its PSF shape information.
439 row = visitSummary.find(detectorId)
442 self.log.warning(
"Removing visit %d detector %d because summary stats not available.",
443 row[
"visit"], detectorId)
446 medianE = np.sqrt(row[
"psfStarDeltaE1Median"]**2. + row[
"psfStarDeltaE2Median"]**2.)
447 scatterSize = row[
"psfStarDeltaSizeScatter"]
448 scaledScatterSize = row[
"psfStarScaledDeltaSizeScatter"]
451 if self.config.maxEllipResidual
and medianE > self.config.maxEllipResidual:
452 self.log.info(
"Removing visit %d detector %d because median e residual too large: %f vs %f",
453 row[
"visit"], detectorId, medianE, self.config.maxEllipResidual)
455 elif self.config.maxSizeScatter
and scatterSize > self.config.maxSizeScatter:
456 self.log.info(
"Removing visit %d detector %d because size scatter too large: %f vs %f",
457 row[
"visit"], detectorId, scatterSize, self.config.maxSizeScatter)
459 elif self.config.maxScaledSizeScatter
and scaledScatterSize > self.config.maxScaledSizeScatter:
460 self.log.info(
"Removing visit %d detector %d because scaled size scatter too large: %f vs %f",
461 row[
"visit"], detectorId, scaledScatterSize, self.config.maxScaledSizeScatter)
466 def isValidLegacy(self, srcCatalog, dataId=None):
467 """Should this ccd be selected based on its PSF shape information.
469 This routine is only used
in legacy processing (gen2
and
470 backwards compatible old calexps)
and should be removed after v24.
475 dataId : `dict` of dataId keys, optional.
476 Used only
for logging. Defaults to
None.
483 mask = srcCatalog[self.config.starSelection]
485 starXX = srcCatalog[self.config.starShape+'_xx'][mask]
486 starYY = srcCatalog[self.config.starShape+
'_yy'][mask]
487 starXY = srcCatalog[self.config.starShape+
'_xy'][mask]
488 psfXX = srcCatalog[self.config.psfShape+
'_xx'][mask]
489 psfYY = srcCatalog[self.config.psfShape+
'_yy'][mask]
490 psfXY = srcCatalog[self.config.psfShape+
'_xy'][mask]
492 starSize = np.power(starXX*starYY - starXY**2, 0.25)
493 starE1 = (starXX - starYY)/(starXX + starYY)
494 starE2 = 2*starXY/(starXX + starYY)
495 medianSize = np.median(starSize)
497 psfSize = np.power(psfXX*psfYY - psfXY**2, 0.25)
498 psfE1 = (psfXX - psfYY)/(psfXX + psfYY)
499 psfE2 = 2*psfXY/(psfXX + psfYY)
501 medianE1 = np.abs(np.median(starE1 - psfE1))
502 medianE2 = np.abs(np.median(starE2 - psfE2))
503 medianE = np.sqrt(medianE1**2 + medianE2**2)
505 scatterSize =
sigmaMad(starSize - psfSize)
506 scaledScatterSize = scatterSize/medianSize**2
509 if self.config.maxEllipResidual
and medianE > self.config.maxEllipResidual:
510 self.log.info(
"Removing visit %s because median e residual too large: %f vs %f",
511 dataId, medianE, self.config.maxEllipResidual)
513 elif self.config.maxSizeScatter
and scatterSize > self.config.maxSizeScatter:
514 self.log.info(
"Removing visit %s because size scatter is too large: %f vs %f",
515 dataId, scatterSize, self.config.maxSizeScatter)
517 elif self.config.maxScaledSizeScatter
and scaledScatterSize > self.config.maxScaledSizeScatter:
518 self.log.info(
"Removing visit %s because scaled size scatter is too large: %f vs %f",
519 dataId, scaledScatterSize, self.config.maxScaledSizeScatter)
525class BestSeeingWcsSelectImageConfig(WcsSelectImagesTask.ConfigClass):
526 """Base configuration for BestSeeingSelectImagesTask.
528 nImagesMax = pexConfig.RangeField(
530 doc="Maximum number of images to select",
533 maxPsfFwhm = pexConfig.Field(
535 doc=
"Maximum PSF FWHM (in arcseconds) to select",
538 minPsfFwhm = pexConfig.Field(
540 doc=
"Minimum PSF FWHM (in arcseconds) to select",
546 """Select up to a maximum number of the best-seeing images using their Wcs.
548 ConfigClass = BestSeeingWcsSelectImageConfig
550 def runDataRef(self, dataRef, coordList, makeDataRefList=True,
551 selectDataList=None):
552 """Select the best-seeing images in the selectDataList that overlap the patch.
554 This method is the old entry point
for the Gen2 commandline tasks
and drivers
555 Will be deprecated
in v22.
559 dataRef : `lsst.daf.persistence.ButlerDataRef`
560 Data reference
for coadd/tempExp (
with tract, patch)
562 List of ICRS sky coordinates specifying boundary of patch
563 makeDataRefList : `boolean`, optional
564 Construct a list of data references?
565 selectDataList : `list` of `SelectStruct`
566 List of SelectStruct, to consider
for selection
570 result : `lsst.pipe.base.Struct`
571 Result struct
with components:
572 - ``exposureList``: the selected exposures
574 - ``dataRefList``: the optional data references corresponding to
575 each element of ``exposureList``
576 (`list` of `lsst.daf.persistence.ButlerDataRef`,
or `
None`).
580 exposureInfoList = []
582 if selectDataList
is None:
585 result = super().runDataRef(dataRef, coordList, makeDataRefList=
True, selectDataList=selectDataList)
587 for dataRef, exposureInfo
in zip(result.dataRefList, result.exposureInfoList):
588 cal = dataRef.get(
"calexp", immediate=
True)
591 pixToArcseconds = cal.getWcs().getPixelScale().asArcseconds()
593 psfAvgPos = cal.getPsf().getAveragePosition()
594 psfSize = cal.getPsf().computeShape(psfAvgPos).getDeterminantRadius()*pixToArcseconds
595 sizeFwhm = psfSize * np.sqrt(8.*np.log(2.))
596 if self.config.maxPsfFwhm
and sizeFwhm > self.config.maxPsfFwhm:
598 if self.config.minPsfFwhm
and sizeFwhm < self.config.minPsfFwhm:
600 psfSizes.append(sizeFwhm)
601 dataRefList.append(dataRef)
602 exposureInfoList.append(exposureInfo)
604 if len(psfSizes) > self.config.nImagesMax:
605 sortedIndices = np.argsort(psfSizes)[:self.config.nImagesMax]
606 filteredDataRefList = [dataRefList[i]
for i
in sortedIndices]
607 filteredExposureInfoList = [exposureInfoList[i]
for i
in sortedIndices]
608 self.log.info(
"%d images selected with FWHM range of %f--%f arcseconds",
609 len(sortedIndices), psfSizes[sortedIndices[0]], psfSizes[sortedIndices[-1]])
612 if len(psfSizes) == 0:
613 self.log.warning(
"0 images selected.")
615 self.log.debug(
"%d images selected with FWHM range of %d--%d arcseconds",
616 len(psfSizes), psfSizes[0], psfSizes[-1])
617 filteredDataRefList = dataRefList
618 filteredExposureInfoList = exposureInfoList
620 return pipeBase.Struct(
621 dataRefList=filteredDataRefList
if makeDataRefList
else None,
622 exposureInfoList=filteredExposureInfoList,
626class BestSeeingSelectVisitsConnections(pipeBase.PipelineTaskConnections,
627 dimensions=(
"tract",
"patch",
"skymap",
"band",
"instrument"),
628 defaultTemplates={
"coaddName":
"goodSeeing"}):
629 skyMap = pipeBase.connectionTypes.Input(
630 doc=
"Input definition of geometry/bbox and projection/wcs for coadded exposures",
631 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
632 storageClass=
"SkyMap",
633 dimensions=(
"skymap",),
635 visitSummaries = pipeBase.connectionTypes.Input(
636 doc=
"Per-visit consolidated exposure metadata from ConsolidateVisitSummaryTask",
638 storageClass=
"ExposureCatalog",
639 dimensions=(
"instrument",
"visit",),
643 goodVisits = pipeBase.connectionTypes.Output(
644 doc=
"Selected visits to be coadded.",
645 name=
"{coaddName}Visits",
646 storageClass=
"StructuredDataDict",
647 dimensions=(
"instrument",
"tract",
"patch",
"skymap",
"band"),
651class BestSeeingSelectVisitsConfig(pipeBase.PipelineTaskConfig,
652 pipelineConnections=BestSeeingSelectVisitsConnections):
653 nVisitsMax = pexConfig.RangeField(
655 doc=
"Maximum number of visits to select",
659 maxPsfFwhm = pexConfig.Field(
661 doc=
"Maximum PSF FWHM (in arcseconds) to select",
665 minPsfFwhm = pexConfig.Field(
667 doc=
"Minimum PSF FWHM (in arcseconds) to select",
671 doConfirmOverlap = pexConfig.Field(
673 doc=
"Do remove visits that do not actually overlap the patch?",
676 minMJD = pexConfig.Field(
678 doc=
"Minimum visit MJD to select",
682 maxMJD = pexConfig.Field(
684 doc=
"Maximum visit MJD to select",
690class BestSeeingSelectVisitsTask(pipeBase.PipelineTask):
691 """Select up to a maximum number of the best-seeing visits
693 Don't exceed the FWHM range specified by configs min(max)PsfFwhm.
694 This Task is a port of the Gen2 image-selector used
in the AP pipeline:
695 BestSeeingSelectImagesTask. This Task selects full visits based on the
696 average PSF of the entire visit.
698 ConfigClass = BestSeeingSelectVisitsConfig
699 _DefaultName = 'bestSeeingSelectVisits'
701 def runQuantum(self, butlerQC, inputRefs, outputRefs):
702 inputs = butlerQC.get(inputRefs)
703 quantumDataId = butlerQC.quantum.dataId
704 outputs = self.run(**inputs, dataId=quantumDataId)
705 butlerQC.put(outputs, outputRefs)
707 def run(self, visitSummaries, skyMap, dataId):
712 visitSummary : `list`
713 List of `lsst.pipe.base.connections.DeferredDatasetRef` of
715 skyMap : `lsst.skyMap.SkyMap`
716 SkyMap for checking visits overlap patch
717 dataId : `dict` of dataId keys
718 For retrieving patch info
for checking visits overlap patch
722 result : `lsst.pipe.base.Struct`
723 Result struct
with components:
725 - `goodVisits`: `dict`
with selected visit ids
as keys,
726 so that it can be be saved
as a StructuredDataDict.
727 StructuredDataList
's are currently limited.
730 if self.config.doConfirmOverlap:
731 patchPolygon = self.makePatchPolygon(skyMap, dataId)
733 inputVisits = [visitSummary.ref.dataId[
'visit']
for visitSummary
in visitSummaries]
736 for visit, visitSummary
in zip(inputVisits, visitSummaries):
738 visitSummary = visitSummary.get()
741 mjd = visitSummary[0].getVisitInfo().getDate().get(system=DateTime.MJD)
743 pixToArcseconds = [vs.getWcs().getPixelScale(vs.getBBox().getCenter()).asArcseconds()
744 for vs
in visitSummary]
746 psfSigmas = np.array([vs[
'psfSigma']
for vs
in visitSummary])
747 fwhm = np.nanmean(psfSigmas * pixToArcseconds) * np.sqrt(8.*np.log(2.))
749 if self.config.maxPsfFwhm
and fwhm > self.config.maxPsfFwhm:
751 if self.config.minPsfFwhm
and fwhm < self.config.minPsfFwhm:
753 if self.config.minMJD
and mjd < self.config.minMJD:
754 self.log.debug(
'MJD %f earlier than %.2f; rejecting', mjd, self.config.minMJD)
756 if self.config.maxMJD
and mjd > self.config.maxMJD:
757 self.log.debug(
'MJD %f later than %.2f; rejecting', mjd, self.config.maxMJD)
759 if self.config.doConfirmOverlap
and not self.doesIntersectPolygon(visitSummary, patchPolygon):
762 fwhmSizes.append(fwhm)
765 sortedVisits = [ind
for (_, ind)
in sorted(zip(fwhmSizes, visits))]
766 output = sortedVisits[:self.config.nVisitsMax]
767 self.log.info(
"%d images selected with FWHM range of %d--%d arcseconds",
768 len(output), fwhmSizes[visits.index(output[0])], fwhmSizes[visits.index(output[-1])])
771 goodVisits = {key:
True for key
in output}
772 return pipeBase.Struct(goodVisits=goodVisits)
774 def makePatchPolygon(self, skyMap, dataId):
775 """Return True if sky polygon overlaps visit
780 Exposure catalog with per-detector geometry
781 dataId : `dict` of dataId keys
782 For retrieving patch info
786 result :` lsst.sphgeom.ConvexPolygon.convexHull`
787 Polygon of patch
's outer bbox
789 wcs = skyMap[dataId['tract']].getWcs()
790 bbox = skyMap[dataId[
'tract']][dataId[
'patch']].getOuterBBox()
795 def doesIntersectPolygon(self, visitSummary, polygon):
796 """Return True if sky polygon overlaps visit
801 Exposure catalog with per-detector geometry
802 polygon :` lsst.sphgeom.ConvexPolygon.convexHull`
803 Polygon to check overlap
807 doesIntersect: `bool`
808 Does the visit overlap the polygon
810 doesIntersect = False
811 for detectorSummary
in visitSummary:
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 nVisitsMin = pexConfig.Field(
841 doc=
"At least this number of visits selected and supercedes quantile. For example, if 10 visits "
842 "cover this patch, qMin=0.33, and nVisitsMin=5, the best 5 visits will be selected.",
846 doConfirmOverlap = pexConfig.Field(
848 doc=
"Do remove visits that do not actually overlap the patch?",
851 minMJD = pexConfig.Field(
853 doc=
"Minimum visit MJD to select",
857 maxMJD = pexConfig.Field(
859 doc=
"Maximum visit MJD to select",
865class BestSeeingQuantileSelectVisitsTask(BestSeeingSelectVisitsTask):
866 """Select a quantile of the best-seeing visits
868 Selects the best (for example, third) full visits based on the average
869 PSF width
in the entire visit. It can also be used
for difference imaging
870 experiments that require templates
with the worst seeing visits.
871 For example, selecting the worst third can be acheived by
872 changing the config parameters qMin to 0.66
and qMax to 1.
874 ConfigClass = BestSeeingQuantileSelectVisitsConfig
875 _DefaultName = 'bestSeeingQuantileSelectVisits'
877 @utils.inheritDoc(BestSeeingSelectVisitsTask)
878 def run(self, visitSummaries, skyMap, dataId):
879 if self.config.doConfirmOverlap:
880 patchPolygon = self.makePatchPolygon(skyMap, dataId)
881 visits = np.array([visitSummary.ref.dataId[
'visit']
for visitSummary
in visitSummaries])
882 radius = np.empty(len(visits))
883 intersects = np.full(len(visits),
True)
884 for i, visitSummary
in enumerate(visitSummaries):
886 visitSummary = visitSummary.get()
888 psfSigma = np.nanmedian([vs[
'psfSigma']
for vs
in visitSummary])
890 if self.config.doConfirmOverlap:
891 intersects[i] = self.doesIntersectPolygon(visitSummary, patchPolygon)
892 if self.config.minMJD
or self.config.maxMJD:
894 mjd = visitSummary[0].getVisitInfo().getDate().get(system=DateTime.MJD)
895 aboveMin = mjd > self.config.minMJD
if self.config.minMJD
else True
896 belowMax = mjd < self.config.maxMJD
if self.config.maxMJD
else True
897 intersects[i] = intersects[i]
and aboveMin
and belowMax
899 sortedVisits = [v
for rad, v
in sorted(zip(radius[intersects], visits[intersects]))]
900 lowerBound = min(int(np.round(self.config.qMin*len(visits[intersects]))),
901 max(0, len(visits[intersects]) - self.config.nVisitsMin))
902 upperBound = max(int(np.round(self.config.qMax*len(visits[intersects]))), self.config.nVisitsMin)
905 goodVisits = {int(visit):
True for visit
in sortedVisits[lowerBound:upperBound]}
906 return pipeBase.Struct(goodVisits=goodVisits)
def __init__(self, dataId, coordList)
def _runArgDictFromDataId(self, dataId)
def runDataRef(self, dataRef, coordList, makeDataRefList=True, selectDataList=[])
def __init__(self, dataRef, wcs, bbox)
def getValidImageCorners(self, imageWcs, imageBox, patchPoly, dataId=None)
def runDataRef(self, dataRef, coordList, makeDataRefList=True, selectDataList=[])
def run(self, wcsList, bboxList, coordList, dataIds=None, **kwargs)
static ConvexPolygon convexHull(std::vector< UnitVector3d > const &points)