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

204 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-07 03:43 -0700

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 

266 

267class PsfWcsSelectImagesTask(WcsSelectImagesTask): 

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

269 

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

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

272 

273 The criteria are: 

274 - the median of the ellipticty residuals. 

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

276 deviation). 

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

278 the median size. 

279 """ 

280 

281 ConfigClass = PsfWcsSelectImagesConfig 

282 _DefaultName = "PsfWcsSelectImages" 

283 

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

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

286 

287 Parameters 

288 ---------- 

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

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

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

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

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

294 ICRS coordinates specifying boundary of the patch. 

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

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

297 selected. 

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

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

300 **kwargs 

301 Additional keyword arguments. 

302 

303 Returns 

304 ------- 

305 goodPsf : `list` [`int`] 

306 The indices of selected ccds. 

307 """ 

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

309 coordList=coordList, dataIds=dataIds) 

310 

311 goodPsf = [] 

312 

313 for i, dataId in enumerate(dataIds): 

314 if i not in goodWcs: 

315 continue 

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

317 goodPsf.append(i) 

318 

319 return goodPsf 

320 

321 def isValid(self, visitSummary, detectorId): 

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

323 

324 Parameters 

325 ---------- 

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

327 Exposure catalog with per-detector summary information. 

328 detectorId : `int` 

329 Detector identifier. 

330 

331 Returns 

332 ------- 

333 valid : `bool` 

334 True if selected. 

335 """ 

336 row = visitSummary.find(detectorId) 

337 if row is None: 

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

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

340 return False 

341 

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

343 scatterSize = row["psfStarDeltaSizeScatter"] 

344 scaledScatterSize = row["psfStarScaledDeltaSizeScatter"] 

345 

346 valid = True 

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

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

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

350 valid = False 

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

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

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

354 valid = False 

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

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

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

358 valid = False 

359 

360 return valid 

361 

362 

363class BestSeeingSelectVisitsConnections(pipeBase.PipelineTaskConnections, 

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

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

366 skyMap = pipeBase.connectionTypes.Input( 

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

368 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME, 

369 storageClass="SkyMap", 

370 dimensions=("skymap",), 

371 ) 

372 visitSummaries = pipeBase.connectionTypes.Input( 

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

374 name="visitSummary", 

375 storageClass="ExposureCatalog", 

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

377 multiple=True, 

378 deferLoad=True 

379 ) 

380 goodVisits = pipeBase.connectionTypes.Output( 

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

382 name="{coaddName}Visits", 

383 storageClass="StructuredDataDict", 

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

385 ) 

386 

387 

388class BestSeeingSelectVisitsConfig(pipeBase.PipelineTaskConfig, 

389 pipelineConnections=BestSeeingSelectVisitsConnections): 

390 nVisitsMax = pexConfig.RangeField( 

391 dtype=int, 

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

393 default=12, 

394 min=0 

395 ) 

396 maxPsfFwhm = pexConfig.Field( 

397 dtype=float, 

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

399 default=1.5, 

400 optional=True 

401 ) 

402 minPsfFwhm = pexConfig.Field( 

403 dtype=float, 

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

405 default=0., 

406 optional=True 

407 ) 

408 doConfirmOverlap = pexConfig.Field( 

409 dtype=bool, 

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

411 default=True, 

412 ) 

413 minMJD = pexConfig.Field( 

414 dtype=float, 

415 doc="Minimum visit MJD to select", 

416 default=None, 

417 optional=True 

418 ) 

419 maxMJD = pexConfig.Field( 

420 dtype=float, 

421 doc="Maximum visit MJD to select", 

422 default=None, 

423 optional=True 

424 ) 

425 

426 

427class BestSeeingSelectVisitsTask(pipeBase.PipelineTask): 

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

429 

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

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

432 BestSeeingSelectImagesTask. This Task selects full visits based on the 

433 average PSF of the entire visit. 

434 """ 

435 

436 ConfigClass = BestSeeingSelectVisitsConfig 

437 _DefaultName = 'bestSeeingSelectVisits' 

438 

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

440 inputs = butlerQC.get(inputRefs) 

441 quantumDataId = butlerQC.quantum.dataId 

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

443 butlerQC.put(outputs, outputRefs) 

444 

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

446 """Run task. 

447 

448 Parameters 

449 ---------- 

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

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

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

453 skyMap : `lsst.skyMap.SkyMap` 

454 SkyMap for checking visits overlap patch. 

455 dataId : `dict` of dataId keys 

456 For retrieving patch info for checking visits overlap patch. 

457 

458 Returns 

459 ------- 

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

461 Results as a struct with attributes: 

462 

463 ``goodVisits`` 

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

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

466 StructuredDataList's are currently limited. 

467 """ 

468 if self.config.doConfirmOverlap: 

469 patchPolygon = self.makePatchPolygon(skyMap, dataId) 

470 

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

472 fwhmSizes = [] 

473 visits = [] 

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

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

476 visitSummary = visitSummary.get() 

477 

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

479 # visitSummary. 

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

481 

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

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

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

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

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

487 

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

489 continue 

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

491 continue 

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

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

494 continue 

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

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

497 continue 

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

499 continue 

500 

501 fwhmSizes.append(fwhm) 

502 visits.append(visit) 

503 

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

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

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

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

508 

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

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

511 return pipeBase.Struct(goodVisits=goodVisits) 

512 

513 def makePatchPolygon(self, skyMap, dataId): 

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

515 

516 Parameters 

517 ---------- 

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

519 Exposure catalog with per-detector geometry. 

520 dataId : `dict` of dataId keys 

521 For retrieving patch info. 

522 

523 Returns 

524 ------- 

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

526 Polygon of patch's outer bbox. 

527 """ 

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

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

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

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

532 return result 

533 

534 def doesIntersectPolygon(self, visitSummary, polygon): 

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

536 

537 Parameters 

538 ---------- 

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

540 Exposure catalog with per-detector geometry. 

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

542 Polygon to check overlap. 

543 

544 Returns 

545 ------- 

546 doesIntersect : `bool` 

547 True if the visit overlaps the polygon. 

548 """ 

549 doesIntersect = False 

550 for detectorSummary in visitSummary: 

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

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

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

554 for (ra, decl) in 

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

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

557 if detectorPolygon.intersects(polygon): 

558 doesIntersect = True 

559 break 

560 return doesIntersect 

561 

562 

563class BestSeeingQuantileSelectVisitsConfig(pipeBase.PipelineTaskConfig, 

564 pipelineConnections=BestSeeingSelectVisitsConnections): 

565 qMin = pexConfig.RangeField( 

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

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

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

569 dtype=float, 

570 default=0, 

571 min=0, 

572 max=1, 

573 ) 

574 qMax = pexConfig.RangeField( 

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

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

577 dtype=float, 

578 default=0.33, 

579 min=0, 

580 max=1, 

581 ) 

582 nVisitsMin = pexConfig.Field( 

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

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

585 dtype=int, 

586 default=6, 

587 ) 

588 doConfirmOverlap = pexConfig.Field( 

589 dtype=bool, 

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

591 default=True, 

592 ) 

593 minMJD = pexConfig.Field( 

594 dtype=float, 

595 doc="Minimum visit MJD to select", 

596 default=None, 

597 optional=True 

598 ) 

599 maxMJD = pexConfig.Field( 

600 dtype=float, 

601 doc="Maximum visit MJD to select", 

602 default=None, 

603 optional=True 

604 ) 

605 

606 

607class BestSeeingQuantileSelectVisitsTask(BestSeeingSelectVisitsTask): 

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

609 

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

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

612 experiments that require templates with the worst seeing visits. 

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

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

615 """ 

616 ConfigClass = BestSeeingQuantileSelectVisitsConfig 

617 _DefaultName = 'bestSeeingQuantileSelectVisits' 

618 

619 @utils.inheritDoc(BestSeeingSelectVisitsTask) 

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

621 if self.config.doConfirmOverlap: 

622 patchPolygon = self.makePatchPolygon(skyMap, dataId) 

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

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

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

626 for i, visitSummary in enumerate(visitSummaries): 

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

628 visitSummary = visitSummary.get() 

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

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

631 radius[i] = psfSigma 

632 if self.config.doConfirmOverlap: 

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

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

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

636 # visitSummary. 

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

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

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

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

641 

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

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

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

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

646 

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

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

649 return pipeBase.Struct(goodVisits=goodVisits)