Coverage for python/lsst/pipe/tasks/selectImages.py: 24%

209 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-01-07 02:56 -0800

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.utils as utils 

29import lsst.pex.config as pexConfig 

30import lsst.pex.exceptions as pexExceptions 

31import lsst.geom as geom 

32import lsst.pipe.base as pipeBase 

33from lsst.skymap import BaseSkyMap 

34from lsst.daf.base import DateTime 

35from lsst.utils.timer import timeMethod 

36 

37 

38class DatabaseSelectImagesConfig(pexConfig.Config): 

39 """Base configuration for subclasses of BaseSelectImagesTask that use a 

40 database. 

41 """ 

42 

43 host = pexConfig.Field( 

44 doc="Database server host name", 

45 dtype=str, 

46 ) 

47 port = pexConfig.Field( 

48 doc="Database server port", 

49 dtype=int, 

50 ) 

51 database = pexConfig.Field( 

52 doc="Name of database", 

53 dtype=str, 

54 ) 

55 maxExposures = pexConfig.Field( 

56 doc="maximum exposures to select; intended for debugging; ignored if None", 

57 dtype=int, 

58 optional=True, 

59 ) 

60 

61 

62class BaseExposureInfo(pipeBase.Struct): 

63 """Data about a selected exposure. 

64 

65 Parameters 

66 ---------- 

67 dataId : `dict` 

68 Data ID keys of exposure. 

69 coordList : `list` [`lsst.afw.geom.SpherePoint`] 

70 ICRS coordinates of the corners of the exposure 

71 plus any others items that are desired. 

72 """ 

73 

74 def __init__(self, dataId, coordList): 

75 super(BaseExposureInfo, self).__init__(dataId=dataId, coordList=coordList) 

76 

77 

78class BaseSelectImagesTask(pipeBase.Task): 

79 """Base task for selecting images suitable for coaddition. 

80 """ 

81 

82 ConfigClass = pexConfig.Config 

83 _DefaultName = "selectImages" 

84 

85 @timeMethod 

86 def run(self, coordList): 

87 """Select images suitable for coaddition in a particular region. 

88 

89 Parameters 

90 ---------- 

91 coordList : `list` [`lsst.geom.SpherePoint`] or `None` 

92 List of coordinates defining region of interest; if `None`, then 

93 select all images subclasses may add additional keyword arguments, 

94 as required. 

95 

96 Returns 

97 ------- 

98 result : `pipeBase.Struct` 

99 Results as a struct with attributes: 

100 

101 ``exposureInfoList`` 

102 A list of exposure information objects (subclasses of 

103 BaseExposureInfo), which have at least the following fields: 

104 - dataId: Data ID dictionary (`dict`). 

105 - coordList: ICRS coordinates of the corners of the exposure. 

106 (`list` [`lsst.geom.SpherePoint`]) 

107 """ 

108 raise NotImplementedError() 

109 

110 

111def _extractKeyValue(dataList, keys=None): 

112 """Extract the keys and values from a list of dataIds. 

113 

114 The input dataList is a list of objects that have 'dataId' members. 

115 This allows it to be used for both a list of data references and a 

116 list of ExposureInfo. 

117 

118 Parameters 

119 ---------- 

120 dataList : `Unknown` 

121 keys : `Unknown` 

122 

123 Returns 

124 ------- 

125 keys : `Unknown` 

126 values : `Unknown` 

127 

128 Raises 

129 ------ 

130 RuntimeError 

131 Raised if DataId keys are inconsistent. 

132 """ 

133 assert len(dataList) > 0 

134 if keys is None: 

135 keys = sorted(dataList[0].dataId.keys()) 

136 keySet = set(keys) 

137 values = list() 

138 for data in dataList: 

139 thisKeys = set(data.dataId.keys()) 

140 if thisKeys != keySet: 

141 raise RuntimeError("DataId keys inconsistent: %s vs %s" % (keySet, thisKeys)) 

142 values.append(tuple(data.dataId[k] for k in keys)) 

143 return keys, values 

144 

145 

146class SelectStruct(pipeBase.Struct): 

147 """A container for data to be passed to the WcsSelectImagesTask. 

148 

149 Parameters 

150 ---------- 

151 dataRef : `Unknown` 

152 Data reference. 

153 wcs : `lsst.afw.geom.SkyWcs` 

154 Coordinate system definition (wcs). 

155 bbox : `lsst.geom.box.Box2I` 

156 Integer bounding box for image. 

157 """ 

158 

159 def __init__(self, dataRef, wcs, bbox): 

160 super(SelectStruct, self).__init__(dataRef=dataRef, wcs=wcs, bbox=bbox) 

161 

162 

163class WcsSelectImagesTask(BaseSelectImagesTask): 

164 """Select images using their Wcs. 

165 

166 We use the "convexHull" method of lsst.sphgeom.ConvexPolygon to define 

167 polygons on the celestial sphere, and test the polygon of the 

168 patch for overlap with the polygon of the image. 

169 

170 We use "convexHull" instead of generating a ConvexPolygon 

171 directly because the standard for the inputs to ConvexPolygon 

172 are pretty high and we don't want to be responsible for reaching them. 

173 """ 

174 

175 def run(self, wcsList, bboxList, coordList, dataIds=None, **kwargs): 

176 """Return indices of provided lists that meet the selection criteria. 

177 

178 Parameters 

179 ---------- 

180 wcsList : `list` [`lsst.afw.geom.SkyWcs`] 

181 Specifying the WCS's of the input ccds to be selected. 

182 bboxList : `list` [`lsst.geom.Box2I`] 

183 Specifying the bounding boxes of the input ccds to be selected. 

184 coordList : `list` [`lsst.geom.SpherePoint`] 

185 ICRS coordinates specifying boundary of the patch. 

186 dataIds : iterable [`lsst.daf.butler.dataId`] or `None`, optional 

187 An iterable object of dataIds which point to reference catalogs. 

188 **kwargs 

189 Additional keyword arguments. 

190 

191 Returns 

192 ------- 

193 result : `list` [`int`] 

194 The indices of selected ccds. 

195 """ 

196 if dataIds is None: 

197 dataIds = [None] * len(wcsList) 

198 patchVertices = [coord.getVector() for coord in coordList] 

199 patchPoly = lsst.sphgeom.ConvexPolygon.convexHull(patchVertices) 

200 result = [] 

201 for i, (imageWcs, imageBox, dataId) in enumerate(zip(wcsList, bboxList, dataIds)): 

202 if imageWcs is None: 

203 self.log.info("De-selecting exposure %s: Exposure has no WCS.", dataId) 

204 else: 

205 imageCorners = self.getValidImageCorners(imageWcs, imageBox, patchPoly, dataId) 

206 if imageCorners: 

207 result.append(i) 

208 return result 

209 

210 def getValidImageCorners(self, imageWcs, imageBox, patchPoly, dataId=None): 

211 """Return corners or `None` if bad. 

212 

213 Parameters 

214 ---------- 

215 imageWcs : `Unknown` 

216 imageBox : `Unknown` 

217 patchPoly : `Unknown` 

218 dataId : `Unknown` 

219 """ 

220 try: 

221 imageCorners = [imageWcs.pixelToSky(pix) for pix in geom.Box2D(imageBox).getCorners()] 

222 except (pexExceptions.DomainError, pexExceptions.RuntimeError) as e: 

223 # Protecting ourselves from awful Wcs solutions in input images 

224 self.log.debug("WCS error in testing calexp %s (%s): deselecting", dataId, e) 

225 return None 

226 

227 imagePoly = lsst.sphgeom.ConvexPolygon.convexHull([coord.getVector() for coord in imageCorners]) 

228 if imagePoly is None: 

229 self.log.debug("Unable to create polygon from image %s: deselecting", dataId) 

230 return None 

231 

232 if patchPoly.intersects(imagePoly): 

233 # "intersects" also covers "contains" or "is contained by" 

234 self.log.info("Selecting calexp %s", dataId) 

235 return imageCorners 

236 

237 return None 

238 

239 

240class PsfWcsSelectImagesConnections(pipeBase.PipelineTaskConnections, 

241 dimensions=("tract", "patch", "skymap", "instrument", "visit"), 

242 defaultTemplates={"coaddName": "deep"}): 

243 pass 

244 

245 

246class PsfWcsSelectImagesConfig(pipeBase.PipelineTaskConfig, 

247 pipelineConnections=PsfWcsSelectImagesConnections): 

248 maxEllipResidual = pexConfig.Field( 

249 doc="Maximum median ellipticity residual", 

250 dtype=float, 

251 default=0.007, 

252 optional=True, 

253 ) 

254 maxSizeScatter = pexConfig.Field( 

255 doc="Maximum scatter in the size residuals", 

256 dtype=float, 

257 optional=True, 

258 ) 

259 maxScaledSizeScatter = pexConfig.Field( 

260 doc="Maximum scatter in the size residuals, scaled by the median size", 

261 dtype=float, 

262 default=0.009, 

263 optional=True, 

264 ) 

265 maxPsfTraceRadiusDelta = pexConfig.Field( 

266 doc="Maximum delta (max - min) of model PSF trace radius values evaluated on a grid on " 

267 "the unmasked detector pixels (pixel).", 

268 dtype=float, 

269 default=0.7, 

270 optional=True, 

271 ) 

272 

273 

274class PsfWcsSelectImagesTask(WcsSelectImagesTask): 

275 """Select images using their Wcs and cuts on the PSF properties. 

276 

277 The PSF quality criteria are based on the size and ellipticity 

278 residuals from the adaptive second moments of the star and the PSF. 

279 

280 The criteria are: 

281 - the median of the ellipticty residuals. 

282 - the robust scatter of the size residuals (using the median absolute 

283 deviation). 

284 - the robust scatter of the size residuals scaled by the square of 

285 the median size. 

286 """ 

287 

288 ConfigClass = PsfWcsSelectImagesConfig 

289 _DefaultName = "PsfWcsSelectImages" 

290 

291 def run(self, wcsList, bboxList, coordList, visitSummary, dataIds=None, **kwargs): 

292 """Return indices of provided lists that meet the selection criteria. 

293 

294 Parameters 

295 ---------- 

296 wcsList : `list` [`lsst.afw.geom.SkyWcs`] 

297 Specifying the WCS's of the input ccds to be selected. 

298 bboxList : `list` [`lsst.geom.Box2I`] 

299 Specifying the bounding boxes of the input ccds to be selected. 

300 coordList : `list` [`lsst.geom.SpherePoint`] 

301 ICRS coordinates specifying boundary of the patch. 

302 visitSummary : `list` [`lsst.afw.table.ExposureCatalog`] 

303 containing the PSF shape information for the input ccds to be 

304 selected. 

305 dataIds : iterable [`lsst.daf.butler.dataId`] or `None`, optional 

306 An iterable object of dataIds which point to reference catalogs. 

307 **kwargs 

308 Additional keyword arguments. 

309 

310 Returns 

311 ------- 

312 goodPsf : `list` [`int`] 

313 The indices of selected ccds. 

314 """ 

315 goodWcs = super(PsfWcsSelectImagesTask, self).run(wcsList=wcsList, bboxList=bboxList, 

316 coordList=coordList, dataIds=dataIds) 

317 

318 goodPsf = [] 

319 

320 for i, dataId in enumerate(dataIds): 

321 if i not in goodWcs: 

322 continue 

323 if self.isValid(visitSummary, dataId["detector"]): 

324 goodPsf.append(i) 

325 

326 return goodPsf 

327 

328 def isValid(self, visitSummary, detectorId): 

329 """Should this ccd be selected based on its PSF shape information. 

330 

331 Parameters 

332 ---------- 

333 visitSummary : `lsst.afw.table.ExposureCatalog` 

334 Exposure catalog with per-detector summary information. 

335 detectorId : `int` 

336 Detector identifier. 

337 

338 Returns 

339 ------- 

340 valid : `bool` 

341 True if selected. 

342 """ 

343 row = visitSummary.find(detectorId) 

344 if row is None: 

345 # This is not listed, so it must be bad. 

346 self.log.warning("Removing detector %d because summary stats not available.", detectorId) 

347 return False 

348 

349 medianE = np.sqrt(row["psfStarDeltaE1Median"]**2. + row["psfStarDeltaE2Median"]**2.) 

350 scatterSize = row["psfStarDeltaSizeScatter"] 

351 scaledScatterSize = row["psfStarScaledDeltaSizeScatter"] 

352 psfTraceRadiusDelta = row["psfTraceRadiusDelta"] 

353 

354 valid = True 

355 if self.config.maxEllipResidual and medianE > self.config.maxEllipResidual: 

356 self.log.info("Removing visit %d detector %d because median e residual too large: %f vs %f", 

357 row["visit"], detectorId, medianE, self.config.maxEllipResidual) 

358 valid = False 

359 elif self.config.maxSizeScatter and scatterSize > self.config.maxSizeScatter: 

360 self.log.info("Removing visit %d detector %d because size scatter too large: %f vs %f", 

361 row["visit"], detectorId, scatterSize, self.config.maxSizeScatter) 

362 valid = False 

363 elif self.config.maxScaledSizeScatter and scaledScatterSize > self.config.maxScaledSizeScatter: 

364 self.log.info("Removing visit %d detector %d because scaled size scatter too large: %f vs %f", 

365 row["visit"], detectorId, scaledScatterSize, self.config.maxScaledSizeScatter) 

366 valid = False 

367 elif ( 

368 self.config.maxPsfTraceRadiusDelta 

369 and ( 

370 psfTraceRadiusDelta > self.config.maxPsfTraceRadiusDelta 

371 or ~np.isfinite(psfTraceRadiusDelta) 

372 ) 

373 ): 

374 self.log.info( 

375 "Removing visit %d detector %d because max-min delta of model PSF trace radius values " 

376 "across the unmasked detector pixels is not finite or too large: %.3f vs %.3f (pixels)", 

377 row["visit"], detectorId, psfTraceRadiusDelta, self.config.maxPsfTraceRadiusDelta 

378 ) 

379 valid = False 

380 

381 return valid 

382 

383 

384class BestSeeingSelectVisitsConnections(pipeBase.PipelineTaskConnections, 

385 dimensions=("tract", "patch", "skymap", "band", "instrument"), 

386 defaultTemplates={"coaddName": "goodSeeing"}): 

387 skyMap = pipeBase.connectionTypes.Input( 

388 doc="Input definition of geometry/bbox and projection/wcs for coadded exposures", 

389 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME, 

390 storageClass="SkyMap", 

391 dimensions=("skymap",), 

392 ) 

393 visitSummaries = pipeBase.connectionTypes.Input( 

394 doc="Per-visit consolidated exposure metadata from ConsolidateVisitSummaryTask", 

395 name="visitSummary", 

396 storageClass="ExposureCatalog", 

397 dimensions=("instrument", "visit",), 

398 multiple=True, 

399 deferLoad=True 

400 ) 

401 goodVisits = pipeBase.connectionTypes.Output( 

402 doc="Selected visits to be coadded.", 

403 name="{coaddName}Visits", 

404 storageClass="StructuredDataDict", 

405 dimensions=("instrument", "tract", "patch", "skymap", "band"), 

406 ) 

407 

408 

409class BestSeeingSelectVisitsConfig(pipeBase.PipelineTaskConfig, 

410 pipelineConnections=BestSeeingSelectVisitsConnections): 

411 nVisitsMax = pexConfig.RangeField( 

412 dtype=int, 

413 doc="Maximum number of visits to select", 

414 default=12, 

415 min=0 

416 ) 

417 maxPsfFwhm = pexConfig.Field( 

418 dtype=float, 

419 doc="Maximum PSF FWHM (in arcseconds) to select", 

420 default=1.5, 

421 optional=True 

422 ) 

423 minPsfFwhm = pexConfig.Field( 

424 dtype=float, 

425 doc="Minimum PSF FWHM (in arcseconds) to select", 

426 default=0., 

427 optional=True 

428 ) 

429 doConfirmOverlap = pexConfig.Field( 

430 dtype=bool, 

431 doc="Do remove visits that do not actually overlap the patch?", 

432 default=True, 

433 ) 

434 minMJD = pexConfig.Field( 

435 dtype=float, 

436 doc="Minimum visit MJD to select", 

437 default=None, 

438 optional=True 

439 ) 

440 maxMJD = pexConfig.Field( 

441 dtype=float, 

442 doc="Maximum visit MJD to select", 

443 default=None, 

444 optional=True 

445 ) 

446 

447 

448class BestSeeingSelectVisitsTask(pipeBase.PipelineTask): 

449 """Select up to a maximum number of the best-seeing visits. 

450 

451 Don't exceed the FWHM range specified by configs min(max)PsfFwhm. 

452 This Task is a port of the Gen2 image-selector used in the AP pipeline: 

453 BestSeeingSelectImagesTask. This Task selects full visits based on the 

454 average PSF of the entire visit. 

455 """ 

456 

457 ConfigClass = BestSeeingSelectVisitsConfig 

458 _DefaultName = 'bestSeeingSelectVisits' 

459 

460 def runQuantum(self, butlerQC, inputRefs, outputRefs): 

461 inputs = butlerQC.get(inputRefs) 

462 quantumDataId = butlerQC.quantum.dataId 

463 outputs = self.run(**inputs, dataId=quantumDataId) 

464 butlerQC.put(outputs, outputRefs) 

465 

466 def run(self, visitSummaries, skyMap, dataId): 

467 """Run task. 

468 

469 Parameters 

470 ---------- 

471 visitSummary : `list` [`lsst.pipe.base.connections.DeferredDatasetRef`] 

472 List of `lsst.pipe.base.connections.DeferredDatasetRef` of 

473 visitSummary tables of type `lsst.afw.table.ExposureCatalog`. 

474 skyMap : `lsst.skyMap.SkyMap` 

475 SkyMap for checking visits overlap patch. 

476 dataId : `dict` of dataId keys 

477 For retrieving patch info for checking visits overlap patch. 

478 

479 Returns 

480 ------- 

481 result : `lsst.pipe.base.Struct` 

482 Results as a struct with attributes: 

483 

484 ``goodVisits`` 

485 A `dict` with selected visit ids as keys, 

486 so that it can be be saved as a StructuredDataDict. 

487 StructuredDataList's are currently limited. 

488 """ 

489 if self.config.doConfirmOverlap: 

490 patchPolygon = self.makePatchPolygon(skyMap, dataId) 

491 

492 inputVisits = [visitSummary.ref.dataId['visit'] for visitSummary in visitSummaries] 

493 fwhmSizes = [] 

494 visits = [] 

495 for visit, visitSummary in zip(inputVisits, visitSummaries): 

496 # read in one-by-one and only once. There may be hundreds 

497 visitSummary = visitSummary.get() 

498 

499 # mjd is guaranteed to be the same for every detector in the 

500 # visitSummary. 

501 mjd = visitSummary[0].getVisitInfo().getDate().get(system=DateTime.MJD) 

502 

503 pixToArcseconds = [vs.getWcs().getPixelScale(vs.getBBox().getCenter()).asArcseconds() 

504 for vs in visitSummary if vs.getWcs()] 

505 # psfSigma is PSF model determinant radius at chip center in pixels 

506 psfSigmas = np.array([vs['psfSigma'] for vs in visitSummary if vs.getWcs()]) 

507 fwhm = np.nanmean(psfSigmas * pixToArcseconds) * np.sqrt(8.*np.log(2.)) 

508 

509 if self.config.maxPsfFwhm and fwhm > self.config.maxPsfFwhm: 

510 continue 

511 if self.config.minPsfFwhm and fwhm < self.config.minPsfFwhm: 

512 continue 

513 if self.config.minMJD and mjd < self.config.minMJD: 

514 self.log.debug('MJD %f earlier than %.2f; rejecting', mjd, self.config.minMJD) 

515 continue 

516 if self.config.maxMJD and mjd > self.config.maxMJD: 

517 self.log.debug('MJD %f later than %.2f; rejecting', mjd, self.config.maxMJD) 

518 continue 

519 if self.config.doConfirmOverlap and not self.doesIntersectPolygon(visitSummary, patchPolygon): 

520 continue 

521 

522 fwhmSizes.append(fwhm) 

523 visits.append(visit) 

524 

525 sortedVisits = [ind for (_, ind) in sorted(zip(fwhmSizes, visits))] 

526 output = sortedVisits[:self.config.nVisitsMax] 

527 self.log.info("%d images selected with FWHM range of %d--%d arcseconds", 

528 len(output), fwhmSizes[visits.index(output[0])], fwhmSizes[visits.index(output[-1])]) 

529 

530 # In order to store as a StructuredDataDict, convert list to dict 

531 goodVisits = {key: True for key in output} 

532 return pipeBase.Struct(goodVisits=goodVisits) 

533 

534 def makePatchPolygon(self, skyMap, dataId): 

535 """Return True if sky polygon overlaps visit. 

536 

537 Parameters 

538 ---------- 

539 skyMap : `lsst.afw.table.ExposureCatalog` 

540 Exposure catalog with per-detector geometry. 

541 dataId : `dict` of dataId keys 

542 For retrieving patch info. 

543 

544 Returns 

545 ------- 

546 result : `lsst.sphgeom.ConvexPolygon.convexHull` 

547 Polygon of patch's outer bbox. 

548 """ 

549 wcs = skyMap[dataId['tract']].getWcs() 

550 bbox = skyMap[dataId['tract']][dataId['patch']].getOuterBBox() 

551 sphCorners = wcs.pixelToSky(lsst.geom.Box2D(bbox).getCorners()) 

552 result = lsst.sphgeom.ConvexPolygon.convexHull([coord.getVector() for coord in sphCorners]) 

553 return result 

554 

555 def doesIntersectPolygon(self, visitSummary, polygon): 

556 """Return True if sky polygon overlaps visit. 

557 

558 Parameters 

559 ---------- 

560 visitSummary : `lsst.afw.table.ExposureCatalog` 

561 Exposure catalog with per-detector geometry. 

562 polygon :` lsst.sphgeom.ConvexPolygon.convexHull` 

563 Polygon to check overlap. 

564 

565 Returns 

566 ------- 

567 doesIntersect : `bool` 

568 True if the visit overlaps the polygon. 

569 """ 

570 doesIntersect = False 

571 for detectorSummary in visitSummary: 

572 if (np.all(np.isfinite(detectorSummary['raCorners'])) 

573 and np.all(np.isfinite(detectorSummary['decCorners']))): 

574 corners = [lsst.geom.SpherePoint(ra, decl, units=lsst.geom.degrees).getVector() 

575 for (ra, decl) in 

576 zip(detectorSummary['raCorners'], detectorSummary['decCorners'])] 

577 detectorPolygon = lsst.sphgeom.ConvexPolygon.convexHull(corners) 

578 if detectorPolygon.intersects(polygon): 

579 doesIntersect = True 

580 break 

581 return doesIntersect 

582 

583 

584class BestSeeingQuantileSelectVisitsConfig(pipeBase.PipelineTaskConfig, 

585 pipelineConnections=BestSeeingSelectVisitsConnections): 

586 qMin = pexConfig.RangeField( 

587 doc="Lower bound of quantile range to select. Sorts visits by seeing from narrow to wide, " 

588 "and select those in the interquantile range (qMin, qMax). Set qMin to 0 for Best Seeing. " 

589 "This config should be changed from zero only for exploratory diffIm testing.", 

590 dtype=float, 

591 default=0, 

592 min=0, 

593 max=1, 

594 ) 

595 qMax = pexConfig.RangeField( 

596 doc="Upper bound of quantile range to select. Sorts visits by seeing from narrow to wide, " 

597 "and select those in the interquantile range (qMin, qMax). Set qMax to 1 for Worst Seeing.", 

598 dtype=float, 

599 default=0.33, 

600 min=0, 

601 max=1, 

602 ) 

603 nVisitsMin = pexConfig.Field( 

604 doc="At least this number of visits selected and supercedes quantile. For example, if 10 visits " 

605 "cover this patch, qMin=0.33, and nVisitsMin=5, the best 5 visits will be selected.", 

606 dtype=int, 

607 default=6, 

608 ) 

609 doConfirmOverlap = pexConfig.Field( 

610 dtype=bool, 

611 doc="Do remove visits that do not actually overlap the patch?", 

612 default=True, 

613 ) 

614 minMJD = pexConfig.Field( 

615 dtype=float, 

616 doc="Minimum visit MJD to select", 

617 default=None, 

618 optional=True 

619 ) 

620 maxMJD = pexConfig.Field( 

621 dtype=float, 

622 doc="Maximum visit MJD to select", 

623 default=None, 

624 optional=True 

625 ) 

626 

627 

628class BestSeeingQuantileSelectVisitsTask(BestSeeingSelectVisitsTask): 

629 """Select a quantile of the best-seeing visits. 

630 

631 Selects the best (for example, third) full visits based on the average 

632 PSF width in the entire visit. It can also be used for difference imaging 

633 experiments that require templates with the worst seeing visits. 

634 For example, selecting the worst third can be acheived by 

635 changing the config parameters qMin to 0.66 and qMax to 1. 

636 """ 

637 ConfigClass = BestSeeingQuantileSelectVisitsConfig 

638 _DefaultName = 'bestSeeingQuantileSelectVisits' 

639 

640 @utils.inheritDoc(BestSeeingSelectVisitsTask) 

641 def run(self, visitSummaries, skyMap, dataId): 

642 if self.config.doConfirmOverlap: 

643 patchPolygon = self.makePatchPolygon(skyMap, dataId) 

644 visits = np.array([visitSummary.ref.dataId['visit'] for visitSummary in visitSummaries]) 

645 radius = np.empty(len(visits)) 

646 intersects = np.full(len(visits), True) 

647 for i, visitSummary in enumerate(visitSummaries): 

648 # read in one-by-one and only once. There may be hundreds 

649 visitSummary = visitSummary.get() 

650 # psfSigma is PSF model determinant radius at chip center in pixels 

651 psfSigma = np.nanmedian([vs['psfSigma'] for vs in visitSummary]) 

652 radius[i] = psfSigma 

653 if self.config.doConfirmOverlap: 

654 intersects[i] = self.doesIntersectPolygon(visitSummary, patchPolygon) 

655 if self.config.minMJD or self.config.maxMJD: 

656 # mjd is guaranteed to be the same for every detector in the 

657 # visitSummary. 

658 mjd = visitSummary[0].getVisitInfo().getDate().get(system=DateTime.MJD) 

659 aboveMin = mjd > self.config.minMJD if self.config.minMJD else True 

660 belowMax = mjd < self.config.maxMJD if self.config.maxMJD else True 

661 intersects[i] = intersects[i] and aboveMin and belowMax 

662 

663 sortedVisits = [v for rad, v in sorted(zip(radius[intersects], visits[intersects]))] 

664 lowerBound = min(int(np.round(self.config.qMin*len(visits[intersects]))), 

665 max(0, len(visits[intersects]) - self.config.nVisitsMin)) 

666 upperBound = max(int(np.round(self.config.qMax*len(visits[intersects]))), self.config.nVisitsMin) 

667 

668 # In order to store as a StructuredDataDict, convert list to dict 

669 goodVisits = {int(visit): True for visit in sortedVisits[lowerBound:upperBound]} 

670 return pipeBase.Struct(goodVisits=goodVisits)