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

200 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-09-30 10:45 +0000

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 database.""" 

40 

41 host = pexConfig.Field( 

42 doc="Database server host name", 

43 dtype=str, 

44 ) 

45 port = pexConfig.Field( 

46 doc="Database server port", 

47 dtype=int, 

48 ) 

49 database = pexConfig.Field( 

50 doc="Name of database", 

51 dtype=str, 

52 ) 

53 maxExposures = pexConfig.Field( 

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

55 dtype=int, 

56 optional=True, 

57 ) 

58 

59 

60class BaseExposureInfo(pipeBase.Struct): 

61 """Data about a selected exposure. 

62 

63 Parameters 

64 ---------- 

65 dataId : `dict` of dataId keys 

66 Data ID of exposure. 

67 coordList : `list` of `lsst.afw.geom.SpherePoint` 

68 ICRS coordinates of the corners of the exposure 

69 plus any others items that are desired. 

70 """ 

71 

72 def __init__(self, dataId, coordList): 

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

74 

75 

76class BaseSelectImagesTask(pipeBase.Task): 

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

78 """ 

79 

80 ConfigClass = pexConfig.Config 

81 _DefaultName = "selectImages" 

82 

83 @timeMethod 

84 def run(self, coordList): 

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

86 

87 Parameters 

88 ---------- 

89 coordList : `list` of `lsst.geom.SpherePoint` or `None` 

90 List of coordinates defining region of interest; if None then select all images 

91 subclasses may add additional keyword arguments, as required. 

92 

93 Returns 

94 ------- 

95 result : `pipeBase.Struct` 

96 Results as a struct with attributes: 

97 

98 ``exposureInfoList`` 

99 A list of exposure information objects (subclasses of BaseExposureInfo), 

100 which have at least the following fields: 

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

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

103 (`list` of `lsst.geom.SpherePoint`) 

104 """ 

105 raise NotImplementedError() 

106 

107 

108def _extractKeyValue(dataList, keys=None): 

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

110 

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

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

113 list of ExposureInfo. 

114 

115 Parameters 

116 ---------- 

117 dataList : `Unknown` 

118 keys : `Unknown` 

119 

120 Returns 

121 ------- 

122 keys : `Unknown` 

123 values : `Unknown` 

124 

125 Raises 

126 ------ 

127 RuntimeError 

128 Raised if DataId keys are inconsistent. 

129 """ 

130 assert len(dataList) > 0 

131 if keys is None: 

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

133 keySet = set(keys) 

134 values = list() 

135 for data in dataList: 

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

137 if thisKeys != keySet: 

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

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

140 return keys, values 

141 

142 

143class SelectStruct(pipeBase.Struct): 

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

145 

146 Parameters 

147 ---------- 

148 dataRef : `Unknown` 

149 Data reference. 

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

151 Coordinate system definition (wcs). 

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

153 Integer bounding box for image. 

154 """ 

155 

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

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

158 

159 

160class WcsSelectImagesTask(BaseSelectImagesTask): 

161 """Select images using their Wcs. 

162 

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

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

165 patch for overlap with the polygon of the image. 

166 

167 We use "convexHull" instead of generating a ConvexPolygon 

168 directly because the standard for the inputs to ConvexPolygon 

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

170 """ 

171 

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

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

174 

175 Parameters 

176 ---------- 

177 wcsList : `list` of `lsst.afw.geom.SkyWcs` 

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

179 bboxList : `list` of `lsst.geom.Box2I` 

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

181 coordList : `list` of `lsst.geom.SpherePoint` 

182 ICRS coordinates specifying boundary of the patch. 

183 dataIds : iterable of `lsst.daf.butler.dataId` or `None`, optional 

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

185 **kwargs 

186 Additional keyword arguments. 

187 

188 Returns 

189 ------- 

190 result : `list` of `int` 

191 The indices of selected ccds. 

192 """ 

193 if dataIds is None: 

194 dataIds = [None] * len(wcsList) 

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

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

197 result = [] 

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

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

200 if imageCorners: 

201 result.append(i) 

202 return result 

203 

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

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

206 

207 Parameters 

208 ---------- 

209 imageWcs : `Unknown` 

210 imageBox : `Unknown` 

211 patchPoly : `Unknown` 

212 dataId : `Unknown` 

213 """ 

214 try: 

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

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

217 # Protecting ourselves from awful Wcs solutions in input images 

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

219 return 

220 

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

222 if imagePoly is None: 

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

224 return 

225 

226 if patchPoly.intersects(imagePoly): 

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

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

229 return imageCorners 

230 

231 

232class PsfWcsSelectImagesConnections(pipeBase.PipelineTaskConnections, 

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

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

235 pass 

236 

237 

238class PsfWcsSelectImagesConfig(pipeBase.PipelineTaskConfig, 

239 pipelineConnections=PsfWcsSelectImagesConnections): 

240 maxEllipResidual = pexConfig.Field( 

241 doc="Maximum median ellipticity residual", 

242 dtype=float, 

243 default=0.007, 

244 optional=True, 

245 ) 

246 maxSizeScatter = pexConfig.Field( 

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

248 dtype=float, 

249 optional=True, 

250 ) 

251 maxScaledSizeScatter = pexConfig.Field( 

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

253 dtype=float, 

254 default=0.009, 

255 optional=True, 

256 ) 

257 

258 

259class PsfWcsSelectImagesTask(WcsSelectImagesTask): 

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

261 

262 The PSF quality criteria are based on the size and ellipticity residuals from the 

263 adaptive second moments of the star and the PSF. 

264 

265 The criteria are: 

266 - the median of the ellipticty residuals. 

267 - the robust scatter of the size residuals (using the median absolute deviation). 

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

269 the median size. 

270 """ 

271 

272 ConfigClass = PsfWcsSelectImagesConfig 

273 _DefaultName = "PsfWcsSelectImages" 

274 

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

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

277 

278 Parameters 

279 ---------- 

280 wcsList : `list` of `lsst.afw.geom.SkyWcs` 

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

282 bboxList : `list` of `lsst.geom.Box2I` 

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

284 coordList : `list` of `lsst.geom.SpherePoint` 

285 ICRS coordinates specifying boundary of the patch. 

286 visitSummary : `list` of `lsst.afw.table.ExposureCatalog` 

287 containing the PSF shape information for the input ccds to be selected. 

288 dataIds : iterable of `lsst.daf.butler.dataId` or `None`, optional 

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

290 **kwargs 

291 Additional keyword arguments. 

292 

293 Returns 

294 ------- 

295 goodPsf: `list` of `int` 

296 The indices of selected ccds. 

297 """ 

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

299 coordList=coordList, dataIds=dataIds) 

300 

301 goodPsf = [] 

302 

303 for i, dataId in enumerate(dataIds): 

304 if i not in goodWcs: 

305 continue 

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

307 goodPsf.append(i) 

308 

309 return goodPsf 

310 

311 def isValid(self, visitSummary, detectorId): 

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

313 

314 Parameters 

315 ---------- 

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

317 Exposure catalog with per-detector summary information. 

318 detectorId : `int` 

319 Detector identifier. 

320 

321 Returns 

322 ------- 

323 valid : `bool` 

324 True if selected. 

325 """ 

326 row = visitSummary.find(detectorId) 

327 if row is None: 

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

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

330 return False 

331 

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

333 scatterSize = row["psfStarDeltaSizeScatter"] 

334 scaledScatterSize = row["psfStarScaledDeltaSizeScatter"] 

335 

336 valid = True 

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

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

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

340 valid = False 

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

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

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

344 valid = False 

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

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

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

348 valid = False 

349 

350 return valid 

351 

352 

353class BestSeeingSelectVisitsConnections(pipeBase.PipelineTaskConnections, 

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

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

356 skyMap = pipeBase.connectionTypes.Input( 

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

358 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME, 

359 storageClass="SkyMap", 

360 dimensions=("skymap",), 

361 ) 

362 visitSummaries = pipeBase.connectionTypes.Input( 

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

364 name="visitSummary", 

365 storageClass="ExposureCatalog", 

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

367 multiple=True, 

368 deferLoad=True 

369 ) 

370 goodVisits = pipeBase.connectionTypes.Output( 

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

372 name="{coaddName}Visits", 

373 storageClass="StructuredDataDict", 

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

375 ) 

376 

377 

378class BestSeeingSelectVisitsConfig(pipeBase.PipelineTaskConfig, 

379 pipelineConnections=BestSeeingSelectVisitsConnections): 

380 nVisitsMax = pexConfig.RangeField( 

381 dtype=int, 

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

383 default=12, 

384 min=0 

385 ) 

386 maxPsfFwhm = pexConfig.Field( 

387 dtype=float, 

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

389 default=1.5, 

390 optional=True 

391 ) 

392 minPsfFwhm = pexConfig.Field( 

393 dtype=float, 

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

395 default=0., 

396 optional=True 

397 ) 

398 doConfirmOverlap = pexConfig.Field( 

399 dtype=bool, 

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

401 default=True, 

402 ) 

403 minMJD = pexConfig.Field( 

404 dtype=float, 

405 doc="Minimum visit MJD to select", 

406 default=None, 

407 optional=True 

408 ) 

409 maxMJD = pexConfig.Field( 

410 dtype=float, 

411 doc="Maximum visit MJD to select", 

412 default=None, 

413 optional=True 

414 ) 

415 

416 

417class BestSeeingSelectVisitsTask(pipeBase.PipelineTask): 

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

419 

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

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

422 BestSeeingSelectImagesTask. This Task selects full visits based on the 

423 average PSF of the entire visit. 

424 """ 

425 

426 ConfigClass = BestSeeingSelectVisitsConfig 

427 _DefaultName = 'bestSeeingSelectVisits' 

428 

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

430 inputs = butlerQC.get(inputRefs) 

431 quantumDataId = butlerQC.quantum.dataId 

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

433 butlerQC.put(outputs, outputRefs) 

434 

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

436 """Run task. 

437 

438 Parameters 

439 ---------- 

440 visitSummary : `list` of `lsst.pipe.base.connections.DeferredDatasetRef` 

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

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

443 skyMap : `lsst.skyMap.SkyMap` 

444 SkyMap for checking visits overlap patch. 

445 dataId : `dict` of dataId keys 

446 For retrieving patch info for checking visits overlap patch. 

447 

448 Returns 

449 ------- 

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

451 Results as a struct with attributes: 

452 

453 ``goodVisits`` 

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

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

456 StructuredDataList's are currently limited. 

457 """ 

458 if self.config.doConfirmOverlap: 

459 patchPolygon = self.makePatchPolygon(skyMap, dataId) 

460 

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

462 fwhmSizes = [] 

463 visits = [] 

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

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

466 visitSummary = visitSummary.get() 

467 

468 # mjd is guaranteed to be the same for every detector in the visitSummary. 

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

470 

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

472 for vs in visitSummary] 

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

474 psfSigmas = np.array([vs['psfSigma'] for vs in visitSummary]) 

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

476 

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

478 continue 

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

480 continue 

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

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

483 continue 

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

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

486 continue 

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

488 continue 

489 

490 fwhmSizes.append(fwhm) 

491 visits.append(visit) 

492 

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

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

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

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

497 

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

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

500 return pipeBase.Struct(goodVisits=goodVisits) 

501 

502 def makePatchPolygon(self, skyMap, dataId): 

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

504 

505 Parameters 

506 ---------- 

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

508 Exposure catalog with per-detector geometry. 

509 dataId : `dict` of dataId keys 

510 For retrieving patch info. 

511 

512 Returns 

513 ------- 

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

515 Polygon of patch's outer bbox. 

516 """ 

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

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

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

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

521 return result 

522 

523 def doesIntersectPolygon(self, visitSummary, polygon): 

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

525 

526 Parameters 

527 ---------- 

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

529 Exposure catalog with per-detector geometry. 

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

531 Polygon to check overlap. 

532 

533 Returns 

534 ------- 

535 doesIntersect : `bool` 

536 True if the visit overlaps the polygon. 

537 """ 

538 doesIntersect = False 

539 for detectorSummary in visitSummary: 

540 corners = [lsst.geom.SpherePoint(ra, decl, units=lsst.geom.degrees).getVector() for (ra, decl) in 

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

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

543 if detectorPolygon.intersects(polygon): 

544 doesIntersect = True 

545 break 

546 return doesIntersect 

547 

548 

549class BestSeeingQuantileSelectVisitsConfig(pipeBase.PipelineTaskConfig, 

550 pipelineConnections=BestSeeingSelectVisitsConnections): 

551 qMin = pexConfig.RangeField( 

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

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

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

555 dtype=float, 

556 default=0, 

557 min=0, 

558 max=1, 

559 ) 

560 qMax = pexConfig.RangeField( 

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

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

563 dtype=float, 

564 default=0.33, 

565 min=0, 

566 max=1, 

567 ) 

568 nVisitsMin = pexConfig.Field( 

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

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

571 dtype=int, 

572 default=6, 

573 ) 

574 doConfirmOverlap = pexConfig.Field( 

575 dtype=bool, 

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

577 default=True, 

578 ) 

579 minMJD = pexConfig.Field( 

580 dtype=float, 

581 doc="Minimum visit MJD to select", 

582 default=None, 

583 optional=True 

584 ) 

585 maxMJD = pexConfig.Field( 

586 dtype=float, 

587 doc="Maximum visit MJD to select", 

588 default=None, 

589 optional=True 

590 ) 

591 

592 

593class BestSeeingQuantileSelectVisitsTask(BestSeeingSelectVisitsTask): 

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

595 

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

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

598 experiments that require templates with the worst seeing visits. 

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

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

601 """ 

602 ConfigClass = BestSeeingQuantileSelectVisitsConfig 

603 _DefaultName = 'bestSeeingQuantileSelectVisits' 

604 

605 @utils.inheritDoc(BestSeeingSelectVisitsTask) 

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

607 if self.config.doConfirmOverlap: 

608 patchPolygon = self.makePatchPolygon(skyMap, dataId) 

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

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

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

612 for i, visitSummary in enumerate(visitSummaries): 

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

614 visitSummary = visitSummary.get() 

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

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

617 radius[i] = psfSigma 

618 if self.config.doConfirmOverlap: 

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

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

621 # mjd is guaranteed to be the same for every detector in the visitSummary. 

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

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

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

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

626 

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

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

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

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

631 

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

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

634 return pipeBase.Struct(goodVisits=goodVisits)