Loading [MathJax]/extensions/tex2jax.js
LSST Applications g0fba68d861+05816baf74,g1ec0fe41b4+f536777771,g1fd858c14a+a9301854fb,g35bb328faa+fcb1d3bbc8,g4af146b050+a5c07d5b1d,g4d2262a081+6e5fcc2a4e,g53246c7159+fcb1d3bbc8,g56a49b3a55+9c12191793,g5a012ec0e7+3632fc3ff3,g60b5630c4e+ded28b650d,g67b6fd64d1+ed4b5058f4,g78460c75b0+2f9a1b4bcd,g786e29fd12+cf7ec2a62a,g8352419a5c+fcb1d3bbc8,g87b7deb4dc+7b42cf88bf,g8852436030+e5453db6e6,g89139ef638+ed4b5058f4,g8e3bb8577d+d38d73bdbd,g9125e01d80+fcb1d3bbc8,g94187f82dc+ded28b650d,g989de1cb63+ed4b5058f4,g9d31334357+ded28b650d,g9f33ca652e+50a8019d8c,gabe3b4be73+1e0a283bba,gabf8522325+fa80ff7197,gb1101e3267+d9fb1f8026,gb58c049af0+f03b321e39,gb665e3612d+2a0c9e9e84,gb89ab40317+ed4b5058f4,gcf25f946ba+e5453db6e6,gd6cbbdb0b4+bb83cc51f8,gdd1046aedd+ded28b650d,gde0f65d7ad+941d412827,ge278dab8ac+d65b3c2b70,ge410e46f29+ed4b5058f4,gf23fb2af72+b7cae620c0,gf5e32f922b+fcb1d3bbc8,gf67bdafdda+ed4b5058f4,w.2025.16
LSST Data Management Base Package
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
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
37class DatabaseSelectImagesConfig(pexConfig.Config):
38 """Base configuration for subclasses of BaseSelectImagesTask that use a
39 database.
40 """
41
42 host = pexConfig.Field(
43 doc="Database server host name",
44 dtype=str,
45 )
46 port = pexConfig.Field(
47 doc="Database server port",
48 dtype=int,
49 )
50 database = pexConfig.Field(
51 doc="Name of database",
52 dtype=str,
53 )
54 maxExposures = pexConfig.Field(
55 doc="maximum exposures to select; intended for debugging; ignored if None",
56 dtype=int,
57 optional=True,
58 )
59
60
61class BaseExposureInfo(pipeBase.Struct):
62 """Data about a selected exposure.
63
64 Parameters
65 ----------
66 dataId : `dict`
67 Data ID keys of exposure.
68 coordList : `list` [`lsst.afw.geom.SpherePoint`]
69 ICRS coordinates of the corners of the exposure
70 plus any others items that are desired.
71 """
72
73 def __init__(self, dataId, coordList):
74 super(BaseExposureInfo, self).__init__(dataId=dataId, coordList=coordList)
75
76
77class BaseSelectImagesTask(pipeBase.Task):
78 """Base task for selecting images suitable for coaddition.
79 """
80
81 ConfigClass = pexConfig.Config
82 _DefaultName = "selectImages"
83
84 @timeMethod
85 def run(self, coordList):
86 """Select images suitable for coaddition in a particular region.
87
88 Parameters
89 ----------
90 coordList : `list` [`lsst.geom.SpherePoint`] or `None`
91 List of coordinates defining region of interest; if `None`, then
92 select all images subclasses may add additional keyword arguments,
93 as required.
94
95 Returns
96 -------
97 result : `pipeBase.Struct`
98 Results as a struct with attributes:
99
100 ``exposureInfoList``
101 A list of exposure information objects (subclasses of
102 BaseExposureInfo), which have at least the following fields:
103 - dataId: Data ID dictionary (`dict`).
104 - coordList: ICRS coordinates of the corners of the exposure.
105 (`list` [`lsst.geom.SpherePoint`])
106 """
107 raise NotImplementedError()
108
109
110def _extractKeyValue(dataList, keys=None):
111 """Extract the keys and values from a list of dataIds.
112
113 The input dataList is a list of objects that have 'dataId' members.
114 This allows it to be used for both a list of data references and a
115 list of ExposureInfo.
116
117 Parameters
118 ----------
119 dataList : `Unknown`
120 keys : `Unknown`
121
122 Returns
123 -------
124 keys : `Unknown`
125 values : `Unknown`
126
127 Raises
128 ------
129 RuntimeError
130 Raised if DataId keys are inconsistent.
131 """
132 assert len(dataList) > 0
133 if keys is None:
134 keys = sorted(dataList[0].dataId.keys())
135 keySet = set(keys)
136 values = list()
137 for data in dataList:
138 thisKeys = set(data.dataId.keys())
139 if thisKeys != keySet:
140 raise RuntimeError("DataId keys inconsistent: %s vs %s" % (keySet, thisKeys))
141 values.append(tuple(data.dataId[k] for k in keys))
142 return keys, values
143
144
145class SelectStruct(pipeBase.Struct):
146 """A container for data to be passed to the WcsSelectImagesTask.
147
148 Parameters
149 ----------
150 dataRef : `Unknown`
151 Data reference.
152 wcs : `lsst.afw.geom.SkyWcs`
153 Coordinate system definition (wcs).
154 bbox : `lsst.geom.box.Box2I`
155 Integer bounding box for image.
156 """
157
158 def __init__(self, dataRef, wcs, bbox):
159 super(SelectStruct, self).__init__(dataRef=dataRef, wcs=wcs, bbox=bbox)
160
161
163 """Select images using their Wcs.
164
165 We use the "convexHull" method of lsst.sphgeom.ConvexPolygon to define
166 polygons on the celestial sphere, and test the polygon of the
167 patch for overlap with the polygon of the image.
168
169 We use "convexHull" instead of generating a ConvexPolygon
170 directly because the standard for the inputs to ConvexPolygon
171 are pretty high and we don't want to be responsible for reaching them.
172 """
173
174 def run(self, wcsList, bboxList, coordList, dataIds=None, **kwargs):
175 """Return indices of provided lists that meet the selection criteria.
176
177 Parameters
178 ----------
179 wcsList : `list` [`lsst.afw.geom.SkyWcs`]
180 Specifying the WCS's of the input ccds to be selected.
181 bboxList : `list` [`lsst.geom.Box2I`]
182 Specifying the bounding boxes of the input ccds to be selected.
183 coordList : `list` [`lsst.geom.SpherePoint`]
184 ICRS coordinates specifying boundary of the patch.
185 dataIds : iterable [`lsst.daf.butler.dataId`] or `None`, optional
186 An iterable object of dataIds which point to reference catalogs.
187 **kwargs
188 Additional keyword arguments.
189
190 Returns
191 -------
192 result : `list` [`int`]
193 The indices of selected ccds.
194 """
195 if dataIds is None:
196 dataIds = [None] * len(wcsList)
197 patchVertices = [coord.getVector() for coord in coordList]
198 patchPoly = lsst.sphgeom.ConvexPolygon.convexHull(patchVertices)
199 result = []
200 for i, (imageWcs, imageBox, dataId) in enumerate(zip(wcsList, bboxList, dataIds)):
201 if imageWcs is None:
202 self.log.info("De-selecting exposure %s: Exposure has no WCS.", dataId)
203 else:
204 imageCorners = self.getValidImageCorners(imageWcs, imageBox, patchPoly, dataId)
205 if imageCorners:
206 result.append(i)
207 return result
208
209 def getValidImageCorners(self, imageWcs, imageBox, patchPoly, dataId=None):
210 """Return corners or `None` if bad.
211
212 Parameters
213 ----------
214 imageWcs : `Unknown`
215 imageBox : `Unknown`
216 patchPoly : `Unknown`
217 dataId : `Unknown`
218 """
219 try:
220 imageCorners = [imageWcs.pixelToSky(pix) for pix in geom.Box2D(imageBox).getCorners()]
221 except (pexExceptions.DomainError, pexExceptions.RuntimeError) as e:
222 # Protecting ourselves from awful Wcs solutions in input images
223 self.log.debug("WCS error in testing calexp %s (%s): deselecting", dataId, e)
224 return None
225
226 imagePoly = lsst.sphgeom.ConvexPolygon.convexHull([coord.getVector() for coord in imageCorners])
227 if imagePoly is None:
228 self.log.debug("Unable to create polygon from image %s: deselecting", dataId)
229 return None
230
231 if patchPoly.intersects(imagePoly):
232 # "intersects" also covers "contains" or "is contained by"
233 self.log.info("Selecting calexp %s", dataId)
234 return imageCorners
235
236 return None
237
238
239class PsfWcsSelectImagesConnections(pipeBase.PipelineTaskConnections,
240 dimensions=("tract", "patch", "skymap", "instrument", "visit"),
241 defaultTemplates={"coaddName": "deep"}):
242 pass
243
244
245class PsfWcsSelectImagesConfig(pipeBase.PipelineTaskConfig,
246 pipelineConnections=PsfWcsSelectImagesConnections):
247 maxEllipResidual = pexConfig.Field(
248 doc="Maximum median ellipticity residual",
249 dtype=float,
250 default=0.007,
251 optional=True,
252 )
253 maxSizeScatter = pexConfig.Field(
254 doc="Maximum scatter in the size residuals",
255 dtype=float,
256 optional=True,
257 )
258 maxScaledSizeScatter = pexConfig.Field(
259 doc="Maximum scatter in the size residuals, scaled by the median size",
260 dtype=float,
261 default=0.019,
262 optional=True,
263 )
264 maxPsfTraceRadiusDelta = pexConfig.Field(
265 doc="Maximum delta (max - min) of model PSF trace radius values evaluated on a grid on "
266 "the unmasked detector pixels (pixel).",
267 dtype=float,
268 default=0.7,
269 optional=True,
270 )
271 maxPsfApFluxDelta = pexConfig.Field(
272 doc="Maximum delta (max - min) of model PSF aperture flux (with aperture radius of "
273 "max(2, 3*psfSigma)) values evaluated on a grid on the unmasked detector pixels (based "
274 "on a normalized-to-one flux).",
275 dtype=float,
276 default=0.24,
277 optional=True,
278 )
279 maxPsfApCorrSigmaScaledDelta = pexConfig.Field(
280 doc="Maximum delta (max - min) of model PSF aperture correction values evaluated on a grid "
281 "on the unmasked detector pixels scaled (divided) by the measured model psfSigma.",
282 dtype=float,
283 default=0.22,
284 optional=True,
285 )
286 minNPsfStarPerBand = pexConfig.DictField(
287 keytype=str,
288 itemtype=int,
289 default={
290 "u": 6,
291 "g": 6,
292 "r": 6,
293 "i": 6,
294 "z": 6,
295 "y": 6,
296 "fallback": 6,
297 },
298 doc="Minimum number of PSF stars for the final PSF model to be considered "
299 "well-constrained and suitible for inclusion in the coadd. This number should "
300 "take into consideration the spatial order used for the PSF model. If the current "
301 "band for the exposure is not included as a key in this dict, the value associated "
302 "with the \"fallback\" key will be used.",
303 )
304
305 def validate(self):
306 super().validate()
307 if "fallback" not in self.minNPsfStarPerBand:
308 msg = ("Must include a \"fallback\" key in the config.minNPsfStarPerBand config dict. "
309 f"It is currenly: {self.minNPsfStarPerBand}.")
310 raise pexConfig.FieldValidationError(self.__class__.minNPsfStarPerBand, self, msg)
311
312
314 """Select images using their Wcs and cuts on the PSF properties.
315
316 The PSF quality criteria are based on the size and ellipticity
317 residuals from the adaptive second moments of the star and the PSF.
318
319 The criteria are:
320 - the median of the ellipticty residuals.
321 - the robust scatter of the size residuals (using the median absolute
322 deviation).
323 - the robust scatter of the size residuals scaled by the square of
324 the median size.
325 """
326
327 ConfigClass = PsfWcsSelectImagesConfig
328 _DefaultName = "PsfWcsSelectImages"
329
330 def run(self, wcsList, bboxList, coordList, visitSummary, dataIds=None, **kwargs):
331 """Return indices of provided lists that meet the selection criteria.
332
333 Parameters
334 ----------
335 wcsList : `list` [`lsst.afw.geom.SkyWcs`]
336 Specifying the WCS's of the input ccds to be selected.
337 bboxList : `list` [`lsst.geom.Box2I`]
338 Specifying the bounding boxes of the input ccds to be selected.
339 coordList : `list` [`lsst.geom.SpherePoint`]
340 ICRS coordinates specifying boundary of the patch.
341 visitSummary : `list` [`lsst.afw.table.ExposureCatalog`]
342 containing the PSF shape information for the input ccds to be
343 selected.
344 dataIds : iterable [`lsst.daf.butler.dataId`] or `None`, optional
345 An iterable object of dataIds which point to reference catalogs.
346 **kwargs
347 Additional keyword arguments.
348
349 Returns
350 -------
351 goodPsf : `list` [`int`]
352 The indices of selected ccds.
353 """
354 goodWcs = super(PsfWcsSelectImagesTask, self).run(wcsList=wcsList, bboxList=bboxList,
355 coordList=coordList, dataIds=dataIds)
356
357 goodPsf = []
358
359 for i, dataId in enumerate(dataIds):
360 if i not in goodWcs:
361 continue
362 if self.isValid(visitSummary, dataId["detector"]):
363 goodPsf.append(i)
364
365 return goodPsf
366
367 def isValid(self, visitSummary, detectorId):
368 """Should this ccd be selected based on its PSF shape information.
369
370 Parameters
371 ----------
372 visitSummary : `lsst.afw.table.ExposureCatalog`
373 Exposure catalog with per-detector summary information.
374 detectorId : `int`
375 Detector identifier.
376
377 Returns
378 -------
379 valid : `bool`
380 True if selected.
381 """
382 row = visitSummary.find(detectorId)
383 if row is None:
384 # This is not listed, so it must be bad.
385 self.log.warning("Removing detector %d because summary stats not available.", detectorId)
386 return False
387
388 band = row["band"]
389 if band in self.config.minNPsfStarPerBand:
390 minNPsfStar = self.config.minNPsfStarPerBand[band]
391 else:
392 minNPsfStar = self.config.minNPsfStarPerBand["fallback"]
393 nPsfStar = row["nPsfStar"]
394 medianE = np.sqrt(row["psfStarDeltaE1Median"]**2. + row["psfStarDeltaE2Median"]**2.)
395 scatterSize = row["psfStarDeltaSizeScatter"]
396 scaledScatterSize = row["psfStarScaledDeltaSizeScatter"]
397 psfTraceRadiusDelta = row["psfTraceRadiusDelta"]
398 psfApFluxDelta = row["psfApFluxDelta"]
399 psfApCorrSigmaScaledDelta = row["psfApCorrSigmaScaledDelta"]
400
401 valid = True
402 if self.config.maxEllipResidual and not (medianE <= self.config.maxEllipResidual):
403 self.log.info("Removing visit %d detector %d because median e residual too large: %f vs %f",
404 row["visit"], detectorId, medianE, self.config.maxEllipResidual)
405 valid = False
406 elif self.config.maxSizeScatter and not (scatterSize <= self.config.maxSizeScatter):
407 self.log.info("Removing visit %d detector %d because size scatter too large: %f vs %f",
408 row["visit"], detectorId, scatterSize, self.config.maxSizeScatter)
409 valid = False
410 elif self.config.maxScaledSizeScatter and not (scaledScatterSize <= self.config.maxScaledSizeScatter):
411 self.log.info("Removing visit %d detector %d because scaled size scatter too large: %f vs %f",
412 row["visit"], detectorId, scaledScatterSize, self.config.maxScaledSizeScatter)
413 valid = False
414 elif (
415 self.config.maxPsfTraceRadiusDelta is not None
416 and not (psfTraceRadiusDelta <= self.config.maxPsfTraceRadiusDelta)
417 ):
418 self.log.info(
419 "Removing visit %d detector %d because max-min delta of model PSF trace radius values "
420 "across the unmasked detector pixels is not finite or too large: %.3f vs %.3f (pixels)",
421 row["visit"], detectorId, psfTraceRadiusDelta, self.config.maxPsfTraceRadiusDelta
422 )
423 valid = False
424 elif (
425 self.config.maxPsfApFluxDelta is not None
426 and not (psfApFluxDelta <= self.config.maxPsfApFluxDelta)
427 ):
428 self.log.info(
429 "Removing visit %d detector %d because max-min delta of model PSF aperture flux values "
430 "across the unmasked detector pixels is not finite or too large: %.3f vs %.3f (pixels)",
431 row["visit"], detectorId, psfApFluxDelta, self.config.maxPsfApFluxDelta
432 )
433 valid = False
434 elif (
435 self.config.maxPsfApCorrSigmaScaledDelta is not None
436 and not (psfApCorrSigmaScaledDelta <= self.config.maxPsfApCorrSigmaScaledDelta)
437 ):
438 self.log.info(
439 "Removing visit %d detector %d because max-min delta of the model PSF apterture correction "
440 "values scaled (divided) by the psfSigma across the unmasked detector pixels is not "
441 "finite or too large: %.3f vs %.3f (fatcor)",
442 row["visit"], detectorId, psfApCorrSigmaScaledDelta, self.config.maxPsfApCorrSigmaScaledDelta
443 )
444 valid = False
445 elif minNPsfStar and (nPsfStar < minNPsfStar):
446 self.log.info(
447 "Removing visit %d detector %d because the PSF model had too few stars: %d vs %d",
448 row["visit"], detectorId, nPsfStar, minNPsfStar
449 )
450 valid = False
451
452 return valid
453
454
455class BestSeeingSelectVisitsConnections(pipeBase.PipelineTaskConnections,
456 dimensions=("tract", "patch", "skymap", "band", "instrument"),
457 defaultTemplates={"coaddName": "goodSeeing",
458 "calexpType": ""}):
459 skyMap = pipeBase.connectionTypes.Input(
460 doc="Input definition of geometry/bbox and projection/wcs for coadded exposures",
461 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
462 storageClass="SkyMap",
463 dimensions=("skymap",),
464 )
465 visitSummaries = pipeBase.connectionTypes.Input(
466 doc="Per-visit consolidated exposure metadata",
467 name="finalVisitSummary",
468 storageClass="ExposureCatalog",
469 dimensions=("instrument", "visit",),
470 multiple=True,
471 deferLoad=True
472 )
473 goodVisits = pipeBase.connectionTypes.Output(
474 doc="Selected visits to be coadded.",
475 name="{coaddName}Visits",
476 storageClass="StructuredDataDict",
477 dimensions=("instrument", "tract", "patch", "skymap", "band"),
478 )
479
480
481class BestSeeingSelectVisitsConfig(pipeBase.PipelineTaskConfig,
482 pipelineConnections=BestSeeingSelectVisitsConnections):
483 nVisitsMax = pexConfig.RangeField(
484 dtype=int,
485 doc="Maximum number of visits to select; use -1 to select all.",
486 default=12,
487 min=-1,
488 )
489 maxPsfFwhm = pexConfig.Field(
490 dtype=float,
491 doc="Maximum PSF FWHM (in arcseconds) to select",
492 default=1.5,
493 optional=True
494 )
495 minPsfFwhm = pexConfig.Field(
496 dtype=float,
497 doc="Minimum PSF FWHM (in arcseconds) to select",
498 default=0.,
499 optional=True
500 )
501 doConfirmOverlap = pexConfig.Field(
502 dtype=bool,
503 doc="Do remove visits that do not actually overlap the patch?",
504 default=True,
505 )
506 minMJD = pexConfig.Field(
507 dtype=float,
508 doc="Minimum visit MJD to select",
509 default=None,
510 optional=True
511 )
512 maxMJD = pexConfig.Field(
513 dtype=float,
514 doc="Maximum visit MJD to select",
515 default=None,
516 optional=True
517 )
518
519
520class BestSeeingSelectVisitsTask(pipeBase.PipelineTask):
521 """Select up to a maximum number of the best-seeing visits.
522
523 Don't exceed the FWHM range specified by configs min(max)PsfFwhm.
524 This Task is a port of the Gen2 image-selector used in the AP pipeline:
525 BestSeeingSelectImagesTask. This Task selects full visits based on the
526 average PSF of the entire visit.
527 """
528
529 ConfigClass = BestSeeingSelectVisitsConfig
530 _DefaultName = 'bestSeeingSelectVisits'
531
532 def runQuantum(self, butlerQC, inputRefs, outputRefs):
533 inputs = butlerQC.get(inputRefs)
534 quantumDataId = butlerQC.quantum.dataId
535 outputs = self.run(**inputs, dataId=quantumDataId)
536 butlerQC.put(outputs, outputRefs)
537
538 def run(self, visitSummaries, skyMap, dataId):
539 """Run task.
540
541 Parameters
542 ----------
543 visitSummary : `list` [`lsst.pipe.base.connections.DeferredDatasetRef`]
544 List of `lsst.pipe.base.connections.DeferredDatasetRef` of
545 visitSummary tables of type `lsst.afw.table.ExposureCatalog`.
546 skyMap : `lsst.skyMap.SkyMap`
547 SkyMap for checking visits overlap patch.
548 dataId : `dict` of dataId keys
549 For retrieving patch info for checking visits overlap patch.
550
551 Returns
552 -------
553 result : `lsst.pipe.base.Struct`
554 Results as a struct with attributes:
555
556 ``goodVisits``
557 A `dict` with selected visit ids as keys,
558 so that it can be be saved as a StructuredDataDict.
559 StructuredDataList's are currently limited.
560 """
561 if self.config.doConfirmOverlap:
562 patchPolygon = self.makePatchPolygon(skyMap, dataId)
563
564 inputVisits = [visitSummary.ref.dataId['visit'] for visitSummary in visitSummaries]
565 fwhmSizes = []
566 visits = []
567 for visit, visitSummary in zip(inputVisits, visitSummaries):
568 # read in one-by-one and only once. There may be hundreds
569 visitSummary = visitSummary.get()
570
571 # mjd is guaranteed to be the same for every detector in the
572 # visitSummary.
573 mjd = visitSummary[0].getVisitInfo().getDate().get(system=DateTime.MJD)
574
575 pixelScales = np.array([vs['pixelScale'] for vs in visitSummary if vs.getWcs()])
576 # psfSigma is PSF model determinant radius at chip center in pixels
577 psfSigmas = np.array([vs['psfSigma'] for vs in visitSummary if vs.getWcs()])
578 fwhm = np.nanmean(psfSigmas * pixelScales) * np.sqrt(8.*np.log(2.))
579
580 if self.config.maxPsfFwhm and fwhm > self.config.maxPsfFwhm:
581 continue
582 if self.config.minPsfFwhm and fwhm < self.config.minPsfFwhm:
583 continue
584 if self.config.minMJD and mjd < self.config.minMJD:
585 self.log.debug('MJD %f earlier than %.2f; rejecting', mjd, self.config.minMJD)
586 continue
587 if self.config.maxMJD and mjd > self.config.maxMJD:
588 self.log.debug('MJD %f later than %.2f; rejecting', mjd, self.config.maxMJD)
589 continue
590 if self.config.doConfirmOverlap and not self.doesIntersectPolygon(visitSummary, patchPolygon):
591 continue
592
593 fwhmSizes.append(fwhm)
594 visits.append(visit)
595
596 sortedVisits = [ind for (_, ind) in sorted(zip(fwhmSizes, visits))]
597 if self.config.nVisitsMax < 0:
598 output = sortedVisits
599 else:
600 output = sortedVisits[:self.config.nVisitsMax]
601
602 if len(output) == 0:
603 self.log.info("All images rejected in BestSeeingSelectVisitsTask.")
604 raise pipeBase.NoWorkFound(f"No good images found for {dataId}")
605 else:
606 self.log.info(
607 "%d images selected with FWHM range of %f--%f arcseconds",
608 len(output),
609 fwhmSizes[visits.index(output[0])],
610 fwhmSizes[visits.index(output[-1])],
611 )
612
613 # In order to store as a StructuredDataDict, convert list to dict
614 goodVisits = {key: True for key in output}
615 return pipeBase.Struct(goodVisits=goodVisits)
616
617 def makePatchPolygon(self, skyMap, dataId):
618 """Return True if sky polygon overlaps visit.
619
620 Parameters
621 ----------
622 skyMap : `lsst.afw.table.ExposureCatalog`
623 Exposure catalog with per-detector geometry.
624 dataId : `dict` of dataId keys
625 For retrieving patch info.
626
627 Returns
628 -------
629 result : `lsst.sphgeom.ConvexPolygon.convexHull`
630 Polygon of patch's outer bbox.
631 """
632 wcs = skyMap[dataId['tract']].getWcs()
633 bbox = skyMap[dataId['tract']][dataId['patch']].getOuterBBox()
634 sphCorners = wcs.pixelToSky(lsst.geom.Box2D(bbox).getCorners())
635 result = lsst.sphgeom.ConvexPolygon.convexHull([coord.getVector() for coord in sphCorners])
636 return result
637
638 def doesIntersectPolygon(self, visitSummary, polygon):
639 """Return True if sky polygon overlaps visit.
640
641 Parameters
642 ----------
643 visitSummary : `lsst.afw.table.ExposureCatalog`
644 Exposure catalog with per-detector geometry.
645 polygon :` lsst.sphgeom.ConvexPolygon.convexHull`
646 Polygon to check overlap.
647
648 Returns
649 -------
650 doesIntersect : `bool`
651 True if the visit overlaps the polygon.
652 """
653 doesIntersect = False
654 for detectorSummary in visitSummary:
655 if (np.all(np.isfinite(detectorSummary['raCorners']))
656 and np.all(np.isfinite(detectorSummary['decCorners']))):
657 corners = [lsst.geom.SpherePoint(ra, dec, units=lsst.geom.degrees).getVector()
658 for (ra, dec) in
659 zip(detectorSummary['raCorners'], detectorSummary['decCorners'])]
660 detectorPolygon = lsst.sphgeom.ConvexPolygon.convexHull(corners)
661 if detectorPolygon.intersects(polygon):
662 doesIntersect = True
663 break
664 return doesIntersect
665
666
667class BestSeeingQuantileSelectVisitsConfig(pipeBase.PipelineTaskConfig,
668 pipelineConnections=BestSeeingSelectVisitsConnections):
669 qMin = pexConfig.RangeField(
670 doc="Lower bound of quantile range to select. Sorts visits by seeing from narrow to wide, "
671 "and select those in the interquantile range (qMin, qMax). Set qMin to 0 for Best Seeing. "
672 "This config should be changed from zero only for exploratory diffIm testing.",
673 dtype=float,
674 default=0,
675 min=0,
676 max=1,
677 )
678 qMax = pexConfig.RangeField(
679 doc="Upper bound of quantile range to select. Sorts visits by seeing from narrow to wide, "
680 "and select those in the interquantile range (qMin, qMax). Set qMax to 1 for Worst Seeing.",
681 dtype=float,
682 default=0.33,
683 min=0,
684 max=1,
685 )
686 nVisitsMin = pexConfig.Field(
687 doc="The minimum number of visits to select, if qMin and qMax alone would have "
688 "selected fewer. In regimes with many visits, at least this number of visits will be "
689 "selected, superceding quantile when necessary. "
690 "For example, if 10 visits cover this patch, qMin=0, qMax=0.33, and nVisitsMin=5, "
691 "the best 5 visits will be selected, even though 5 > 0.33*10. "
692 "In regimes with few visits, all available visits will be selected. "
693 "For example, if 2 visits cover this patch and nVisitsMin=12, "
694 "both visits will be selected regardless of qMin and qMax.",
695 dtype=int,
696 default=6,
697 )
698 doConfirmOverlap = pexConfig.Field(
699 dtype=bool,
700 doc="Do remove visits that do not actually overlap the patch?",
701 default=True,
702 )
703 minMJD = pexConfig.Field(
704 dtype=float,
705 doc="Minimum visit MJD to select",
706 default=None,
707 optional=True
708 )
709 maxMJD = pexConfig.Field(
710 dtype=float,
711 doc="Maximum visit MJD to select",
712 default=None,
713 optional=True
714 )
715
716
717class BestSeeingQuantileSelectVisitsTask(BestSeeingSelectVisitsTask):
718 """Select a quantile of the best-seeing visits.
719
720 Selects the best (for example, third) full visits based on the average
721 PSF width in the entire visit. It can also be used for difference imaging
722 experiments that require templates with the worst seeing visits.
723 For example, selecting the worst third can be acheived by
724 changing the config parameters qMin to 0.66 and qMax to 1.
725 """
726 ConfigClass = BestSeeingQuantileSelectVisitsConfig
727 _DefaultName = 'bestSeeingQuantileSelectVisits'
728
729 def run(self, visitSummaries, skyMap, dataId):
730 if self.config.doConfirmOverlap:
731 patchPolygon = self.makePatchPolygon(skyMap, dataId)
732 visits = np.array([visitSummary.ref.dataId['visit'] for visitSummary in visitSummaries])
733 radius = np.empty(len(visits))
734 intersects = np.full(len(visits), True)
735 for i, visitSummary in enumerate(visitSummaries):
736 # read in one-by-one and only once. There may be hundreds
737 visitSummary = visitSummary.get()
738 # psfSigma is PSF model determinant radius at chip center in pixels
739 psfSigma = np.nanmedian([vs['psfSigma'] for vs in visitSummary])
740 radius[i] = psfSigma
741 if self.config.doConfirmOverlap:
742 intersects[i] = self.doesIntersectPolygon(visitSummary, patchPolygon)
743 if self.config.minMJD or self.config.maxMJD:
744 # mjd is guaranteed to be the same for every detector in the
745 # visitSummary.
746 mjd = visitSummary[0].getVisitInfo().getDate().get(system=DateTime.MJD)
747 aboveMin = mjd > self.config.minMJD if self.config.minMJD else True
748 belowMax = mjd < self.config.maxMJD if self.config.maxMJD else True
749 intersects[i] = intersects[i] and aboveMin and belowMax
750
751 sortedVisits = [v for rad, v in sorted(zip(radius[intersects], visits[intersects]))]
752 lowerBound = min(int(np.round(self.config.qMin*len(visits[intersects]))),
753 max(0, len(visits[intersects]) - self.config.nVisitsMin))
754 upperBound = max(int(np.round(self.config.qMax*len(visits[intersects]))), self.config.nVisitsMin)
755
756 # In order to store as a StructuredDataDict, convert list to dict
757 goodVisits = {int(visit): True for visit in sortedVisits[lowerBound:upperBound]}
758 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)