LSST Applications g04e9c324dd+8c5ae1fdc5,g134cb467dc+1b3060144d,g18429d2f64+f642bf4753,g199a45376c+0ba108daf9,g1fd858c14a+2dcf163641,g262e1987ae+7b8c96d2ca,g29ae962dfc+3bd6ecb08a,g2cef7863aa+aef1011c0b,g35bb328faa+8c5ae1fdc5,g3fd5ace14f+53e1a9e7c5,g4595892280+fef73a337f,g47891489e3+2efcf17695,g4d44eb3520+642b70b07e,g53246c7159+8c5ae1fdc5,g67b6fd64d1+2efcf17695,g67fd3c3899+b70e05ef52,g74acd417e5+317eb4c7d4,g786e29fd12+668abc6043,g87389fa792+8856018cbb,g89139ef638+2efcf17695,g8d7436a09f+3be3c13596,g8ea07a8fe4+9f5ccc88ac,g90f42f885a+a4e7b16d9b,g97be763408+ad77d7208f,g9dd6db0277+b70e05ef52,ga681d05dcb+a3f46e7fff,gabf8522325+735880ea63,gac2eed3f23+2efcf17695,gb89ab40317+2efcf17695,gbf99507273+8c5ae1fdc5,gd8ff7fe66e+b70e05ef52,gdab6d2f7ff+317eb4c7d4,gdc713202bf+b70e05ef52,gdfd2d52018+b10e285e0f,ge365c994fd+310e8507c4,ge410e46f29+2efcf17695,geaed405ab2+562b3308c0,gffca2db377+8c5ae1fdc5,w.2025.35
LSST Data Management Base Package
Loading...
Searching...
No Matches
selectImages.py
Go to the documentation of this file.
1# This file is part of pipe_tasks.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21
22__all__ = ["BaseSelectImagesTask", "BaseExposureInfo", "WcsSelectImagesTask", "PsfWcsSelectImagesTask",
23 "DatabaseSelectImagesConfig", "BestSeeingSelectVisitsTask",
24 "BestSeeingQuantileSelectVisitsTask"]
25
26import numpy as np
27import lsst.sphgeom
28import lsst.pex.config as pexConfig
29import lsst.pex.exceptions as pexExceptions
30import lsst.geom as geom
31import lsst.pipe.base as pipeBase
32from lsst.skymap import BaseSkyMap
33from lsst.daf.base import DateTime
34from lsst.utils.timer import timeMethod
35
36
37def _meetsVisitSummaryMinValues(visit, visitSummary, visitSummaryMinValues, logger=None):
38 """Check if the visitSummary meets minimum values in visitSummaryMinValues.
39
40 Parameters
41 ----------
42 visit : `int`
43 Visit number.
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.
50
51 Returns
52 -------
53 result : `bool`
54 True if all columns in visitSummary meet the minimum values specified
55 in visitSummaryMinValues, False otherwise.
56 """
57 for columnName, minThreshold in visitSummaryMinValues.items():
58 # Get values for this column across all detectors with a valid WCS
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}).')
65 return False
66 return True
67
68
69class DatabaseSelectImagesConfig(pexConfig.Config):
70 """Base configuration for subclasses of BaseSelectImagesTask that use a
71 database.
72 """
73
74 host = pexConfig.Field(
75 doc="Database server host name",
76 dtype=str,
77 )
78 port = pexConfig.Field(
79 doc="Database server port",
80 dtype=int,
81 )
82 database = pexConfig.Field(
83 doc="Name of database",
84 dtype=str,
85 )
86 maxExposures = pexConfig.Field(
87 doc="maximum exposures to select; intended for debugging; ignored if None",
88 dtype=int,
89 optional=True,
90 )
91
92
93class BaseExposureInfo(pipeBase.Struct):
94 """Data about a selected exposure.
95
96 Parameters
97 ----------
98 dataId : `dict`
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.
103 """
104
105 def __init__(self, dataId, coordList):
106 super(BaseExposureInfo, self).__init__(dataId=dataId, coordList=coordList)
107
108
109class BaseSelectImagesTask(pipeBase.Task):
110 """Base task for selecting images suitable for coaddition.
111 """
112
113 ConfigClass = pexConfig.Config
114 _DefaultName = "selectImages"
115
116 @timeMethod
117 def run(self, coordList):
118 """Select images suitable for coaddition in a particular region.
119
120 Parameters
121 ----------
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,
125 as required.
126
127 Returns
128 -------
129 result : `pipeBase.Struct`
130 Results as a struct with attributes:
131
132 ``exposureInfoList``
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`])
138 """
139 raise NotImplementedError()
140
141
142def _extractKeyValue(dataList, keys=None):
143 """Extract the keys and values from a list of dataIds.
144
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.
148
149 Parameters
150 ----------
151 dataList : `Unknown`
152 keys : `Unknown`
153
154 Returns
155 -------
156 keys : `Unknown`
157 values : `Unknown`
158
159 Raises
160 ------
161 RuntimeError
162 Raised if DataId keys are inconsistent.
163 """
164 assert len(dataList) > 0
165 if keys is None:
166 keys = sorted(dataList[0].dataId.keys())
167 keySet = set(keys)
168 values = list()
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))
174 return keys, values
175
176
177class SelectStruct(pipeBase.Struct):
178 """A container for data to be passed to the WcsSelectImagesTask.
179
180 Parameters
181 ----------
182 dataRef : `Unknown`
183 Data reference.
184 wcs : `lsst.afw.geom.SkyWcs`
185 Coordinate system definition (wcs).
186 bbox : `lsst.geom.box.Box2I`
187 Integer bounding box for image.
188 """
189
190 def __init__(self, dataRef, wcs, bbox):
191 super(SelectStruct, self).__init__(dataRef=dataRef, wcs=wcs, bbox=bbox)
192
193
194class WcsSelectImagesConfig(pexConfig.Config):
195 excludeDetectors = pexConfig.ListField(
196 dtype=int,
197 default=[],
198 doc="Detectors to exclude from selection.",
199 optional=True,
200 )
201
202
204 """Select images using their Wcs.
205
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.
209
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.
213 """
214
215 ConfigClass = WcsSelectImagesConfig
216 _DefaultName = "WcsSelectImages"
217
218 def run(self, wcsList, bboxList, coordList, dataIds=None, **kwargs):
219 """Return indices of provided lists that meet the selection criteria.
220
221 Parameters
222 ----------
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.
231 **kwargs
232 Additional keyword arguments.
233
234 Returns
235 -------
236 result : `list` [`int`]
237 The indices of selected ccds.
238 """
239 if dataIds is None:
240 dataIds = [None] * len(wcsList)
241 patchVertices = [coord.getVector() for coord in coordList]
242 patchPoly = lsst.sphgeom.ConvexPolygon.convexHull(patchVertices)
243 result = []
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)
250 else:
251 imageCorners = self.getValidImageCorners(imageWcs, imageBox, patchPoly, dataId)
252 if imageCorners:
253 result.append(i)
254 return result
255
256 def getValidImageCorners(self, imageWcs, imageBox, patchPoly, dataId=None):
257 """Return corners or `None` if bad.
258
259 Parameters
260 ----------
261 imageWcs : `Unknown`
262 imageBox : `Unknown`
263 patchPoly : `Unknown`
264 dataId : `Unknown`
265 """
266 try:
267 imageCorners = [imageWcs.pixelToSky(pix) for pix in geom.Box2D(imageBox).getCorners()]
268 except (pexExceptions.DomainError, pexExceptions.RuntimeError) as e:
269 # Protecting ourselves from awful Wcs solutions in input images
270 self.log.debug("WCS error in testing calexp %s (%s): deselecting", dataId, e)
271 return None
272
273 imagePoly = lsst.sphgeom.ConvexPolygon.convexHull([coord.getVector() for coord in imageCorners])
274 if imagePoly is None:
275 self.log.debug("Unable to create polygon from image %s: deselecting", dataId)
276 return None
277
278 if patchPoly.intersects(imagePoly):
279 # "intersects" also covers "contains" or "is contained by"
280 self.log.info("Selecting calexp %s", dataId)
281 return imageCorners
282
283 return None
284
285
286class PsfWcsSelectImagesConnections(pipeBase.PipelineTaskConnections,
287 dimensions=("tract", "patch", "skymap", "instrument", "visit"),
288 defaultTemplates={"coaddName": "deep"}):
289 pass
290
291
292class PsfWcsSelectImagesConfig(pipeBase.PipelineTaskConfig,
293 pipelineConnections=PsfWcsSelectImagesConnections):
294 maxEllipResidual = pexConfig.Field(
295 doc="Maximum median ellipticity residual",
296 dtype=float,
297 default=0.007,
298 optional=True,
299 )
300 maxSizeScatter = pexConfig.Field(
301 doc="Maximum scatter in the size residuals",
302 dtype=float,
303 optional=True,
304 )
305 maxScaledSizeScatter = pexConfig.Field(
306 doc="Maximum scatter in the size residuals, scaled by the median size",
307 dtype=float,
308 default=0.019,
309 optional=True,
310 )
311 maxPsfTraceRadiusDelta = pexConfig.Field(
312 doc="Maximum delta (max - min) of model PSF trace radius values evaluated on a grid on "
313 "the unmasked detector pixels (pixel).",
314 dtype=float,
315 default=0.7,
316 optional=True,
317 )
318 maxPsfApFluxDelta = pexConfig.Field(
319 doc="Maximum delta (max - min) of model PSF aperture flux (with aperture radius of "
320 "max(2, 3*psfSigma)) values evaluated on a grid on the unmasked detector pixels (based "
321 "on a normalized-to-one flux).",
322 dtype=float,
323 default=0.24,
324 optional=True,
325 )
326 maxPsfApCorrSigmaScaledDelta = pexConfig.Field(
327 doc="Maximum delta (max - min) of model PSF aperture correction values evaluated on a grid "
328 "on the unmasked detector pixels scaled (divided) by the measured model psfSigma.",
329 dtype=float,
330 default=0.22,
331 optional=True,
332 )
333 minNPsfStarPerBand = pexConfig.DictField(
334 keytype=str,
335 itemtype=int,
336 default={
337 "u": 6,
338 "g": 6,
339 "r": 6,
340 "i": 6,
341 "z": 6,
342 "y": 6,
343 "fallback": 6,
344 },
345 doc="Minimum number of PSF stars for the final PSF model to be considered "
346 "well-constrained and suitible for inclusion in the coadd. This number should "
347 "take into consideration the spatial order used for the PSF model. If the current "
348 "band for the exposure is not included as a key in this dict, the value associated "
349 "with the \"fallback\" key will be used.",
350 )
351 excludeDetectors = pexConfig.ListField(
352 dtype=int,
353 default=[],
354 doc="Detectors to exclude from selection.",
355 optional=True,
356 )
357
358 def validate(self):
359 super().validate()
360 if "fallback" not in self.minNPsfStarPerBand:
361 msg = ("Must include a \"fallback\" key in the config.minNPsfStarPerBand config dict. "
362 f"It is currenly: {self.minNPsfStarPerBand}.")
363 raise pexConfig.FieldValidationError(self.__class__.minNPsfStarPerBand, self, msg)
364
365
367 """Select images using their Wcs and cuts on the PSF properties.
368
369 The PSF quality criteria are based on the size and ellipticity
370 residuals from the adaptive second moments of the star and the PSF.
371
372 The criteria are:
373 - the median of the ellipticty residuals.
374 - the robust scatter of the size residuals (using the median absolute
375 deviation).
376 - the robust scatter of the size residuals scaled by the square of
377 the median size.
378 """
379
380 ConfigClass = PsfWcsSelectImagesConfig
381 _DefaultName = "PsfWcsSelectImages"
382
383 def run(self, wcsList, bboxList, coordList, visitSummary, dataIds=None, **kwargs):
384 """Return indices of provided lists that meet the selection criteria.
385
386 Parameters
387 ----------
388 wcsList : `list` [`lsst.afw.geom.SkyWcs`]
389 Specifying the WCS's of the input ccds to be selected.
390 bboxList : `list` [`lsst.geom.Box2I`]
391 Specifying the bounding boxes of the input ccds to be selected.
392 coordList : `list` [`lsst.geom.SpherePoint`]
393 ICRS coordinates specifying boundary of the patch.
394 visitSummary : `list` [`lsst.afw.table.ExposureCatalog`]
395 containing the PSF shape information for the input ccds to be
396 selected.
397 dataIds : iterable [`lsst.daf.butler.dataId`] or `None`, optional
398 An iterable object of dataIds which point to reference catalogs.
399 **kwargs
400 Additional keyword arguments.
401
402 Returns
403 -------
404 goodPsf : `list` [`int`]
405 The indices of selected ccds.
406 """
407 goodWcs = super(PsfWcsSelectImagesTask, self).run(wcsList=wcsList, bboxList=bboxList,
408 coordList=coordList, dataIds=dataIds)
409
410 goodPsf = []
411
412 for i, dataId in enumerate(dataIds):
413 if i not in goodWcs:
414 continue
415 if self.isValid(visitSummary, dataId["detector"]):
416 goodPsf.append(i)
417
418 return goodPsf
419
420 def isValid(self, visitSummary, detectorId):
421 """Should this ccd be selected based on its PSF shape information.
422
423 Parameters
424 ----------
425 visitSummary : `lsst.afw.table.ExposureCatalog`
426 Exposure catalog with per-detector summary information.
427 detectorId : `int`
428 Detector identifier.
429
430 Returns
431 -------
432 valid : `bool`
433 True if selected.
434 """
435 row = visitSummary.find(detectorId)
436 if row is None:
437 # This is not listed, so it must be bad.
438 self.log.warning("Removing detector %d because summary stats not available.", detectorId)
439 return False
440
441 band = row["band"]
442 if band in self.config.minNPsfStarPerBand:
443 minNPsfStar = self.config.minNPsfStarPerBand[band]
444 else:
445 minNPsfStar = self.config.minNPsfStarPerBand["fallback"]
446 nPsfStar = row["nPsfStar"]
447 medianE = np.sqrt(row["psfStarDeltaE1Median"]**2. + row["psfStarDeltaE2Median"]**2.)
448 scatterSize = row["psfStarDeltaSizeScatter"]
449 scaledScatterSize = row["psfStarScaledDeltaSizeScatter"]
450 psfTraceRadiusDelta = row["psfTraceRadiusDelta"]
451 psfApFluxDelta = row["psfApFluxDelta"]
452 psfApCorrSigmaScaledDelta = row["psfApCorrSigmaScaledDelta"]
453
454 valid = True
455 if self.config.maxEllipResidual and not (medianE <= self.config.maxEllipResidual):
456 self.log.info("Removing visit %d detector %d because median e residual too large: %f vs %f",
457 row["visit"], detectorId, medianE, self.config.maxEllipResidual)
458 valid = False
459 elif self.config.maxSizeScatter and not (scatterSize <= self.config.maxSizeScatter):
460 self.log.info("Removing visit %d detector %d because size scatter too large: %f vs %f",
461 row["visit"], detectorId, scatterSize, self.config.maxSizeScatter)
462 valid = False
463 elif self.config.maxScaledSizeScatter and not (scaledScatterSize <= self.config.maxScaledSizeScatter):
464 self.log.info("Removing visit %d detector %d because scaled size scatter too large: %f vs %f",
465 row["visit"], detectorId, scaledScatterSize, self.config.maxScaledSizeScatter)
466 valid = False
467 elif (
468 self.config.maxPsfTraceRadiusDelta is not None
469 and not (psfTraceRadiusDelta <= self.config.maxPsfTraceRadiusDelta)
470 ):
471 self.log.info(
472 "Removing visit %d detector %d because max-min delta of model PSF trace radius values "
473 "across the unmasked detector pixels is not finite or too large: %.3f vs %.3f (pixels)",
474 row["visit"], detectorId, psfTraceRadiusDelta, self.config.maxPsfTraceRadiusDelta
475 )
476 valid = False
477 elif (
478 self.config.maxPsfApFluxDelta is not None
479 and not (psfApFluxDelta <= self.config.maxPsfApFluxDelta)
480 ):
481 self.log.info(
482 "Removing visit %d detector %d because max-min delta of model PSF aperture flux values "
483 "across the unmasked detector pixels is not finite or too large: %.3f vs %.3f (pixels)",
484 row["visit"], detectorId, psfApFluxDelta, self.config.maxPsfApFluxDelta
485 )
486 valid = False
487 elif (
488 self.config.maxPsfApCorrSigmaScaledDelta is not None
489 and not (psfApCorrSigmaScaledDelta <= self.config.maxPsfApCorrSigmaScaledDelta)
490 ):
491 self.log.info(
492 "Removing visit %d detector %d because max-min delta of the model PSF apterture correction "
493 "values scaled (divided) by the psfSigma across the unmasked detector pixels is not "
494 "finite or too large: %.3f vs %.3f (fatcor)",
495 row["visit"], detectorId, psfApCorrSigmaScaledDelta, self.config.maxPsfApCorrSigmaScaledDelta
496 )
497 valid = False
498 elif minNPsfStar and (nPsfStar < minNPsfStar):
499 self.log.info(
500 "Removing visit %d detector %d because the PSF model had too few stars: %d vs %d",
501 row["visit"], detectorId, nPsfStar, minNPsfStar
502 )
503 valid = False
504
505 return valid
506
507
508class BestSeeingSelectVisitsConnections(pipeBase.PipelineTaskConnections,
509 dimensions=("tract", "patch", "skymap", "band", "instrument"),
510 defaultTemplates={"coaddName": "goodSeeing",
511 "calexpType": ""}):
512 skyMap = pipeBase.connectionTypes.Input(
513 doc="Input definition of geometry/bbox and projection/wcs for coadded exposures",
514 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
515 storageClass="SkyMap",
516 dimensions=("skymap",),
517 )
518 visitSummaries = pipeBase.connectionTypes.Input(
519 doc="Per-visit consolidated exposure metadata",
520 name="finalVisitSummary",
521 storageClass="ExposureCatalog",
522 dimensions=("instrument", "visit",),
523 multiple=True,
524 deferLoad=True
525 )
526 goodVisits = pipeBase.connectionTypes.Output(
527 doc="Selected visits to be coadded.",
528 name="{coaddName}Visits",
529 storageClass="StructuredDataDict",
530 dimensions=("instrument", "tract", "patch", "skymap", "band"),
531 )
532
533
534class BestSeeingSelectVisitsConfig(pipeBase.PipelineTaskConfig,
535 pipelineConnections=BestSeeingSelectVisitsConnections):
536 nVisitsMax = pexConfig.RangeField(
537 dtype=int,
538 doc="Maximum number of visits to select; use -1 to select all.",
539 default=12,
540 min=-1,
541 )
542 maxPsfFwhm = pexConfig.Field(
543 dtype=float,
544 doc="Maximum PSF FWHM (in arcseconds) to select",
545 default=1.5,
546 optional=True
547 )
548 minPsfFwhm = pexConfig.Field(
549 dtype=float,
550 doc="Minimum PSF FWHM (in arcseconds) to select",
551 default=0.,
552 optional=True
553 )
554 doConfirmOverlap = pexConfig.Field(
555 dtype=bool,
556 doc="Do remove visits that do not actually overlap the patch?",
557 default=True,
558 )
559 minMJD = pexConfig.Field(
560 dtype=float,
561 doc="Minimum visit MJD to select",
562 default=None,
563 optional=True
564 )
565 maxMJD = pexConfig.Field(
566 dtype=float,
567 doc="Maximum visit MJD to select",
568 default=None,
569 optional=True
570 )
571 visitSummaryMinValues = pexConfig.DictField(
572 keytype=str,
573 itemtype=float,
574 doc=("Optional thresholds for visit selection. Keys are visit_summary column names"
575 "(e.g. 'effTimeZeroPointScale'), and values are minimum allowed values."),
576 default=None,
577 optional=True
578 )
579
580
581class BestSeeingSelectVisitsTask(pipeBase.PipelineTask):
582 """Select up to a maximum number of the best-seeing visits.
583
584 Don't exceed the FWHM range specified by configs min(max)PsfFwhm.
585 This Task is a port of the Gen2 image-selector used in the AP pipeline:
586 BestSeeingSelectImagesTask. This Task selects full visits based on the
587 average PSF of the entire visit.
588 """
589
590 ConfigClass = BestSeeingSelectVisitsConfig
591 _DefaultName = 'bestSeeingSelectVisits'
592
593 def runQuantum(self, butlerQC, inputRefs, outputRefs):
594 inputs = butlerQC.get(inputRefs)
595 quantumDataId = butlerQC.quantum.dataId
596 outputs = self.run(**inputs, dataId=quantumDataId)
597 butlerQC.put(outputs, outputRefs)
598
599 def run(self, visitSummaries, skyMap, dataId):
600 """Run task.
601
602 Parameters
603 ----------
604 visitSummary : `list` [`lsst.pipe.base.connections.DeferredDatasetRef`]
605 List of `lsst.pipe.base.connections.DeferredDatasetRef` of
606 visitSummary tables of type `lsst.afw.table.ExposureCatalog`.
607 skyMap : `lsst.skyMap.SkyMap`
608 SkyMap for checking visits overlap patch.
609 dataId : `dict` of dataId keys
610 For retrieving patch info for checking visits overlap patch.
611
612 Returns
613 -------
614 result : `lsst.pipe.base.Struct`
615 Results as a struct with attributes:
616
617 ``goodVisits``
618 A `dict` with selected visit ids as keys,
619 so that it can be be saved as a StructuredDataDict.
620 StructuredDataList's are currently limited.
621 """
622 if self.config.doConfirmOverlap:
623 patchPolygon = self.makePatchPolygon(skyMap, dataId)
624
625 inputVisits = [visitSummary.ref.dataId['visit'] for visitSummary in visitSummaries]
626 fwhmSizes = []
627 visits = []
628 for visit, visitSummary in zip(inputVisits, visitSummaries):
629 # read in one-by-one and only once. There may be hundreds
630 visitSummary = visitSummary.get()
631
632 # mjd is guaranteed to be the same for every detector in the
633 # visitSummary.
634 mjd = visitSummary[0].getVisitInfo().getDate().get(system=DateTime.MJD)
635
636 pixelScales = np.array([vs['pixelScale'] for vs in visitSummary if vs.getWcs()])
637 # psfSigma is PSF model determinant radius at chip center in pixels
638 psfSigmas = np.array([vs['psfSigma'] for vs in visitSummary if vs.getWcs()])
639 fwhm = np.nanmean(psfSigmas * pixelScales) * np.sqrt(8.*np.log(2.))
640
641 if self.config.maxPsfFwhm and fwhm > self.config.maxPsfFwhm:
642 continue
643 if self.config.minPsfFwhm and fwhm < self.config.minPsfFwhm:
644 continue
645 if self.config.minMJD and mjd < self.config.minMJD:
646 self.log.debug('MJD %f earlier than %.2f; rejecting', mjd, self.config.minMJD)
647 continue
648 if self.config.maxMJD and mjd > self.config.maxMJD:
649 self.log.debug('MJD %f later than %.2f; rejecting', mjd, self.config.maxMJD)
650 continue
651 if self.config.doConfirmOverlap and not self.doesIntersectPolygon(visitSummary, patchPolygon):
652 continue
653 if (self.config.visitSummaryMinValues
654 and not _meetsVisitSummaryMinValues(visit, visitSummary, self.config.visitSummaryMinValues,
655 self.log)):
656 continue
657
658 fwhmSizes.append(fwhm)
659 visits.append(visit)
660
661 sortedVisits = [ind for (_, ind) in sorted(zip(fwhmSizes, visits))]
662 if self.config.nVisitsMax < 0:
663 output = sortedVisits
664 else:
665 output = sortedVisits[:self.config.nVisitsMax]
666
667 if len(output) == 0:
668 self.log.info("All images rejected in BestSeeingSelectVisitsTask.")
669 raise pipeBase.NoWorkFound(f"No good images found for {dataId}")
670 else:
671 self.log.info(
672 "%d images selected with FWHM range of %f--%f arcseconds",
673 len(output),
674 fwhmSizes[visits.index(output[0])],
675 fwhmSizes[visits.index(output[-1])],
676 )
677
678 # In order to store as a StructuredDataDict, convert list to dict
679 goodVisits = {key: True for key in output}
680 return pipeBase.Struct(goodVisits=goodVisits)
681
682 def makePatchPolygon(self, skyMap, dataId):
683 """Return True if sky polygon overlaps visit.
684
685 Parameters
686 ----------
687 skyMap : `lsst.afw.table.ExposureCatalog`
688 Exposure catalog with per-detector geometry.
689 dataId : `dict` of dataId keys
690 For retrieving patch info.
691
692 Returns
693 -------
694 result : `lsst.sphgeom.ConvexPolygon.convexHull`
695 Polygon of patch's outer bbox.
696 """
697 wcs = skyMap[dataId['tract']].getWcs()
698 bbox = skyMap[dataId['tract']][dataId['patch']].getOuterBBox()
699 sphCorners = wcs.pixelToSky(lsst.geom.Box2D(bbox).getCorners())
700 result = lsst.sphgeom.ConvexPolygon.convexHull([coord.getVector() for coord in sphCorners])
701 return result
702
703 def doesIntersectPolygon(self, visitSummary, polygon):
704 """Return True if sky polygon overlaps visit.
705
706 Parameters
707 ----------
708 visitSummary : `lsst.afw.table.ExposureCatalog`
709 Exposure catalog with per-detector geometry.
710 polygon :` lsst.sphgeom.ConvexPolygon.convexHull`
711 Polygon to check overlap.
712
713 Returns
714 -------
715 doesIntersect : `bool`
716 True if the visit overlaps the polygon.
717 """
718 doesIntersect = False
719 for detectorSummary in visitSummary:
720 if (np.all(np.isfinite(detectorSummary['raCorners']))
721 and np.all(np.isfinite(detectorSummary['decCorners']))):
722 corners = [lsst.geom.SpherePoint(ra, dec, units=lsst.geom.degrees).getVector()
723 for (ra, dec) in
724 zip(detectorSummary['raCorners'], detectorSummary['decCorners'])]
725 detectorPolygon = lsst.sphgeom.ConvexPolygon.convexHull(corners)
726 if detectorPolygon.intersects(polygon):
727 doesIntersect = True
728 break
729 return doesIntersect
730
731
732class BestSeeingQuantileSelectVisitsConfig(pipeBase.PipelineTaskConfig,
733 pipelineConnections=BestSeeingSelectVisitsConnections):
734 qMin = pexConfig.RangeField(
735 doc="Lower bound of quantile range to select. Sorts visits by seeing from narrow to wide, "
736 "and select those in the interquantile range (qMin, qMax). Set qMin to 0 for Best Seeing. "
737 "This config should be changed from zero only for exploratory diffIm testing.",
738 dtype=float,
739 default=0,
740 min=0,
741 max=1,
742 )
743 qMax = pexConfig.RangeField(
744 doc="Upper bound of quantile range to select. Sorts visits by seeing from narrow to wide, "
745 "and select those in the interquantile range (qMin, qMax). Set qMax to 1 for Worst Seeing.",
746 dtype=float,
747 default=0.33,
748 min=0,
749 max=1,
750 )
751 nVisitsMin = pexConfig.Field(
752 doc="The minimum number of visits to select, if qMin and qMax alone would have "
753 "selected fewer. In regimes with many visits, at least this number of visits will be "
754 "selected, superceding quantile when necessary. "
755 "For example, if 10 visits cover this patch, qMin=0, qMax=0.33, and nVisitsMin=5, "
756 "the best 5 visits will be selected, even though 5 > 0.33*10. "
757 "In regimes with few visits, all available visits will be selected. "
758 "For example, if 2 visits cover this patch and nVisitsMin=12, "
759 "both visits will be selected regardless of qMin and qMax.",
760 dtype=int,
761 default=6,
762 )
763 doConfirmOverlap = pexConfig.Field(
764 dtype=bool,
765 doc="Do remove visits that do not actually overlap the patch?",
766 default=True,
767 )
768 minMJD = pexConfig.Field(
769 dtype=float,
770 doc="Minimum visit MJD to select",
771 default=None,
772 optional=True
773 )
774 maxMJD = pexConfig.Field(
775 dtype=float,
776 doc="Maximum visit MJD to select",
777 default=None,
778 optional=True
779 )
780 visitSummaryMinValues = pexConfig.DictField(
781 keytype=str,
782 itemtype=float,
783 doc=("Optional thresholds for visit selection. Keys are visit_summary column names"
784 "(e.g. 'effTimeZeroPointScale'), and values are minimum allowed values."),
785 default=None,
786 optional=True
787 )
788
789
790class BestSeeingQuantileSelectVisitsTask(BestSeeingSelectVisitsTask):
791 """Select a quantile of the best-seeing visits.
792
793 Selects the best (for example, third) full visits based on the average
794 PSF width in the entire visit. It can also be used for difference imaging
795 experiments that require templates with the worst seeing visits.
796 For example, selecting the worst third can be acheived by
797 changing the config parameters qMin to 0.66 and qMax to 1.
798 """
799 ConfigClass = BestSeeingQuantileSelectVisitsConfig
800 _DefaultName = 'bestSeeingQuantileSelectVisits'
801
802 def run(self, visitSummaries, skyMap, dataId):
803 if self.config.doConfirmOverlap:
804 patchPolygon = self.makePatchPolygon(skyMap, dataId)
805 visits = np.array([visitSummary.ref.dataId['visit'] for visitSummary in visitSummaries])
806 radius = np.empty(len(visits))
807 intersects = np.full(len(visits), True)
808 for i, visitSummary in enumerate(visitSummaries):
809 # read in one-by-one and only once. There may be hundreds
810 visitSummary = visitSummary.get()
811 # psfSigma is PSF model determinant radius at chip center in pixels
812 psfSigma = np.nanmedian([vs['psfSigma'] for vs in visitSummary])
813 radius[i] = psfSigma
814 if self.config.doConfirmOverlap:
815 intersects[i] = self.doesIntersectPolygon(visitSummary, patchPolygon)
816 if self.config.minMJD or self.config.maxMJD:
817 # mjd is guaranteed to be the same for every detector in the
818 # visitSummary.
819 mjd = visitSummary[0].getVisitInfo().getDate().get(system=DateTime.MJD)
820 aboveMin = mjd > self.config.minMJD if self.config.minMJD else True
821 belowMax = mjd < self.config.maxMJD if self.config.maxMJD else True
822 intersects[i] = intersects[i] and aboveMin and belowMax
823 if (self.config.visitSummaryMinValues
824 and not _meetsVisitSummaryMinValues(visits[i], visitSummary,
825 self.config.visitSummaryMinValues, self.log)):
826 intersects[i] = False
827
828 sortedVisits = [v for rad, v in sorted(zip(radius[intersects], visits[intersects]))]
829 lowerBound = min(int(np.round(self.config.qMin*len(visits[intersects]))),
830 max(0, len(visits[intersects]) - self.config.nVisitsMin))
831 upperBound = max(int(np.round(self.config.qMax*len(visits[intersects]))), self.config.nVisitsMin)
832
833 # In order to store as a StructuredDataDict, convert list to dict
834 goodVisits = {int(visit): True for visit in sortedVisits[lowerBound:upperBound]}
835 return pipeBase.Struct(goodVisits=goodVisits)
A floating-point coordinate rectangle geometry.
Definition Box.h:413
Point in an unspecified spherical coordinate system.
Definition SpherePoint.h:57
run(self, wcsList, bboxList, coordList, visitSummary, dataIds=None, **kwargs)
run(self, wcsList, bboxList, coordList, dataIds=None, **kwargs)
getValidImageCorners(self, imageWcs, imageBox, patchPoly, dataId=None)
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...
_extractKeyValue(dataList, keys=None)
_meetsVisitSummaryMinValues(visit, visitSummary, visitSummaryMinValues, logger=None)