Coverage for python/lsst/ip/diffim/getTemplate.py: 21%

304 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-08-27 03:16 -0700

1# 

2# LSST Data Management System 

3# Copyright 2016 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# 

22 

23import numpy as np 

24 

25import lsst.afw.image as afwImage 

26import lsst.geom as geom 

27import lsst.afw.geom as afwGeom 

28import lsst.afw.table as afwTable 

29import lsst.afw.math as afwMath 

30import lsst.pex.config as pexConfig 

31import lsst.pipe.base as pipeBase 

32from lsst.skymap import BaseSkyMap 

33from lsst.daf.butler import DeferredDatasetHandle 

34from lsst.ip.diffim.dcrModel import DcrModel 

35from lsst.meas.algorithms import CoaddPsf, CoaddPsfConfig 

36 

37__all__ = ["GetCoaddAsTemplateTask", "GetCoaddAsTemplateConfig", 

38 "GetTemplateTask", "GetTemplateConfig", 

39 "GetDcrTemplateTask", "GetDcrTemplateConfig", 

40 "GetMultiTractCoaddTemplateTask", "GetMultiTractCoaddTemplateConfig"] 

41 

42 

43class GetCoaddAsTemplateConfig(pexConfig.Config): 

44 templateBorderSize = pexConfig.Field( 

45 dtype=int, 

46 default=20, 

47 doc="Number of pixels to grow the requested template image to account for warping" 

48 ) 

49 coaddName = pexConfig.Field( 

50 doc="coadd name: typically one of 'deep', 'goodSeeing', or 'dcr'", 

51 dtype=str, 

52 default="deep", 

53 ) 

54 warpType = pexConfig.Field( 

55 doc="Warp type of the coadd template: one of 'direct' or 'psfMatched'", 

56 dtype=str, 

57 default="direct", 

58 ) 

59 

60 

61class GetCoaddAsTemplateTask(pipeBase.Task): 

62 """Subtask to retrieve coadd for use as an image difference template. 

63 

64 This is the default getTemplate Task to be run as a subtask by 

65 ``pipe.tasks.ImageDifferenceTask``. 

66 

67 Notes 

68 ----- 

69 From the given skymap, the closest tract is selected; multiple tracts are 

70 not supported. The assembled template inherits the WCS of the selected 

71 skymap tract and the resolution of the template exposures. Overlapping box 

72 regions of the input template patches are pixel by pixel copied into the 

73 assembled template image. There is no warping or pixel resampling. 

74 

75 Pixels with no overlap of any available input patches are set to ``nan`` value 

76 and ``NO_DATA`` flagged. 

77 """ 

78 

79 ConfigClass = GetCoaddAsTemplateConfig 

80 _DefaultName = "GetCoaddAsTemplateTask" 

81 

82 def runQuantum(self, exposure, butlerQC, skyMapRef, coaddExposureRefs): 

83 """Gen3 task entry point. Retrieve and mosaic a template coadd exposure 

84 that overlaps the science exposure. 

85 

86 Parameters 

87 ---------- 

88 exposure : `lsst.afw.image.Exposure` 

89 The science exposure to define the sky region of the template coadd. 

90 butlerQC : `lsst.pipe.base.ButlerQuantumContext` 

91 Butler like object that supports getting data by DatasetRef. 

92 skyMapRef : `lsst.daf.butler.DatasetRef` 

93 Reference to SkyMap object that corresponds to the template coadd. 

94 coaddExposureRefs : iterable of `lsst.daf.butler.DeferredDatasetRef` 

95 Iterable of references to the available template coadd patches. 

96 

97 Returns 

98 ------- 

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

100 - ``exposure`` : `lsst.afw.image.ExposureF` 

101 a template coadd exposure assembled out of patches 

102 - ``sources`` : `None` for this subtask 

103 """ 

104 self.log.warn("GetCoaddAsTemplateTask is deprecated. Use GetTemplateTask instead.") 

105 skyMap = butlerQC.get(skyMapRef) 

106 coaddExposureRefs = butlerQC.get(coaddExposureRefs) 

107 tracts = [ref.dataId['tract'] for ref in coaddExposureRefs] 

108 if tracts.count(tracts[0]) == len(tracts): 

109 tractInfo = skyMap[tracts[0]] 

110 else: 

111 raise RuntimeError("Templates constructed from multiple Tracts not supported by this task. " 

112 "Use GetTemplateTask instead.") 

113 

114 detectorBBox = exposure.getBBox() 

115 detectorWcs = exposure.getWcs() 

116 detectorCorners = detectorWcs.pixelToSky(geom.Box2D(detectorBBox).getCorners()) 

117 validPolygon = exposure.getInfo().getValidPolygon() 

118 detectorPolygon = validPolygon if validPolygon else geom.Box2D(detectorBBox) 

119 

120 availableCoaddRefs = dict() 

121 overlappingArea = 0 

122 for coaddRef in coaddExposureRefs: 

123 dataId = coaddRef.dataId 

124 patchWcs = skyMap[dataId['tract']].getWcs() 

125 patchBBox = skyMap[dataId['tract']][dataId['patch']].getOuterBBox() 

126 patchCorners = patchWcs.pixelToSky(geom.Box2D(patchBBox).getCorners()) 

127 patchPolygon = afwGeom.Polygon(detectorWcs.skyToPixel(patchCorners)) 

128 if patchPolygon.intersection(detectorPolygon): 

129 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea() 

130 if self.config.coaddName == 'dcr': 

131 self.log.info("Using template input tract=%s, patch=%s, subfilter=%s", 

132 dataId['tract'], dataId['patch'], dataId['subfilter']) 

133 if dataId['patch'] in availableCoaddRefs: 

134 availableCoaddRefs[dataId['patch']].append(coaddRef) 

135 else: 

136 availableCoaddRefs[dataId['patch']] = [coaddRef, ] 

137 else: 

138 self.log.info("Using template input tract=%s, patch=%s", 

139 dataId['tract'], dataId['patch']) 

140 availableCoaddRefs[dataId['patch']] = coaddRef 

141 

142 if overlappingArea == 0: 

143 templateExposure = None 

144 pixGood = 0 

145 self.log.warning("No overlapping template patches found") 

146 else: 

147 patchList = [tractInfo[patch] for patch in availableCoaddRefs.keys()] 

148 templateExposure = self.run(tractInfo, patchList, detectorCorners, availableCoaddRefs, 

149 visitInfo=exposure.getInfo().getVisitInfo()) 

150 # Count the number of pixels with the NO_DATA mask bit set 

151 # counting NaN pixels is insufficient because pixels without data are often intepolated over) 

152 pixNoData = np.count_nonzero(templateExposure.mask.array 

153 & templateExposure.mask.getPlaneBitMask('NO_DATA')) 

154 pixGood = templateExposure.getBBox().getArea() - pixNoData 

155 self.log.info("template has %d good pixels (%.1f%%)", pixGood, 

156 100*pixGood/templateExposure.getBBox().getArea()) 

157 return pipeBase.Struct(exposure=templateExposure, sources=None, area=pixGood) 

158 

159 def getOverlapPatchList(self, exposure, skyMap): 

160 """Select the relevant tract and its patches that overlap with the science exposure. 

161 

162 Parameters 

163 ---------- 

164 exposure : `lsst.afw.image.Exposure` 

165 The science exposure to define the sky region of the template coadd. 

166 

167 skyMap : `lsst.skymap.BaseSkyMap` 

168 SkyMap object that corresponds to the template coadd. 

169 

170 Returns 

171 ------- 

172 result : `tuple` of 

173 - ``tractInfo`` : `lsst.skymap.TractInfo` 

174 The selected tract. 

175 - ``patchList`` : `list` of `lsst.skymap.PatchInfo` 

176 List of all overlap patches of the selected tract. 

177 - ``skyCorners`` : `list` of `lsst.geom.SpherePoint` 

178 Corners of the exposure in the sky in the order given by `lsst.geom.Box2D.getCorners`. 

179 """ 

180 expWcs = exposure.getWcs() 

181 expBoxD = geom.Box2D(exposure.getBBox()) 

182 expBoxD.grow(self.config.templateBorderSize) 

183 ctrSkyPos = expWcs.pixelToSky(expBoxD.getCenter()) 

184 tractInfo = skyMap.findTract(ctrSkyPos) 

185 self.log.info("Using skyMap tract %s", tractInfo.getId()) 

186 skyCorners = [expWcs.pixelToSky(pixPos) for pixPos in expBoxD.getCorners()] 

187 patchList = tractInfo.findPatchList(skyCorners) 

188 

189 if not patchList: 

190 raise RuntimeError("No suitable tract found") 

191 

192 self.log.info("Assembling %d coadd patches", len(patchList)) 

193 self.log.info("exposure dimensions=%s", exposure.getDimensions()) 

194 

195 return (tractInfo, patchList, skyCorners) 

196 

197 def run(self, tractInfo, patchList, skyCorners, availableCoaddRefs, 

198 sensorRef=None, visitInfo=None): 

199 """Determination of exposure dimensions and copying of pixels from 

200 overlapping patch regions. 

201 

202 Parameters 

203 ---------- 

204 skyMap : `lsst.skymap.BaseSkyMap` 

205 SkyMap object that corresponds to the template coadd. 

206 tractInfo : `lsst.skymap.TractInfo` 

207 The selected tract. 

208 patchList : iterable of `lsst.skymap.patchInfo.PatchInfo` 

209 Patches to consider for making the template exposure. 

210 skyCorners : list of `lsst.geom.SpherePoint` 

211 Sky corner coordinates to be covered by the template exposure. 

212 availableCoaddRefs : `dict` [`int`] 

213 Dictionary of spatially relevant retrieved coadd patches, 

214 indexed by their sequential patch number. Values are 

215 `lsst.daf.butler.DeferredDatasetHandle` and ``.get()`` is called. 

216 sensorRef : `None` 

217 Must always be `None`. Gen2 parameters are no longer used. 

218 visitInfo : `lsst.afw.image.VisitInfo` 

219 VisitInfo to make dcr model. 

220 

221 Returns 

222 ------- 

223 templateExposure: `lsst.afw.image.ExposureF` 

224 The created template exposure. 

225 """ 

226 if sensorRef is not None: 

227 raise ValueError("sensorRef parameter is a Gen2 parameter that is no longer usable." 

228 " Please move to Gen3 middleware.") 

229 coaddWcs = tractInfo.getWcs() 

230 

231 # compute coadd bbox 

232 coaddBBox = geom.Box2D() 

233 for skyPos in skyCorners: 

234 coaddBBox.include(coaddWcs.skyToPixel(skyPos)) 

235 coaddBBox = geom.Box2I(coaddBBox) 

236 self.log.info("coadd dimensions=%s", coaddBBox.getDimensions()) 

237 

238 coaddExposure = afwImage.ExposureF(coaddBBox, coaddWcs) 

239 coaddExposure.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan) 

240 nPatchesFound = 0 

241 coaddFilterLabel = None 

242 coaddPsf = None 

243 coaddPhotoCalib = None 

244 for patchInfo in patchList: 

245 patchNumber = tractInfo.getSequentialPatchIndex(patchInfo) 

246 patchSubBBox = patchInfo.getOuterBBox() 

247 patchSubBBox.clip(coaddBBox) 

248 if patchNumber not in availableCoaddRefs: 

249 self.log.warning("skip patch=%d; patch does not exist for this coadd", patchNumber) 

250 continue 

251 if patchSubBBox.isEmpty(): 

252 if isinstance(availableCoaddRefs[patchNumber], DeferredDatasetHandle): 

253 tract = availableCoaddRefs[patchNumber].dataId['tract'] 

254 else: 

255 tract = availableCoaddRefs[patchNumber]['tract'] 

256 self.log.info("skip tract=%d patch=%d; no overlapping pixels", tract, patchNumber) 

257 continue 

258 

259 if self.config.coaddName == 'dcr': 

260 patchInnerBBox = patchInfo.getInnerBBox() 

261 patchInnerBBox.clip(coaddBBox) 

262 if np.min(patchInnerBBox.getDimensions()) <= 2*self.config.templateBorderSize: 

263 self.log.info("skip tract=%(tract)s, patch=%(patch)s; too few pixels.", 

264 availableCoaddRefs[patchNumber]) 

265 continue 

266 self.log.info("Constructing DCR-matched template for patch %s", 

267 availableCoaddRefs[patchNumber]) 

268 

269 dcrModel = DcrModel.fromQuantum(availableCoaddRefs[patchNumber], 

270 self.config.effectiveWavelength, 

271 self.config.bandwidth) 

272 # The edge pixels of the DcrCoadd may contain artifacts due to missing data. 

273 # Each patch has significant overlap, and the contaminated edge pixels in 

274 # a new patch will overwrite good pixels in the overlap region from 

275 # previous patches. 

276 # Shrink the BBox to remove the contaminated pixels, 

277 # but make sure it is only the overlap region that is reduced. 

278 dcrBBox = geom.Box2I(patchSubBBox) 

279 dcrBBox.grow(-self.config.templateBorderSize) 

280 dcrBBox.include(patchInnerBBox) 

281 coaddPatch = dcrModel.buildMatchedExposure(bbox=dcrBBox, 

282 visitInfo=visitInfo) 

283 else: 

284 coaddPatch = availableCoaddRefs[patchNumber].get() 

285 

286 nPatchesFound += 1 

287 

288 # Gen2 get() seems to clip based on bbox kwarg but we removed bbox 

289 # calculation from caller code. Gen3 also does not do this. 

290 overlapBox = coaddPatch.getBBox() 

291 overlapBox.clip(coaddBBox) 

292 coaddExposure.maskedImage.assign(coaddPatch.maskedImage[overlapBox], overlapBox) 

293 

294 if coaddFilterLabel is None: 

295 coaddFilterLabel = coaddPatch.getFilter() 

296 

297 # Retrieve the PSF for this coadd tract, if not already retrieved 

298 if coaddPsf is None and coaddPatch.hasPsf(): 

299 coaddPsf = coaddPatch.getPsf() 

300 

301 # Retrieve the calibration for this coadd tract, if not already retrieved 

302 if coaddPhotoCalib is None: 

303 coaddPhotoCalib = coaddPatch.getPhotoCalib() 

304 

305 if coaddPhotoCalib is None: 

306 raise RuntimeError("No coadd PhotoCalib found!") 

307 if nPatchesFound == 0: 

308 raise RuntimeError("No patches found!") 

309 if coaddPsf is None: 

310 raise RuntimeError("No coadd Psf found!") 

311 

312 coaddExposure.setPhotoCalib(coaddPhotoCalib) 

313 coaddExposure.setPsf(coaddPsf) 

314 coaddExposure.setFilter(coaddFilterLabel) 

315 return coaddExposure 

316 

317 def getCoaddDatasetName(self): 

318 """Return coadd name for given task config 

319 

320 Returns 

321 ------- 

322 CoaddDatasetName : `string` 

323 

324 TODO: This nearly duplicates a method in CoaddBaseTask (DM-11985) 

325 """ 

326 warpType = self.config.warpType 

327 suffix = "" if warpType == "direct" else warpType[0].upper() + warpType[1:] 

328 return self.config.coaddName + "Coadd" + suffix 

329 

330 

331class GetTemplateConnections(pipeBase.PipelineTaskConnections, 

332 dimensions=("instrument", "visit", "detector", "skymap"), 

333 defaultTemplates={"coaddName": "goodSeeing", 

334 "warpTypeSuffix": "", 

335 "fakesType": ""}): 

336 bbox = pipeBase.connectionTypes.Input( 

337 doc="BBoxes of calexp used determine geometry of output template", 

338 name="{fakesType}calexp.bbox", 

339 storageClass="Box2I", 

340 dimensions=("instrument", "visit", "detector"), 

341 ) 

342 wcs = pipeBase.connectionTypes.Input( 

343 doc="WCS of the calexp that we want to fetch the template for", 

344 name="{fakesType}calexp.wcs", 

345 storageClass="Wcs", 

346 dimensions=("instrument", "visit", "detector"), 

347 ) 

348 skyMap = pipeBase.connectionTypes.Input( 

349 doc="Input definition of geometry/bbox and projection/wcs for template exposures", 

350 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME, 

351 dimensions=("skymap", ), 

352 storageClass="SkyMap", 

353 ) 

354 # TODO DM-31292: Add option to use global external wcs from jointcal 

355 # Needed for DRP HSC 

356 coaddExposures = pipeBase.connectionTypes.Input( 

357 doc="Input template to match and subtract from the exposure", 

358 dimensions=("tract", "patch", "skymap", "band"), 

359 storageClass="ExposureF", 

360 name="{fakesType}{coaddName}Coadd{warpTypeSuffix}", 

361 multiple=True, 

362 deferLoad=True 

363 ) 

364 template = pipeBase.connectionTypes.Output( 

365 doc="Warped template used to create `subtractedExposure`.", 

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

367 storageClass="ExposureF", 

368 name="{fakesType}{coaddName}Diff_templateExp{warpTypeSuffix}", 

369 ) 

370 

371 

372class GetTemplateConfig(pipeBase.PipelineTaskConfig, 

373 pipelineConnections=GetTemplateConnections): 

374 templateBorderSize = pexConfig.Field( 

375 dtype=int, 

376 default=20, 

377 doc="Number of pixels to grow the requested template image to account for warping" 

378 ) 

379 warp = pexConfig.ConfigField( 

380 dtype=afwMath.Warper.ConfigClass, 

381 doc="warper configuration", 

382 ) 

383 coaddPsf = pexConfig.ConfigField( 

384 doc="Configuration for CoaddPsf", 

385 dtype=CoaddPsfConfig, 

386 ) 

387 

388 def setDefaults(self): 

389 self.warp.warpingKernelName = 'lanczos5' 

390 self.coaddPsf.warpingKernelName = 'lanczos5' 

391 

392 

393class GetTemplateTask(pipeBase.PipelineTask): 

394 ConfigClass = GetTemplateConfig 

395 _DefaultName = "getTemplate" 

396 

397 def __init__(self, *args, **kwargs): 

398 super().__init__(*args, **kwargs) 

399 self.warper = afwMath.Warper.fromConfig(self.config.warp) 

400 

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

402 # Read in all inputs. 

403 inputs = butlerQC.get(inputRefs) 

404 results = self.getOverlappingExposures(inputs) 

405 inputs["coaddExposures"] = results.coaddExposures 

406 inputs["dataIds"] = results.dataIds 

407 outputs = self.run(**inputs) 

408 butlerQC.put(outputs, outputRefs) 

409 

410 def getOverlappingExposures(self, inputs): 

411 """Return lists of coadds and their corresponding dataIds that overlap the detector. 

412 

413 The spatial index in the registry has generous padding and often supplies 

414 patches near, but not directly overlapping the detector. 

415 Filters inputs so that we don't have to read in all input coadds. 

416 

417 Parameters 

418 ---------- 

419 inputs : `dict` of task Inputs, containing: 

420 - coaddExposureRefs : list of elements of type 

421 `lsst.daf.butler.DeferredDatasetHandle` of 

422 `lsst.afw.image.Exposure` 

423 Data references to exposures that might overlap the detector. 

424 - bbox : `lsst.geom.Box2I` 

425 Template Bounding box of the detector geometry onto which to 

426 resample the coaddExposures 

427 - skyMap : `lsst.skymap.SkyMap` 

428 Input definition of geometry/bbox and projection/wcs for template exposures 

429 - wcs : `lsst.afw.geom.SkyWcs` 

430 Template WCS onto which to resample the coaddExposures 

431 

432 Returns 

433 ------- 

434 result : `lsst.pipe.base.Struct` containing these fields: 

435 - coaddExposures : `list` of elements of type `lsst.afw.image.Exposure` 

436 Coadd exposures that overlap the detector. 

437 - dataIds : `list` of `lsst.daf.butler.DataCoordinate` 

438 Data IDs of the coadd exposures that overlap the detector. 

439 

440 Raises 

441 ------ 

442 NoWorkFound 

443 Raised if no patches overlap the input detector bbox 

444 """ 

445 # Check that the patches actually overlap the detector 

446 # Exposure's validPolygon would be more accurate 

447 detectorPolygon = geom.Box2D(inputs['bbox']) 

448 overlappingArea = 0 

449 coaddExposureList = [] 

450 dataIds = [] 

451 for coaddRef in inputs['coaddExposures']: 

452 dataId = coaddRef.dataId 

453 patchWcs = inputs['skyMap'][dataId['tract']].getWcs() 

454 patchBBox = inputs['skyMap'][dataId['tract']][dataId['patch']].getOuterBBox() 

455 patchCorners = patchWcs.pixelToSky(geom.Box2D(patchBBox).getCorners()) 

456 patchPolygon = afwGeom.Polygon(inputs['wcs'].skyToPixel(patchCorners)) 

457 if patchPolygon.intersection(detectorPolygon): 

458 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea() 

459 self.log.info("Using template input tract=%s, patch=%s" % 

460 (dataId['tract'], dataId['patch'])) 

461 coaddExposureList.append(coaddRef.get()) 

462 dataIds.append(dataId) 

463 

464 if not overlappingArea: 

465 raise pipeBase.NoWorkFound('No patches overlap detector') 

466 

467 return pipeBase.Struct(coaddExposures=coaddExposureList, 

468 dataIds=dataIds) 

469 

470 def run(self, coaddExposures, bbox, wcs, dataIds, **kwargs): 

471 """Warp coadds from multiple tracts to form a template for image diff. 

472 

473 Where the tracts overlap, the resulting template image is averaged. 

474 The PSF on the template is created by combining the CoaddPsf on each 

475 template image into a meta-CoaddPsf. 

476 

477 Parameters 

478 ---------- 

479 coaddExposures : `list` of `lsst.afw.image.Exposure` 

480 Coadds to be mosaicked 

481 bbox : `lsst.geom.Box2I` 

482 Template Bounding box of the detector geometry onto which to 

483 resample the coaddExposures 

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

485 Template WCS onto which to resample the coaddExposures 

486 dataIds : `list` of `lsst.daf.butler.DataCoordinate` 

487 Record of the tract and patch of each coaddExposure. 

488 **kwargs 

489 Any additional keyword parameters. 

490 

491 Returns 

492 ------- 

493 result : `lsst.pipe.base.Struct` containing 

494 - ``template`` : a template coadd exposure assembled out of patches 

495 """ 

496 # Table for CoaddPSF 

497 tractsSchema = afwTable.ExposureTable.makeMinimalSchema() 

498 tractKey = tractsSchema.addField('tract', type=np.int32, doc='Which tract') 

499 patchKey = tractsSchema.addField('patch', type=np.int32, doc='Which patch') 

500 weightKey = tractsSchema.addField('weight', type=float, doc='Weight for each tract, should be 1') 

501 tractsCatalog = afwTable.ExposureCatalog(tractsSchema) 

502 

503 finalWcs = wcs 

504 bbox.grow(self.config.templateBorderSize) 

505 finalBBox = bbox 

506 

507 nPatchesFound = 0 

508 maskedImageList = [] 

509 weightList = [] 

510 

511 for coaddExposure, dataId in zip(coaddExposures, dataIds): 

512 

513 # warp to detector WCS 

514 warped = self.warper.warpExposure(finalWcs, coaddExposure, maxBBox=finalBBox) 

515 

516 # Check if warped image is viable 

517 if not np.any(np.isfinite(warped.image.array)): 

518 self.log.info("No overlap for warped %s. Skipping" % dataId) 

519 continue 

520 

521 exp = afwImage.ExposureF(finalBBox, finalWcs) 

522 exp.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan) 

523 exp.maskedImage.assign(warped.maskedImage, warped.getBBox()) 

524 

525 maskedImageList.append(exp.maskedImage) 

526 weightList.append(1) 

527 record = tractsCatalog.addNew() 

528 record.setPsf(coaddExposure.getPsf()) 

529 record.setWcs(coaddExposure.getWcs()) 

530 record.setPhotoCalib(coaddExposure.getPhotoCalib()) 

531 record.setBBox(coaddExposure.getBBox()) 

532 record.setValidPolygon(afwGeom.Polygon(geom.Box2D(coaddExposure.getBBox()).getCorners())) 

533 record.set(tractKey, dataId['tract']) 

534 record.set(patchKey, dataId['patch']) 

535 record.set(weightKey, 1.) 

536 nPatchesFound += 1 

537 

538 if nPatchesFound == 0: 

539 raise pipeBase.NoWorkFound("No patches found to overlap detector") 

540 

541 # Combine images from individual patches together 

542 statsFlags = afwMath.stringToStatisticsProperty('MEAN') 

543 statsCtrl = afwMath.StatisticsControl() 

544 statsCtrl.setNanSafe(True) 

545 statsCtrl.setWeighted(True) 

546 statsCtrl.setCalcErrorFromInputVariance(True) 

547 

548 templateExposure = afwImage.ExposureF(finalBBox, finalWcs) 

549 templateExposure.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan) 

550 xy0 = templateExposure.getXY0() 

551 # Do not mask any values 

552 templateExposure.maskedImage = afwMath.statisticsStack(maskedImageList, statsFlags, statsCtrl, 

553 weightList, clipped=0, maskMap=[]) 

554 templateExposure.maskedImage.setXY0(xy0) 

555 

556 # CoaddPsf centroid not only must overlap image, but must overlap the part of 

557 # image with data. Use centroid of region with data 

558 boolmask = templateExposure.mask.array & templateExposure.mask.getPlaneBitMask('NO_DATA') == 0 

559 maskx = afwImage.makeMaskFromArray(boolmask.astype(afwImage.MaskPixel)) 

560 centerCoord = afwGeom.SpanSet.fromMask(maskx, 1).computeCentroid() 

561 

562 ctrl = self.config.coaddPsf.makeControl() 

563 coaddPsf = CoaddPsf(tractsCatalog, finalWcs, centerCoord, ctrl.warpingKernelName, ctrl.cacheSize) 

564 if coaddPsf is None: 

565 raise RuntimeError("CoaddPsf could not be constructed") 

566 

567 templateExposure.setPsf(coaddPsf) 

568 templateExposure.setFilter(coaddExposure.getFilter()) 

569 templateExposure.setPhotoCalib(coaddExposure.getPhotoCalib()) 

570 return pipeBase.Struct(template=templateExposure) 

571 

572 

573class GetDcrTemplateConnections(GetTemplateConnections, 

574 dimensions=("instrument", "visit", "detector", "skymap"), 

575 defaultTemplates={"coaddName": "dcr", 

576 "warpTypeSuffix": "", 

577 "fakesType": ""}): 

578 visitInfo = pipeBase.connectionTypes.Input( 

579 doc="VisitInfo of calexp used to determine observing conditions.", 

580 name="{fakesType}calexp.visitInfo", 

581 storageClass="VisitInfo", 

582 dimensions=("instrument", "visit", "detector"), 

583 ) 

584 dcrCoadds = pipeBase.connectionTypes.Input( 

585 doc="Input DCR template to match and subtract from the exposure", 

586 name="{fakesType}dcrCoadd{warpTypeSuffix}", 

587 storageClass="ExposureF", 

588 dimensions=("tract", "patch", "skymap", "band", "subfilter"), 

589 multiple=True, 

590 deferLoad=True 

591 ) 

592 

593 def __init__(self, *, config=None): 

594 super().__init__(config=config) 

595 self.inputs.remove("coaddExposures") 

596 

597 

598class GetDcrTemplateConfig(GetTemplateConfig, 

599 pipelineConnections=GetDcrTemplateConnections): 

600 numSubfilters = pexConfig.Field( 

601 doc="Number of subfilters in the DcrCoadd.", 

602 dtype=int, 

603 default=3, 

604 ) 

605 effectiveWavelength = pexConfig.Field( 

606 doc="Effective wavelength of the filter.", 

607 optional=False, 

608 dtype=float, 

609 ) 

610 bandwidth = pexConfig.Field( 

611 doc="Bandwidth of the physical filter.", 

612 optional=False, 

613 dtype=float, 

614 ) 

615 

616 def validate(self): 

617 if self.effectiveWavelength is None or self.bandwidth is None: 

618 raise ValueError("The effective wavelength and bandwidth of the physical filter " 

619 "must be set in the getTemplate config for DCR coadds. " 

620 "Required until transmission curves are used in DM-13668.") 

621 

622 

623class GetDcrTemplateTask(GetTemplateTask): 

624 ConfigClass = GetDcrTemplateConfig 

625 _DefaultName = "getDcrTemplate" 

626 

627 def getOverlappingExposures(self, inputs): 

628 """Return lists of coadds and their corresponding dataIds that overlap the detector. 

629 

630 The spatial index in the registry has generous padding and often supplies 

631 patches near, but not directly overlapping the detector. 

632 Filters inputs so that we don't have to read in all input coadds. 

633 

634 Parameters 

635 ---------- 

636 inputs : `dict` of task Inputs, containing: 

637 - coaddExposureRefs : `list` of elements of type 

638 `lsst.daf.butler.DeferredDatasetHandle` of 

639 `lsst.afw.image.Exposure` 

640 Data references to exposures that might overlap the detector. 

641 - bbox : `lsst.geom.Box2I` 

642 Template Bounding box of the detector geometry onto which to 

643 resample the coaddExposures 

644 - skyMap : `lsst.skymap.SkyMap` 

645 Input definition of geometry/bbox and projection/wcs for template exposures 

646 - wcs : `lsst.afw.geom.SkyWcs` 

647 Template WCS onto which to resample the coaddExposures 

648 - visitInfo : `lsst.afw.image.VisitInfo` 

649 Metadata for the science image. 

650 

651 Returns 

652 ------- 

653 result : `lsst.pipe.base.Struct` containing these fields: 

654 - coaddExposures : `list` of elements of type `lsst.afw.image.Exposure` 

655 Coadd exposures that overlap the detector. 

656 - dataIds : `list` of `lsst.daf.butler.DataCoordinate` 

657 Data IDs of the coadd exposures that overlap the detector. 

658 

659 Raises 

660 ------ 

661 NoWorkFound 

662 Raised if no patches overlatp the input detector bbox 

663 """ 

664 # Check that the patches actually overlap the detector 

665 # Exposure's validPolygon would be more accurate 

666 detectorPolygon = geom.Box2D(inputs["bbox"]) 

667 overlappingArea = 0 

668 coaddExposureRefList = [] 

669 dataIds = [] 

670 patchList = dict() 

671 for coaddRef in inputs["dcrCoadds"]: 

672 dataId = coaddRef.dataId 

673 patchWcs = inputs["skyMap"][dataId['tract']].getWcs() 

674 patchBBox = inputs["skyMap"][dataId['tract']][dataId['patch']].getOuterBBox() 

675 patchCorners = patchWcs.pixelToSky(geom.Box2D(patchBBox).getCorners()) 

676 patchPolygon = afwGeom.Polygon(inputs["wcs"].skyToPixel(patchCorners)) 

677 if patchPolygon.intersection(detectorPolygon): 

678 overlappingArea += patchPolygon.intersectionSingle(detectorPolygon).calculateArea() 

679 self.log.info("Using template input tract=%s, patch=%s, subfilter=%s" % 

680 (dataId['tract'], dataId['patch'], dataId["subfilter"])) 

681 coaddExposureRefList.append(coaddRef) 

682 if dataId['tract'] in patchList: 

683 patchList[dataId['tract']].append(dataId['patch']) 

684 else: 

685 patchList[dataId['tract']] = [dataId['patch'], ] 

686 dataIds.append(dataId) 

687 

688 if not overlappingArea: 

689 raise pipeBase.NoWorkFound('No patches overlap detector') 

690 

691 self.checkPatchList(patchList) 

692 

693 coaddExposures = self.getDcrModel(patchList, inputs['dcrCoadds'], inputs['visitInfo']) 

694 return pipeBase.Struct(coaddExposures=coaddExposures, 

695 dataIds=dataIds) 

696 

697 def checkPatchList(self, patchList): 

698 """Check that all of the DcrModel subfilters are present for each patch. 

699 

700 Parameters 

701 ---------- 

702 patchList : `dict` 

703 Dict of the patches containing valid data for each tract 

704 

705 Raises 

706 ------ 

707 RuntimeError 

708 If the number of exposures found for a patch does not match the number of subfilters. 

709 """ 

710 for tract in patchList: 

711 for patch in set(patchList[tract]): 

712 if patchList[tract].count(patch) != self.config.numSubfilters: 

713 raise RuntimeError("Invalid number of DcrModel subfilters found: %d vs %d expected", 

714 patchList[tract].count(patch), self.config.numSubfilters) 

715 

716 def getDcrModel(self, patchList, coaddRefs, visitInfo): 

717 """Build DCR-matched coadds from a list of exposure references. 

718 

719 Parameters 

720 ---------- 

721 patchList : `dict` 

722 Dict of the patches containing valid data for each tract 

723 coaddRefs : `list` of elements of type 

724 `lsst.daf.butler.DeferredDatasetHandle` of 

725 `lsst.afw.image.Exposure` 

726 Data references to DcrModels that overlap the detector. 

727 visitInfo : `lsst.afw.image.VisitInfo` 

728 Metadata for the science image. 

729 

730 Returns 

731 ------- 

732 `list` of elements of type `lsst.afw.image.Exposure` 

733 Coadd exposures that overlap the detector. 

734 """ 

735 coaddExposureList = [] 

736 for tract in patchList: 

737 for patch in set(patchList[tract]): 

738 coaddRefList = [coaddRef for coaddRef in coaddRefs 

739 if _selectDataRef(coaddRef, tract, patch)] 

740 

741 dcrModel = DcrModel.fromQuantum(coaddRefList, 

742 self.config.effectiveWavelength, 

743 self.config.bandwidth, 

744 self.config.numSubfilters) 

745 coaddExposureList.append(dcrModel.buildMatchedExposure(visitInfo=visitInfo)) 

746 return coaddExposureList 

747 

748 

749def _selectDataRef(coaddRef, tract, patch): 

750 condition = (coaddRef.dataId['tract'] == tract) & (coaddRef.dataId['patch'] == patch) 

751 return condition 

752 

753 

754class GetMultiTractCoaddTemplateConfig(GetTemplateConfig): 

755 pass 

756 

757 

758class GetMultiTractCoaddTemplateTask(GetTemplateTask): 

759 ConfigClass = GetMultiTractCoaddTemplateConfig 

760 _DefaultName = "getMultiTractCoaddTemplate" 

761 

762 def __init__(self, *args, **kwargs): 

763 super().__init__(*args, **kwargs) 

764 self.log.warn("GetMultiTractCoaddTemplateTask is deprecated. Use GetTemplateTask instead.")