LSST Applications g0f08755f38+82efc23009,g12f32b3c4e+e7bdf1200e,g1653933729+a8ce1bb630,g1a0ca8cf93+50eff2b06f,g28da252d5a+52db39f6a5,g2bbee38e9b+37c5a29d61,g2bc492864f+37c5a29d61,g2cdde0e794+c05ff076ad,g3156d2b45e+41e33cbcdc,g347aa1857d+37c5a29d61,g35bb328faa+a8ce1bb630,g3a166c0a6a+37c5a29d61,g3e281a1b8c+fb992f5633,g414038480c+7f03dfc1b0,g41af890bb2+11b950c980,g5fbc88fb19+17cd334064,g6b1c1869cb+12dd639c9a,g781aacb6e4+a8ce1bb630,g80478fca09+72e9651da0,g82479be7b0+04c31367b4,g858d7b2824+82efc23009,g9125e01d80+a8ce1bb630,g9726552aa6+8047e3811d,ga5288a1d22+e532dc0a0b,gae0086650b+a8ce1bb630,gb58c049af0+d64f4d3760,gc28159a63d+37c5a29d61,gcf0d15dbbd+2acd6d4d48,gd7358e8bfb+778a810b6e,gda3e153d99+82efc23009,gda6a2b7d83+2acd6d4d48,gdaeeff99f8+1711a396fd,ge2409df99d+6b12de1076,ge79ae78c31+37c5a29d61,gf0baf85859+d0a5978c5a,gf3967379c6+4954f8c433,gfb92a5be7c+82efc23009,gfec2e1e490+2aaed99252,w.2024.46
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
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()]
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
287
288class PsfWcsSelectImagesTask(WcsSelectImagesTask):
289 """Select images using their Wcs and cuts on the PSF properties.
290
291 The PSF quality criteria are based on the size and ellipticity
292 residuals from the adaptive second moments of the star and the PSF.
293
294 The criteria are:
295 - the median of the ellipticty residuals.
296 - the robust scatter of the size residuals (using the median absolute
297 deviation).
298 - the robust scatter of the size residuals scaled by the square of
299 the median size.
300 """
301
302 ConfigClass = PsfWcsSelectImagesConfig
303 _DefaultName = "PsfWcsSelectImages"
304
305 def run(self, wcsList, bboxList, coordList, visitSummary, dataIds=None, **kwargs):
306 """Return indices of provided lists that meet the selection criteria.
307
308 Parameters
309 ----------
310 wcsList : `list` [`lsst.afw.geom.SkyWcs`]
311 Specifying the WCS's of the input ccds to be selected.
312 bboxList : `list` [`lsst.geom.Box2I`]
313 Specifying the bounding boxes of the input ccds to be selected.
314 coordList : `list` [`lsst.geom.SpherePoint`]
315 ICRS coordinates specifying boundary of the patch.
316 visitSummary : `list` [`lsst.afw.table.ExposureCatalog`]
317 containing the PSF shape information for the input ccds to be
318 selected.
319 dataIds : iterable [`lsst.daf.butler.dataId`] or `None`, optional
320 An iterable object of dataIds which point to reference catalogs.
321 **kwargs
322 Additional keyword arguments.
323
324 Returns
325 -------
326 goodPsf : `list` [`int`]
327 The indices of selected ccds.
328 """
329 goodWcs = super(PsfWcsSelectImagesTask, self).run(wcsList=wcsList, bboxList=bboxList,
330 coordList=coordList, dataIds=dataIds)
331
332 goodPsf = []
333
334 for i, dataId in enumerate(dataIds):
335 if i not in goodWcs:
336 continue
337 if self.isValid(visitSummary, dataId["detector"]):
338 goodPsf.append(i)
339
340 return goodPsf
341
342 def isValid(self, visitSummary, detectorId):
343 """Should this ccd be selected based on its PSF shape information.
344
345 Parameters
346 ----------
347 visitSummary : `lsst.afw.table.ExposureCatalog`
348 Exposure catalog with per-detector summary information.
349 detectorId : `int`
350 Detector identifier.
351
352 Returns
353 -------
354 valid : `bool`
355 True if selected.
356 """
357 row = visitSummary.find(detectorId)
358 if row is None:
359 # This is not listed, so it must be bad.
360 self.log.warning("Removing detector %d because summary stats not available.", detectorId)
361 return False
362
363 medianE = np.sqrt(row["psfStarDeltaE1Median"]**2. + row["psfStarDeltaE2Median"]**2.)
364 scatterSize = row["psfStarDeltaSizeScatter"]
365 scaledScatterSize = row["psfStarScaledDeltaSizeScatter"]
366 psfTraceRadiusDelta = row["psfTraceRadiusDelta"]
367 psfApFluxDelta = row["psfApFluxDelta"]
368 psfApCorrSigmaScaledDelta = row["psfApCorrSigmaScaledDelta"]
369
370 valid = True
371 if self.config.maxEllipResidual and not (medianE <= self.config.maxEllipResidual):
372 self.log.info("Removing visit %d detector %d because median e residual too large: %f vs %f",
373 row["visit"], detectorId, medianE, self.config.maxEllipResidual)
374 valid = False
375 elif self.config.maxSizeScatter and not (scatterSize <= self.config.maxSizeScatter):
376 self.log.info("Removing visit %d detector %d because size scatter too large: %f vs %f",
377 row["visit"], detectorId, scatterSize, self.config.maxSizeScatter)
378 valid = False
379 elif self.config.maxScaledSizeScatter and not (scaledScatterSize <= self.config.maxScaledSizeScatter):
380 self.log.info("Removing visit %d detector %d because scaled size scatter too large: %f vs %f",
381 row["visit"], detectorId, scaledScatterSize, self.config.maxScaledSizeScatter)
382 valid = False
383 elif (
384 self.config.maxPsfTraceRadiusDelta is not None
385 and not (psfTraceRadiusDelta <= self.config.maxPsfTraceRadiusDelta)
386 ):
387 self.log.info(
388 "Removing visit %d detector %d because max-min delta of model PSF trace radius values "
389 "across the unmasked detector pixels is not finite or too large: %.3f vs %.3f (pixels)",
390 row["visit"], detectorId, psfTraceRadiusDelta, self.config.maxPsfTraceRadiusDelta
391 )
392 valid = False
393 elif (
394 self.config.maxPsfApFluxDelta is not None
395 and not (psfApFluxDelta <= self.config.maxPsfApFluxDelta)
396 ):
397 self.log.info(
398 "Removing visit %d detector %d because max-min delta of model PSF aperture flux values "
399 "across the unmasked detector pixels is not finite or too large: %.3f vs %.3f (pixels)",
400 row["visit"], detectorId, psfApFluxDelta, self.config.maxPsfApFluxDelta
401 )
402 valid = False
403 elif (
404 self.config.maxPsfApCorrSigmaScaledDelta is not None
405 and not (psfApCorrSigmaScaledDelta <= self.config.maxPsfApCorrSigmaScaledDelta)
406 ):
407 self.log.info(
408 "Removing visit %d detector %d because max-min delta of the model PSF apterture correction "
409 "values scaled (divided) by the psfSigma across the unmasked detector pixels is not "
410 "finite or too large: %.3f vs %.3f (fatcor)",
411 row["visit"], detectorId, psfApCorrSigmaScaledDelta, self.config.maxPsfApCorrSigmaScaledDelta
412 )
413 valid = False
414
415 return valid
416
417
418class BestSeeingSelectVisitsConnections(pipeBase.PipelineTaskConnections,
419 dimensions=("tract", "patch", "skymap", "band", "instrument"),
420 defaultTemplates={"coaddName": "goodSeeing",
421 "calexpType": ""}):
422 skyMap = pipeBase.connectionTypes.Input(
423 doc="Input definition of geometry/bbox and projection/wcs for coadded exposures",
424 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
425 storageClass="SkyMap",
426 dimensions=("skymap",),
427 )
428 visitSummaries = pipeBase.connectionTypes.Input(
429 doc="Per-visit consolidated exposure metadata",
430 name="finalVisitSummary",
431 storageClass="ExposureCatalog",
432 dimensions=("instrument", "visit",),
433 multiple=True,
434 deferLoad=True
435 )
436 goodVisits = pipeBase.connectionTypes.Output(
437 doc="Selected visits to be coadded.",
438 name="{coaddName}Visits",
439 storageClass="StructuredDataDict",
440 dimensions=("instrument", "tract", "patch", "skymap", "band"),
441 )
442
443
444class BestSeeingSelectVisitsConfig(pipeBase.PipelineTaskConfig,
445 pipelineConnections=BestSeeingSelectVisitsConnections):
446 nVisitsMax = pexConfig.RangeField(
447 dtype=int,
448 doc="Maximum number of visits to select; use -1 to select all.",
449 default=12,
450 min=-1,
451 )
452 maxPsfFwhm = pexConfig.Field(
453 dtype=float,
454 doc="Maximum PSF FWHM (in arcseconds) to select",
455 default=1.5,
456 optional=True
457 )
458 minPsfFwhm = pexConfig.Field(
459 dtype=float,
460 doc="Minimum PSF FWHM (in arcseconds) to select",
461 default=0.,
462 optional=True
463 )
464 doConfirmOverlap = pexConfig.Field(
465 dtype=bool,
466 doc="Do remove visits that do not actually overlap the patch?",
467 default=True,
468 )
469 minMJD = pexConfig.Field(
470 dtype=float,
471 doc="Minimum visit MJD to select",
472 default=None,
473 optional=True
474 )
475 maxMJD = pexConfig.Field(
476 dtype=float,
477 doc="Maximum visit MJD to select",
478 default=None,
479 optional=True
480 )
481
482
483class BestSeeingSelectVisitsTask(pipeBase.PipelineTask):
484 """Select up to a maximum number of the best-seeing visits.
485
486 Don't exceed the FWHM range specified by configs min(max)PsfFwhm.
487 This Task is a port of the Gen2 image-selector used in the AP pipeline:
488 BestSeeingSelectImagesTask. This Task selects full visits based on the
489 average PSF of the entire visit.
490 """
491
492 ConfigClass = BestSeeingSelectVisitsConfig
493 _DefaultName = 'bestSeeingSelectVisits'
494
495 def runQuantum(self, butlerQC, inputRefs, outputRefs):
496 inputs = butlerQC.get(inputRefs)
497 quantumDataId = butlerQC.quantum.dataId
498 outputs = self.run(**inputs, dataId=quantumDataId)
499 butlerQC.put(outputs, outputRefs)
500
501 def run(self, visitSummaries, skyMap, dataId):
502 """Run task.
503
504 Parameters
505 ----------
506 visitSummary : `list` [`lsst.pipe.base.connections.DeferredDatasetRef`]
507 List of `lsst.pipe.base.connections.DeferredDatasetRef` of
508 visitSummary tables of type `lsst.afw.table.ExposureCatalog`.
509 skyMap : `lsst.skyMap.SkyMap`
510 SkyMap for checking visits overlap patch.
511 dataId : `dict` of dataId keys
512 For retrieving patch info for checking visits overlap patch.
513
514 Returns
515 -------
516 result : `lsst.pipe.base.Struct`
517 Results as a struct with attributes:
518
519 ``goodVisits``
520 A `dict` with selected visit ids as keys,
521 so that it can be be saved as a StructuredDataDict.
522 StructuredDataList's are currently limited.
523 """
524 if self.config.doConfirmOverlap:
525 patchPolygon = self.makePatchPolygon(skyMap, dataId)
526
527 inputVisits = [visitSummary.ref.dataId['visit'] for visitSummary in visitSummaries]
528 fwhmSizes = []
529 visits = []
530 for visit, visitSummary in zip(inputVisits, visitSummaries):
531 # read in one-by-one and only once. There may be hundreds
532 visitSummary = visitSummary.get()
533
534 # mjd is guaranteed to be the same for every detector in the
535 # visitSummary.
536 mjd = visitSummary[0].getVisitInfo().getDate().get(system=DateTime.MJD)
537
538 pixelScales = np.array([vs['pixelScale'] for vs in visitSummary if vs.getWcs()])
539 # psfSigma is PSF model determinant radius at chip center in pixels
540 psfSigmas = np.array([vs['psfSigma'] for vs in visitSummary if vs.getWcs()])
541 fwhm = np.nanmean(psfSigmas * pixelScales) * np.sqrt(8.*np.log(2.))
542
543 if self.config.maxPsfFwhm and fwhm > self.config.maxPsfFwhm:
544 continue
545 if self.config.minPsfFwhm and fwhm < self.config.minPsfFwhm:
546 continue
547 if self.config.minMJD and mjd < self.config.minMJD:
548 self.log.debug('MJD %f earlier than %.2f; rejecting', mjd, self.config.minMJD)
549 continue
550 if self.config.maxMJD and mjd > self.config.maxMJD:
551 self.log.debug('MJD %f later than %.2f; rejecting', mjd, self.config.maxMJD)
552 continue
553 if self.config.doConfirmOverlap and not self.doesIntersectPolygon(visitSummary, patchPolygon):
554 continue
555
556 fwhmSizes.append(fwhm)
557 visits.append(visit)
558
559 sortedVisits = [ind for (_, ind) in sorted(zip(fwhmSizes, visits))]
560 if self.config.nVisitsMax < 0:
561 output = sortedVisits
562 else:
563 output = sortedVisits[:self.config.nVisitsMax]
564
565 if len(output) == 0:
566 self.log.info("All images rejected in BestSeeingSelectVisitsTask.")
567 raise pipeBase.NoWorkFound(f"No good images found for {dataId}")
568 else:
569 self.log.info(
570 "%d images selected with FWHM range of %f--%f arcseconds",
571 len(output),
572 fwhmSizes[visits.index(output[0])],
573 fwhmSizes[visits.index(output[-1])],
574 )
575
576 # In order to store as a StructuredDataDict, convert list to dict
577 goodVisits = {key: True for key in output}
578 return pipeBase.Struct(goodVisits=goodVisits)
579
580 def makePatchPolygon(self, skyMap, dataId):
581 """Return True if sky polygon overlaps visit.
582
583 Parameters
584 ----------
585 skyMap : `lsst.afw.table.ExposureCatalog`
586 Exposure catalog with per-detector geometry.
587 dataId : `dict` of dataId keys
588 For retrieving patch info.
589
590 Returns
591 -------
592 result : `lsst.sphgeom.ConvexPolygon.convexHull`
593 Polygon of patch's outer bbox.
594 """
595 wcs = skyMap[dataId['tract']].getWcs()
596 bbox = skyMap[dataId['tract']][dataId['patch']].getOuterBBox()
597 sphCorners = wcs.pixelToSky(lsst.geom.Box2D(bbox).getCorners())
598 result = lsst.sphgeom.ConvexPolygon.convexHull([coord.getVector() for coord in sphCorners])
599 return result
600
601 def doesIntersectPolygon(self, visitSummary, polygon):
602 """Return True if sky polygon overlaps visit.
603
604 Parameters
605 ----------
606 visitSummary : `lsst.afw.table.ExposureCatalog`
607 Exposure catalog with per-detector geometry.
608 polygon :` lsst.sphgeom.ConvexPolygon.convexHull`
609 Polygon to check overlap.
610
611 Returns
612 -------
613 doesIntersect : `bool`
614 True if the visit overlaps the polygon.
615 """
616 doesIntersect = False
617 for detectorSummary in visitSummary:
618 if (np.all(np.isfinite(detectorSummary['raCorners']))
619 and np.all(np.isfinite(detectorSummary['decCorners']))):
620 corners = [lsst.geom.SpherePoint(ra, dec, units=lsst.geom.degrees).getVector()
621 for (ra, dec) in
622 zip(detectorSummary['raCorners'], detectorSummary['decCorners'])]
623 detectorPolygon = lsst.sphgeom.ConvexPolygon.convexHull(corners)
624 if detectorPolygon.intersects(polygon):
625 doesIntersect = True
626 break
627 return doesIntersect
628
629
630class BestSeeingQuantileSelectVisitsConfig(pipeBase.PipelineTaskConfig,
631 pipelineConnections=BestSeeingSelectVisitsConnections):
632 qMin = pexConfig.RangeField(
633 doc="Lower bound of quantile range to select. Sorts visits by seeing from narrow to wide, "
634 "and select those in the interquantile range (qMin, qMax). Set qMin to 0 for Best Seeing. "
635 "This config should be changed from zero only for exploratory diffIm testing.",
636 dtype=float,
637 default=0,
638 min=0,
639 max=1,
640 )
641 qMax = pexConfig.RangeField(
642 doc="Upper bound of quantile range to select. Sorts visits by seeing from narrow to wide, "
643 "and select those in the interquantile range (qMin, qMax). Set qMax to 1 for Worst Seeing.",
644 dtype=float,
645 default=0.33,
646 min=0,
647 max=1,
648 )
649 nVisitsMin = pexConfig.Field(
650 doc="The minimum number of visits to select, if qMin and qMax alone would have "
651 "selected fewer. In regimes with many visits, at least this number of visits will be "
652 "selected, superceding quantile when necessary. "
653 "For example, if 10 visits cover this patch, qMin=0, qMax=0.33, and nVisitsMin=5, "
654 "the best 5 visits will be selected, even though 5 > 0.33*10. "
655 "In regimes with few visits, all available visits will be selected. "
656 "For example, if 2 visits cover this patch and nVisitsMin=12, "
657 "both visits will be selected regardless of qMin and qMax.",
658 dtype=int,
659 default=6,
660 )
661 doConfirmOverlap = pexConfig.Field(
662 dtype=bool,
663 doc="Do remove visits that do not actually overlap the patch?",
664 default=True,
665 )
666 minMJD = pexConfig.Field(
667 dtype=float,
668 doc="Minimum visit MJD to select",
669 default=None,
670 optional=True
671 )
672 maxMJD = pexConfig.Field(
673 dtype=float,
674 doc="Maximum visit MJD to select",
675 default=None,
676 optional=True
677 )
678
679
680class BestSeeingQuantileSelectVisitsTask(BestSeeingSelectVisitsTask):
681 """Select a quantile of the best-seeing visits.
682
683 Selects the best (for example, third) full visits based on the average
684 PSF width in the entire visit. It can also be used for difference imaging
685 experiments that require templates with the worst seeing visits.
686 For example, selecting the worst third can be acheived by
687 changing the config parameters qMin to 0.66 and qMax to 1.
688 """
689 ConfigClass = BestSeeingQuantileSelectVisitsConfig
690 _DefaultName = 'bestSeeingQuantileSelectVisits'
691
692 def run(self, visitSummaries, skyMap, dataId):
693 if self.config.doConfirmOverlap:
694 patchPolygon = self.makePatchPolygon(skyMap, dataId)
695 visits = np.array([visitSummary.ref.dataId['visit'] for visitSummary in visitSummaries])
696 radius = np.empty(len(visits))
697 intersects = np.full(len(visits), True)
698 for i, visitSummary in enumerate(visitSummaries):
699 # read in one-by-one and only once. There may be hundreds
700 visitSummary = visitSummary.get()
701 # psfSigma is PSF model determinant radius at chip center in pixels
702 psfSigma = np.nanmedian([vs['psfSigma'] for vs in visitSummary])
703 radius[i] = psfSigma
704 if self.config.doConfirmOverlap:
705 intersects[i] = self.doesIntersectPolygon(visitSummary, patchPolygon)
706 if self.config.minMJD or self.config.maxMJD:
707 # mjd is guaranteed to be the same for every detector in the
708 # visitSummary.
709 mjd = visitSummary[0].getVisitInfo().getDate().get(system=DateTime.MJD)
710 aboveMin = mjd > self.config.minMJD if self.config.minMJD else True
711 belowMax = mjd < self.config.maxMJD if self.config.maxMJD else True
712 intersects[i] = intersects[i] and aboveMin and belowMax
713
714 sortedVisits = [v for rad, v in sorted(zip(radius[intersects], visits[intersects]))]
715 lowerBound = min(int(np.round(self.config.qMin*len(visits[intersects]))),
716 max(0, len(visits[intersects]) - self.config.nVisitsMin))
717 upperBound = max(int(np.round(self.config.qMax*len(visits[intersects]))), self.config.nVisitsMin)
718
719 # In order to store as a StructuredDataDict, convert list to dict
720 goodVisits = {int(visit): True for visit in sortedVisits[lowerBound:upperBound]}
721 return pipeBase.Struct(goodVisits=goodVisits)
int min
int max
A floating-point coordinate rectangle geometry.
Definition Box.h:413
Point in an unspecified spherical coordinate system.
Definition SpherePoint.h:57
Reports arguments outside the domain of an operation.
Definition Runtime.h:57
Reports errors that are due to events beyond the control of the program.
Definition Runtime.h:104
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...
bool isValid
Definition fits.cc:404
_extractKeyValue(dataList, keys=None)