Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# 

2# LSST Data Management System 

3# Copyright 2008, 2009, 2010 LSST Corporation. 

4# 

5# This product includes software developed by the 

6# LSST Project (http://www.lsst.org/). 

7# 

8# This program is free software: you can redistribute it and/or modify 

9# it under the terms of the GNU General Public License as published by 

10# the Free Software Foundation, either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# This program is distributed in the hope that it will be useful, 

14# but WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <http://www.lsstcorp.org/LegalNotices/>. 

21# 

22import numpy as np 

23import lsst.sphgeom 

24import lsst.utils as utils 

25import lsst.pex.config as pexConfig 

26import lsst.pex.exceptions as pexExceptions 

27import lsst.geom as geom 

28import lsst.pipe.base as pipeBase 

29from lsst.skymap import BaseSkyMap 

30from lsst.daf.base import DateTime 

31 

32__all__ = ["BaseSelectImagesTask", "BaseExposureInfo", "WcsSelectImagesTask", "PsfWcsSelectImagesTask", 

33 "DatabaseSelectImagesConfig", "BestSeeingWcsSelectImagesTask", "BestSeeingSelectVisitsTask", 

34 "BestSeeingQuantileSelectVisitsTask"] 

35 

36 

37class DatabaseSelectImagesConfig(pexConfig.Config): 

38 """Base configuration for subclasses of BaseSelectImagesTask that use a database""" 

39 host = pexConfig.Field( 

40 doc="Database server host name", 

41 dtype=str, 

42 ) 

43 port = pexConfig.Field( 

44 doc="Database server port", 

45 dtype=int, 

46 ) 

47 database = pexConfig.Field( 

48 doc="Name of database", 

49 dtype=str, 

50 ) 

51 maxExposures = pexConfig.Field( 

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

53 dtype=int, 

54 optional=True, 

55 ) 

56 

57 

58class BaseExposureInfo(pipeBase.Struct): 

59 """Data about a selected exposure 

60 """ 

61 

62 def __init__(self, dataId, coordList): 

63 """Create exposure information that can be used to generate data references 

64 

65 The object has the following fields: 

66 - dataId: data ID of exposure (a dict) 

67 - coordList: ICRS coordinates of the corners of the exposure (list of lsst.geom.SpherePoint) 

68 plus any others items that are desired 

69 """ 

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

71 

72 

73class BaseSelectImagesTask(pipeBase.Task): 

74 """Base task for selecting images suitable for coaddition 

75 """ 

76 ConfigClass = pexConfig.Config 

77 _DefaultName = "selectImages" 

78 

79 @pipeBase.timeMethod 

80 def run(self, coordList): 

81 """Select images suitable for coaddition in a particular region 

82 

83 @param[in] coordList: list of coordinates defining region of interest; if None then select all images 

84 subclasses may add additional keyword arguments, as required 

85 

86 @return a pipeBase Struct containing: 

87 - exposureInfoList: a list of exposure information objects (subclasses of BaseExposureInfo), 

88 which have at least the following fields: 

89 - dataId: data ID dictionary 

90 - coordList: ICRS coordinates of the corners of the exposure (list of lsst.geom.SpherePoint) 

91 """ 

92 raise NotImplementedError() 

93 

94 def _runArgDictFromDataId(self, dataId): 

95 """Extract keyword arguments for run (other than coordList) from a data ID 

96 

97 @return keyword arguments for run (other than coordList), as a dict 

98 """ 

99 raise NotImplementedError() 

100 

101 def runDataRef(self, dataRef, coordList, makeDataRefList=True, selectDataList=[]): 

102 """Run based on a data reference 

103 

104 This delegates to run() and _runArgDictFromDataId() to do the actual 

105 selection. In the event that the selectDataList is non-empty, this will 

106 be used to further restrict the selection, providing the user with 

107 additional control over the selection. 

108 

109 @param[in] dataRef: data reference; must contain any extra keys needed by the subclass 

110 @param[in] coordList: list of coordinates defining region of interest; if None, search the whole sky 

111 @param[in] makeDataRefList: if True, return dataRefList 

112 @param[in] selectDataList: List of SelectStruct with dataRefs to consider for selection 

113 @return a pipeBase Struct containing: 

114 - exposureInfoList: a list of objects derived from ExposureInfo 

115 - dataRefList: a list of data references (None if makeDataRefList False) 

116 """ 

117 runArgDict = self._runArgDictFromDataId(dataRef.dataId) 

118 exposureInfoList = self.run(coordList, **runArgDict).exposureInfoList 

119 

120 if len(selectDataList) > 0 and len(exposureInfoList) > 0: 

121 # Restrict the exposure selection further 

122 ccdKeys, ccdValues = _extractKeyValue(exposureInfoList) 

123 inKeys, inValues = _extractKeyValue([s.dataRef for s in selectDataList], keys=ccdKeys) 

124 inValues = set(inValues) 

125 newExposureInfoList = [] 

126 for info, ccdVal in zip(exposureInfoList, ccdValues): 

127 if ccdVal in inValues: 

128 newExposureInfoList.append(info) 

129 else: 

130 self.log.info("De-selecting exposure %s: not in selectDataList" % info.dataId) 

131 exposureInfoList = newExposureInfoList 

132 

133 if makeDataRefList: 

134 butler = dataRef.butlerSubset.butler 

135 dataRefList = [butler.dataRef(datasetType="calexp", 

136 dataId=expInfo.dataId, 

137 ) for expInfo in exposureInfoList] 

138 else: 

139 dataRefList = None 

140 

141 return pipeBase.Struct( 

142 dataRefList=dataRefList, 

143 exposureInfoList=exposureInfoList, 

144 ) 

145 

146 

147def _extractKeyValue(dataList, keys=None): 

148 """Extract the keys and values from a list of dataIds 

149 

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

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

152 list of ExposureInfo 

153 """ 

154 assert len(dataList) > 0 

155 if keys is None: 

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

157 keySet = set(keys) 

158 values = list() 

159 for data in dataList: 

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

161 if thisKeys != keySet: 

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

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

164 return keys, values 

165 

166 

167class SelectStruct(pipeBase.Struct): 

168 """A container for data to be passed to the WcsSelectImagesTask""" 

169 

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

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

172 

173 

174class WcsSelectImagesTask(BaseSelectImagesTask): 

175 """Select images using their Wcs 

176 

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

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

179 patch for overlap with the polygon of the image. 

180 

181 We use "convexHull" instead of generating a ConvexPolygon 

182 directly because the standard for the inputs to ConvexPolygon 

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

184 """ 

185 

186 def runDataRef(self, dataRef, coordList, makeDataRefList=True, selectDataList=[]): 

187 """Select images in the selectDataList that overlap the patch 

188 

189 This method is the old entry point for the Gen2 commandline tasks and drivers 

190 Will be deprecated in v22. 

191 

192 @param dataRef: Data reference for coadd/tempExp (with tract, patch) 

193 @param coordList: List of ICRS coordinates (lsst.geom.SpherePoint) specifying boundary of patch 

194 @param makeDataRefList: Construct a list of data references? 

195 @param selectDataList: List of SelectStruct, to consider for selection 

196 """ 

197 dataRefList = [] 

198 exposureInfoList = [] 

199 

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

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

202 

203 for data in selectDataList: 

204 dataRef = data.dataRef 

205 imageWcs = data.wcs 

206 imageBox = data.bbox 

207 

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

209 if imageCorners: 

210 dataRefList.append(dataRef) 

211 exposureInfoList.append(BaseExposureInfo(dataRef.dataId, imageCorners)) 

212 

213 return pipeBase.Struct( 

214 dataRefList=dataRefList if makeDataRefList else None, 

215 exposureInfoList=exposureInfoList, 

216 ) 

217 

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

219 """Return indices of provided lists that meet the selection criteria 

220 

221 Parameters: 

222 ----------- 

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

224 specifying the WCS's of the input ccds to be selected 

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

226 specifying the bounding boxes of the input ccds to be selected 

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

228 ICRS coordinates specifying boundary of the patch. 

229 

230 Returns: 

231 -------- 

232 result: `list` of `int` 

233 of indices of selected ccds 

234 """ 

235 if dataIds is None: 

236 dataIds = [None] * len(wcsList) 

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

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

239 result = [] 

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

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

242 if imageCorners: 

243 result.append(i) 

244 return result 

245 

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

247 "Return corners or None if bad" 

248 try: 

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

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

251 # Protecting ourselves from awful Wcs solutions in input images 

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

253 return 

254 

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

256 if imagePoly is None: 

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

258 return 

259 

260 if patchPoly.intersects(imagePoly): 

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

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

263 return imageCorners 

264 

265 

266def sigmaMad(array): 

267 "Return median absolute deviation scaled to normally distributed data" 

268 return 1.4826*np.median(np.abs(array - np.median(array))) 

269 

270 

271class PsfWcsSelectImagesConnections(pipeBase.PipelineTaskConnections, 

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

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

274 pass 

275 

276 

277class PsfWcsSelectImagesConfig(pipeBase.PipelineTaskConfig, 

278 pipelineConnections=PsfWcsSelectImagesConnections): 

279 maxEllipResidual = pexConfig.Field( 

280 doc="Maximum median ellipticity residual", 

281 dtype=float, 

282 default=0.007, 

283 optional=True, 

284 ) 

285 maxSizeScatter = pexConfig.Field( 

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

287 dtype=float, 

288 optional=True, 

289 ) 

290 maxScaledSizeScatter = pexConfig.Field( 

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

292 dtype=float, 

293 default=0.009, 

294 optional=True, 

295 ) 

296 starSelection = pexConfig.Field( 

297 doc="select star with this field", 

298 dtype=str, 

299 default='calib_psf_used' 

300 ) 

301 starShape = pexConfig.Field( 

302 doc="name of star shape", 

303 dtype=str, 

304 default='base_SdssShape' 

305 ) 

306 psfShape = pexConfig.Field( 

307 doc="name of psf shape", 

308 dtype=str, 

309 default='base_SdssShape_psf' 

310 ) 

311 

312 

313class PsfWcsSelectImagesTask(WcsSelectImagesTask): 

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

315 

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

317 adaptive second moments of the star and the PSF. 

318 

319 The criteria are: 

320 - the median of the ellipticty residuals 

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

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

323 the median size 

324 """ 

325 

326 ConfigClass = PsfWcsSelectImagesConfig 

327 _DefaultName = "PsfWcsSelectImages" 

328 

329 def runDataRef(self, dataRef, coordList, makeDataRefList=True, selectDataList=[]): 

330 """Select images in the selectDataList that overlap the patch and satisfy PSF quality critera. 

331 

332 This method is the old entry point for the Gen2 commandline tasks and drivers 

333 Will be deprecated in v22. 

334 

335 @param dataRef: Data reference for coadd/tempExp (with tract, patch) 

336 @param coordList: List of ICRS coordinates (lsst.geom.SpherePoint) specifying boundary of patch 

337 @param makeDataRefList: Construct a list of data references? 

338 @param selectDataList: List of SelectStruct, to consider for selection 

339 """ 

340 result = super(PsfWcsSelectImagesTask, self).runDataRef(dataRef, coordList, makeDataRefList, 

341 selectDataList) 

342 

343 dataRefList = [] 

344 exposureInfoList = [] 

345 for dataRef, exposureInfo in zip(result.dataRefList, result.exposureInfoList): 

346 butler = dataRef.butlerSubset.butler 

347 srcCatalog = butler.get('src', dataRef.dataId) 

348 valid = self.isValid(srcCatalog, dataRef.dataId) 

349 if valid is False: 

350 continue 

351 

352 dataRefList.append(dataRef) 

353 exposureInfoList.append(exposureInfo) 

354 

355 return pipeBase.Struct( 

356 dataRefList=dataRefList, 

357 exposureInfoList=exposureInfoList, 

358 ) 

359 

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

361 """Return indices of provided lists that meet the selection criteria 

362 

363 Parameters: 

364 ----------- 

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

366 specifying the WCS's of the input ccds to be selected 

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

368 specifying the bounding boxes of the input ccds to be selected 

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

370 ICRS coordinates specifying boundary of the patch. 

371 srcList : `list` of `lsst.afw.table.SourceCatalog` 

372 containing the PSF shape information for the input ccds to be selected 

373 

374 Returns: 

375 -------- 

376 goodPsf: `list` of `int` 

377 of indices of selected ccds 

378 """ 

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

380 coordList=coordList, dataIds=dataIds) 

381 

382 goodPsf = [] 

383 if dataIds is None: 

384 dataIds = [None] * len(srcList) 

385 for i, (srcCatalog, dataId) in enumerate(zip(srcList, dataIds)): 

386 if i not in goodWcs: 

387 continue 

388 if self.isValid(srcCatalog, dataId): 

389 goodPsf.append(i) 

390 

391 return goodPsf 

392 

393 def isValid(self, srcCatalog, dataId=None): 

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

395 

396 Parameters 

397 ---------- 

398 srcCatalog : `lsst.afw.table.SourceCatalog` 

399 dataId : `dict` of dataId keys, optional. 

400 Used only for logging. Defaults to None. 

401 

402 Returns 

403 ------- 

404 valid : `bool` 

405 True if selected. 

406 """ 

407 mask = srcCatalog[self.config.starSelection] 

408 

409 starXX = srcCatalog[self.config.starShape+'_xx'][mask] 

410 starYY = srcCatalog[self.config.starShape+'_yy'][mask] 

411 starXY = srcCatalog[self.config.starShape+'_xy'][mask] 

412 psfXX = srcCatalog[self.config.psfShape+'_xx'][mask] 

413 psfYY = srcCatalog[self.config.psfShape+'_yy'][mask] 

414 psfXY = srcCatalog[self.config.psfShape+'_xy'][mask] 

415 

416 starSize = np.power(starXX*starYY - starXY**2, 0.25) 

417 starE1 = (starXX - starYY)/(starXX + starYY) 

418 starE2 = 2*starXY/(starXX + starYY) 

419 medianSize = np.median(starSize) 

420 

421 psfSize = np.power(psfXX*psfYY - psfXY**2, 0.25) 

422 psfE1 = (psfXX - psfYY)/(psfXX + psfYY) 

423 psfE2 = 2*psfXY/(psfXX + psfYY) 

424 

425 medianE1 = np.abs(np.median(starE1 - psfE1)) 

426 medianE2 = np.abs(np.median(starE2 - psfE2)) 

427 medianE = np.sqrt(medianE1**2 + medianE2**2) 

428 

429 scatterSize = sigmaMad(starSize - psfSize) 

430 scaledScatterSize = scatterSize/medianSize**2 

431 

432 valid = True 

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

434 self.log.info("Removing visit %s because median e residual too large: %f vs %f" % 

435 (dataId, medianE, self.config.maxEllipResidual)) 

436 valid = False 

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

438 self.log.info("Removing visit %s because size scatter is too large: %f vs %f" % 

439 (dataId, scatterSize, self.config.maxSizeScatter)) 

440 valid = False 

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

442 self.log.info("Removing visit %s because scaled size scatter is too large: %f vs %f" % 

443 (dataId, scaledScatterSize, self.config.maxScaledSizeScatter)) 

444 valid = False 

445 

446 return valid 

447 

448 

449class BestSeeingWcsSelectImageConfig(WcsSelectImagesTask.ConfigClass): 

450 """Base configuration for BestSeeingSelectImagesTask. 

451 """ 

452 nImagesMax = pexConfig.RangeField( 

453 dtype=int, 

454 doc="Maximum number of images to select", 

455 default=5, 

456 min=0) 

457 maxPsfFwhm = pexConfig.Field( 

458 dtype=float, 

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

460 default=1.5, 

461 optional=True) 

462 minPsfFwhm = pexConfig.Field( 

463 dtype=float, 

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

465 default=0., 

466 optional=True) 

467 

468 

469class BestSeeingWcsSelectImagesTask(WcsSelectImagesTask): 

470 """Select up to a maximum number of the best-seeing images using their Wcs. 

471 """ 

472 ConfigClass = BestSeeingWcsSelectImageConfig 

473 

474 def runDataRef(self, dataRef, coordList, makeDataRefList=True, 

475 selectDataList=None): 

476 """Select the best-seeing images in the selectDataList that overlap the patch. 

477 

478 This method is the old entry point for the Gen2 commandline tasks and drivers 

479 Will be deprecated in v22. 

480 

481 Parameters 

482 ---------- 

483 dataRef : `lsst.daf.persistence.ButlerDataRef` 

484 Data reference for coadd/tempExp (with tract, patch) 

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

486 List of ICRS sky coordinates specifying boundary of patch 

487 makeDataRefList : `boolean`, optional 

488 Construct a list of data references? 

489 selectDataList : `list` of `SelectStruct` 

490 List of SelectStruct, to consider for selection 

491 

492 Returns 

493 ------- 

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

495 Result struct with components: 

496 - ``exposureList``: the selected exposures 

497 (`list` of `lsst.pipe.tasks.selectImages.BaseExposureInfo`). 

498 - ``dataRefList``: the optional data references corresponding to 

499 each element of ``exposureList`` 

500 (`list` of `lsst.daf.persistence.ButlerDataRef`, or `None`). 

501 """ 

502 psfSizes = [] 

503 dataRefList = [] 

504 exposureInfoList = [] 

505 

506 if selectDataList is None: 

507 selectDataList = [] 

508 

509 result = super().runDataRef(dataRef, coordList, makeDataRefList=True, selectDataList=selectDataList) 

510 

511 for dataRef, exposureInfo in zip(result.dataRefList, result.exposureInfoList): 

512 cal = dataRef.get("calexp", immediate=True) 

513 

514 # if min/max PSF values are defined, remove images out of bounds 

515 pixToArcseconds = cal.getWcs().getPixelScale().asArcseconds() 

516 psfSize = cal.getPsf().computeShape().getDeterminantRadius()*pixToArcseconds 

517 sizeFwhm = psfSize * np.sqrt(8.*np.log(2.)) 

518 if self.config.maxPsfFwhm and sizeFwhm > self.config.maxPsfFwhm: 

519 continue 

520 if self.config.minPsfFwhm and sizeFwhm < self.config.minPsfFwhm: 

521 continue 

522 psfSizes.append(sizeFwhm) 

523 dataRefList.append(dataRef) 

524 exposureInfoList.append(exposureInfo) 

525 

526 if len(psfSizes) > self.config.nImagesMax: 

527 sortedIndices = np.argsort(psfSizes)[:self.config.nImagesMax] 

528 filteredDataRefList = [dataRefList[i] for i in sortedIndices] 

529 filteredExposureInfoList = [exposureInfoList[i] for i in sortedIndices] 

530 self.log.info(f"{len(sortedIndices)} images selected with FWHM " 

531 f"range of {psfSizes[sortedIndices[0]]}--{psfSizes[sortedIndices[-1]]} arcseconds") 

532 

533 else: 

534 if len(psfSizes) == 0: 

535 self.log.warn("0 images selected.") 

536 else: 

537 self.log.debug(f"{len(psfSizes)} images selected with FWHM range " 

538 f"of {psfSizes[0]}--{psfSizes[-1]} arcseconds") 

539 filteredDataRefList = dataRefList 

540 filteredExposureInfoList = exposureInfoList 

541 

542 return pipeBase.Struct( 

543 dataRefList=filteredDataRefList if makeDataRefList else None, 

544 exposureInfoList=filteredExposureInfoList, 

545 ) 

546 

547 

548class BestSeeingSelectVisitsConnections(pipeBase.PipelineTaskConnections, 

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

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

551 skyMap = pipeBase.connectionTypes.Input( 

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

553 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME, 

554 storageClass="SkyMap", 

555 dimensions=("skymap",), 

556 ) 

557 visitSummaries = pipeBase.connectionTypes.Input( 

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

559 name="visitSummary", 

560 storageClass="ExposureCatalog", 

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

562 multiple=True, 

563 deferLoad=True 

564 ) 

565 goodVisits = pipeBase.connectionTypes.Output( 

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

567 name="{coaddName}Visits", 

568 storageClass="StructuredDataDict", 

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

570 ) 

571 

572 

573class BestSeeingSelectVisitsConfig(pipeBase.PipelineTaskConfig, 

574 pipelineConnections=BestSeeingSelectVisitsConnections): 

575 nVisitsMax = pexConfig.RangeField( 

576 dtype=int, 

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

578 default=12, 

579 min=0 

580 ) 

581 maxPsfFwhm = pexConfig.Field( 

582 dtype=float, 

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

584 default=1.5, 

585 optional=True 

586 ) 

587 minPsfFwhm = pexConfig.Field( 

588 dtype=float, 

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

590 default=0., 

591 optional=True 

592 ) 

593 doConfirmOverlap = pexConfig.Field( 

594 dtype=bool, 

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

596 default=True, 

597 ) 

598 minMJD = pexConfig.Field( 

599 dtype=float, 

600 doc="Minimum visit MJD to select", 

601 default=None, 

602 optional=True 

603 ) 

604 maxMJD = pexConfig.Field( 

605 dtype=float, 

606 doc="Maximum visit MJD to select", 

607 default=None, 

608 optional=True 

609 ) 

610 

611 

612class BestSeeingSelectVisitsTask(pipeBase.PipelineTask): 

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

614 

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

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

617 BestSeeingSelectImagesTask. This Task selects full visits based on the 

618 average PSF of the entire visit. 

619 """ 

620 ConfigClass = BestSeeingSelectVisitsConfig 

621 _DefaultName = 'bestSeeingSelectVisits' 

622 

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

624 inputs = butlerQC.get(inputRefs) 

625 quantumDataId = butlerQC.quantum.dataId 

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

627 butlerQC.put(outputs, outputRefs) 

628 

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

630 """Run task 

631 

632 Parameters: 

633 ----------- 

634 visitSummary : `list` 

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

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

637 skyMap : `lsst.skyMap.SkyMap` 

638 SkyMap for checking visits overlap patch 

639 dataId : `dict` of dataId keys 

640 For retrieving patch info for checking visits overlap patch 

641 

642 Returns 

643 ------- 

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

645 Result struct with components: 

646 

647 - `goodVisits`: `dict` with selected visit ids as keys, 

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

649 StructuredDataList's are currently limited. 

650 """ 

651 

652 if self.config.doConfirmOverlap: 

653 patchPolygon = self.makePatchPolygon(skyMap, dataId) 

654 

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

656 fwhmSizes = [] 

657 visits = [] 

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

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

660 visitSummary = visitSummary.get() 

661 

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

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

664 

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

666 for vs in visitSummary] 

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

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

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

670 

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

672 continue 

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

674 continue 

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

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

677 continue 

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

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

680 continue 

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

682 continue 

683 

684 fwhmSizes.append(fwhm) 

685 visits.append(visit) 

686 

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

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

689 self.log.info(f"{len(output)} images selected with FWHM " 

690 f"range of {fwhmSizes[visits.index(output[0])]}" 

691 f"--{fwhmSizes[visits.index(output[-1])]} arcseconds") 

692 

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

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

695 return pipeBase.Struct(goodVisits=goodVisits) 

696 

697 def makePatchPolygon(self, skyMap, dataId): 

698 """Return True if sky polygon overlaps visit 

699 

700 Parameters: 

701 ----------- 

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

703 Exposure catalog with per-detector geometry 

704 dataId : `dict` of dataId keys 

705 For retrieving patch info 

706 

707 Returns: 

708 -------- 

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

710 Polygon of patch's outer bbox 

711 """ 

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

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

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

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

716 return result 

717 

718 def doesIntersectPolygon(self, visitSummary, polygon): 

719 """Return True if sky polygon overlaps visit 

720 

721 Parameters: 

722 ----------- 

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

724 Exposure catalog with per-detector geometry 

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

726 Polygon to check overlap 

727 

728 Returns: 

729 -------- 

730 doesIntersect: `bool` 

731 Does the visit overlap the polygon 

732 """ 

733 doesIntersect = False 

734 for detectorSummary in visitSummary: 

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

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

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

738 if detectorPolygon.intersects(polygon): 

739 doesIntersect = True 

740 break 

741 return doesIntersect 

742 

743 

744class BestSeeingQuantileSelectVisitsConfig(pipeBase.PipelineTaskConfig, 

745 pipelineConnections=BestSeeingSelectVisitsConnections): 

746 qMin = pexConfig.RangeField( 

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

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

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

750 dtype=float, 

751 default=0, 

752 min=0, 

753 max=1, 

754 ) 

755 qMax = pexConfig.RangeField( 

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

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

758 dtype=float, 

759 default=0.33, 

760 min=0, 

761 max=1, 

762 ) 

763 nVisitsMin = pexConfig.Field( 

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

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

766 dtype=int, 

767 default=3, 

768 ) 

769 doConfirmOverlap = pexConfig.Field( 

770 dtype=bool, 

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

772 default=True, 

773 ) 

774 minMJD = pexConfig.Field( 

775 dtype=float, 

776 doc="Minimum visit MJD to select", 

777 default=None, 

778 optional=True 

779 ) 

780 maxMJD = pexConfig.Field( 

781 dtype=float, 

782 doc="Maximum visit MJD to select", 

783 default=None, 

784 optional=True 

785 ) 

786 

787 

788class BestSeeingQuantileSelectVisitsTask(BestSeeingSelectVisitsTask): 

789 """Select a quantile of the best-seeing visits 

790 

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

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

793 experiments that require templates with the worst seeing visits. 

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

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

796 """ 

797 ConfigClass = BestSeeingQuantileSelectVisitsConfig 

798 _DefaultName = 'bestSeeingQuantileSelectVisits' 

799 

800 @utils.inheritDoc(BestSeeingSelectVisitsTask) 

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

802 if self.config.doConfirmOverlap: 

803 patchPolygon = self.makePatchPolygon(skyMap, dataId) 

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

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

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

807 for i, visitSummary in enumerate(visitSummaries): 

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

809 visitSummary = visitSummary.get() 

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

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

812 radius[i] = psfSigma 

813 if self.config.doConfirmOverlap: 

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

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

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

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

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

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

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

821 

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

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

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

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

826 

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

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

829 return pipeBase.Struct(goodVisits=goodVisits)