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

311 statements  

« prev     ^ index     » next       coverage.py v7.0.0, created at 2022-12-20 10:18 +0000

1# This file is part of ip_diffim. 

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/>. 

21import numpy as np 

22 

23import lsst.afw.image as afwImage 

24import lsst.geom as geom 

25import lsst.afw.geom as afwGeom 

26import lsst.afw.table as afwTable 

27import lsst.afw.math as afwMath 

28import lsst.pex.config as pexConfig 

29import lsst.pipe.base as pipeBase 

30from lsst.skymap import BaseSkyMap 

31from lsst.daf.butler import DeferredDatasetHandle 

32from lsst.ip.diffim.dcrModel import DcrModel 

33from lsst.meas.algorithms import CoaddPsf, CoaddPsfConfig 

34 

35__all__ = ["GetCoaddAsTemplateTask", "GetCoaddAsTemplateConfig", 

36 "GetTemplateTask", "GetTemplateConfig", 

37 "GetDcrTemplateTask", "GetDcrTemplateConfig", 

38 "GetMultiTractCoaddTemplateTask", "GetMultiTractCoaddTemplateConfig"] 

39 

40 

41class GetCoaddAsTemplateConfig(pexConfig.Config): 

42 templateBorderSize = pexConfig.Field( 

43 dtype=int, 

44 default=20, 

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

46 ) 

47 coaddName = pexConfig.Field( 

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

49 dtype=str, 

50 default="deep", 

51 ) 

52 warpType = pexConfig.Field( 

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

54 dtype=str, 

55 default="direct", 

56 ) 

57 

58 

59class GetCoaddAsTemplateTask(pipeBase.Task): 

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

61 

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

63 ``pipe.tasks.ImageDifferenceTask``. 

64 

65 Notes 

66 ----- 

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

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

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

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

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

72 

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

74 value and ``NO_DATA`` flagged. 

75 """ 

76 

77 ConfigClass = GetCoaddAsTemplateConfig 

78 _DefaultName = "GetCoaddAsTemplateTask" 

79 

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

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

82 that overlaps the science exposure. 

83 

84 Parameters 

85 ---------- 

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

87 The science exposure to define the sky region of the template 

88 coadd. 

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

90 Butler like object that supports getting data by DatasetRef. 

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

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

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

94 Iterable of references to the available template coadd patches. 

95 

96 Returns 

97 ------- 

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

99 A struct with attibutes: 

100 

101 ``exposure`` 

102 Template coadd exposure assembled out of patches 

103 (`lsst.afw.image.ExposureF`). 

104 ``sources`` 

105 Always `None` for this subtask. 

106 

107 """ 

108 self.log.warning("GetCoaddAsTemplateTask is deprecated. Use GetTemplateTask instead.") 

109 skyMap = butlerQC.get(skyMapRef) 

110 coaddExposureRefs = butlerQC.get(coaddExposureRefs) 

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

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

113 tractInfo = skyMap[tracts[0]] 

114 else: 

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

116 "Use GetTemplateTask instead.") 

117 

118 detectorWcs = exposure.getWcs() 

119 if detectorWcs is None: 

120 templateExposure = None 

121 pixGood = 0 

122 self.log.info("Exposure has no WCS, so cannot create associated template.") 

123 else: 

124 detectorBBox = exposure.getBBox() 

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

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

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

128 

129 availableCoaddRefs = dict() 

130 overlappingArea = 0 

131 for coaddRef in coaddExposureRefs: 

132 dataId = coaddRef.dataId 

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

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

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

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

137 if patchPolygon.intersection(detectorPolygon): 

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

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

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

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

142 if dataId['patch'] in availableCoaddRefs: 

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

144 else: 

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

146 else: 

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

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

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

150 

151 if overlappingArea == 0: 

152 templateExposure = None 

153 pixGood = 0 

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

155 else: 

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

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

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

159 # Count the number of pixels with the NO_DATA mask bit set. 

160 # Counting NaN pixels is insufficient because pixels without 

161 # data are often intepolated over. 

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

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

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

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

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

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

168 

169 def getOverlapPatchList(self, exposure, skyMap): 

170 """Select the relevant tract and its patches that overlap with the 

171 science exposure. 

172 

173 Parameters 

174 ---------- 

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

176 The science exposure to define the sky region of the template 

177 coadd. 

178 

179 skyMap : `lsst.skymap.BaseSkyMap` 

180 SkyMap object that corresponds to the template coadd. 

181 

182 Returns 

183 ------- 

184 result : `tuple` of 

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

186 The selected tract. 

187 - ``patchList`` : `list` [`lsst.skymap.PatchInfo`] 

188 List of all overlap patches of the selected tract. 

189 - ``skyCorners`` : `list` [`lsst.geom.SpherePoint`] 

190 Corners of the exposure in the sky in the order given by 

191 `lsst.geom.Box2D.getCorners`. 

192 """ 

193 expWcs = exposure.getWcs() 

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

195 expBoxD.grow(self.config.templateBorderSize) 

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

197 tractInfo = skyMap.findTract(ctrSkyPos) 

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

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

200 patchList = tractInfo.findPatchList(skyCorners) 

201 

202 if not patchList: 

203 raise RuntimeError("No suitable tract found") 

204 

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

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

207 

208 return (tractInfo, patchList, skyCorners) 

209 

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

211 sensorRef=None, visitInfo=None): 

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

213 overlapping patch regions. 

214 

215 Parameters 

216 ---------- 

217 skyMap : `lsst.skymap.BaseSkyMap` 

218 SkyMap object that corresponds to the template coadd. 

219 tractInfo : `lsst.skymap.TractInfo` 

220 The selected tract. 

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

222 Patches to consider for making the template exposure. 

223 skyCorners : `list` [`lsst.geom.SpherePoint`] 

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

225 availableCoaddRefs : `dict` [`int`] 

226 Dictionary of spatially relevant retrieved coadd patches, 

227 indexed by their sequential patch number. Values are 

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

229 sensorRef : `None` 

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

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

232 VisitInfo to make dcr model. 

233 

234 Returns 

235 ------- 

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

237 The created template exposure. 

238 """ 

239 if sensorRef is not None: 

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

241 " Please move to Gen3 middleware.") 

242 coaddWcs = tractInfo.getWcs() 

243 

244 # compute coadd bbox 

245 coaddBBox = geom.Box2D() 

246 for skyPos in skyCorners: 

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

248 coaddBBox = geom.Box2I(coaddBBox) 

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

250 

251 coaddExposure = afwImage.ExposureF(coaddBBox, coaddWcs) 

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

253 nPatchesFound = 0 

254 coaddFilterLabel = None 

255 coaddPsf = None 

256 coaddPhotoCalib = None 

257 for patchInfo in patchList: 

258 patchNumber = tractInfo.getSequentialPatchIndex(patchInfo) 

259 patchSubBBox = patchInfo.getOuterBBox() 

260 patchSubBBox.clip(coaddBBox) 

261 if patchNumber not in availableCoaddRefs: 

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

263 continue 

264 if patchSubBBox.isEmpty(): 

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

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

267 else: 

268 tract = availableCoaddRefs[patchNumber]['tract'] 

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

270 continue 

271 

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

273 patchInnerBBox = patchInfo.getInnerBBox() 

274 patchInnerBBox.clip(coaddBBox) 

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

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

277 availableCoaddRefs[patchNumber]) 

278 continue 

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

280 availableCoaddRefs[patchNumber]) 

281 

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

283 self.config.effectiveWavelength, 

284 self.config.bandwidth) 

285 # The edge pixels of the DcrCoadd may contain artifacts due to 

286 # missing data. Each patch has significant overlap, and the 

287 # contaminated edge pixels in a new patch will overwrite good 

288 # pixels in the overlap region from previous patches. 

289 # Shrink the BBox to remove the contaminated pixels, 

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

291 dcrBBox = geom.Box2I(patchSubBBox) 

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

293 dcrBBox.include(patchInnerBBox) 

294 coaddPatch = dcrModel.buildMatchedExposure(bbox=dcrBBox, 

295 visitInfo=visitInfo) 

296 else: 

297 coaddPatch = availableCoaddRefs[patchNumber].get() 

298 

299 nPatchesFound += 1 

300 

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

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

303 overlapBox = coaddPatch.getBBox() 

304 overlapBox.clip(coaddBBox) 

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

306 

307 if coaddFilterLabel is None: 

308 coaddFilterLabel = coaddPatch.getFilter() 

309 

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

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

312 coaddPsf = coaddPatch.getPsf() 

313 

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

315 # retrieved> 

316 if coaddPhotoCalib is None: 

317 coaddPhotoCalib = coaddPatch.getPhotoCalib() 

318 

319 if coaddPhotoCalib is None: 

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

321 if nPatchesFound == 0: 

322 raise RuntimeError("No patches found!") 

323 if coaddPsf is None: 

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

325 

326 coaddExposure.setPhotoCalib(coaddPhotoCalib) 

327 coaddExposure.setPsf(coaddPsf) 

328 coaddExposure.setFilter(coaddFilterLabel) 

329 return coaddExposure 

330 

331 def getCoaddDatasetName(self): 

332 """Return coadd name for given task config 

333 

334 Returns 

335 ------- 

336 CoaddDatasetName : `string` 

337 

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

339 """ 

340 warpType = self.config.warpType 

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

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

343 

344 

345class GetTemplateConnections(pipeBase.PipelineTaskConnections, 

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

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

348 "warpTypeSuffix": "", 

349 "fakesType": ""}): 

350 bbox = pipeBase.connectionTypes.Input( 

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

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

353 storageClass="Box2I", 

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

355 ) 

356 wcs = pipeBase.connectionTypes.Input( 

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

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

359 storageClass="Wcs", 

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

361 ) 

362 skyMap = pipeBase.connectionTypes.Input( 

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

364 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME, 

365 dimensions=("skymap", ), 

366 storageClass="SkyMap", 

367 ) 

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

369 # Needed for DRP HSC 

370 coaddExposures = pipeBase.connectionTypes.Input( 

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

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

373 storageClass="ExposureF", 

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

375 multiple=True, 

376 deferLoad=True 

377 ) 

378 template = pipeBase.connectionTypes.Output( 

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

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

381 storageClass="ExposureF", 

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

383 ) 

384 

385 

386class GetTemplateConfig(pipeBase.PipelineTaskConfig, 

387 pipelineConnections=GetTemplateConnections): 

388 templateBorderSize = pexConfig.Field( 

389 dtype=int, 

390 default=20, 

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

392 ) 

393 warp = pexConfig.ConfigField( 

394 dtype=afwMath.Warper.ConfigClass, 

395 doc="warper configuration", 

396 ) 

397 coaddPsf = pexConfig.ConfigField( 

398 doc="Configuration for CoaddPsf", 

399 dtype=CoaddPsfConfig, 

400 ) 

401 

402 def setDefaults(self): 

403 self.warp.warpingKernelName = 'lanczos5' 

404 self.coaddPsf.warpingKernelName = 'lanczos5' 

405 

406 

407class GetTemplateTask(pipeBase.PipelineTask): 

408 ConfigClass = GetTemplateConfig 

409 _DefaultName = "getTemplate" 

410 

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

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

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

414 

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

416 # Read in all inputs. 

417 inputs = butlerQC.get(inputRefs) 

418 results = self.getOverlappingExposures(inputs) 

419 inputs["coaddExposures"] = results.coaddExposures 

420 inputs["dataIds"] = results.dataIds 

421 outputs = self.run(**inputs) 

422 butlerQC.put(outputs, outputRefs) 

423 

424 def getOverlappingExposures(self, inputs): 

425 """Return lists of coadds and their corresponding dataIds that overlap 

426 the detector. 

427 

428 The spatial index in the registry has generous padding and often 

429 supplies patches near, but not directly overlapping the detector. 

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

431 

432 Parameters 

433 ---------- 

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

435 - coaddExposureRefs : `list` 

436 [`lsst.daf.butler.DeferredDatasetHandle` of 

437 `lsst.afw.image.Exposure`] 

438 Data references to exposures that might overlap the detector. 

439 - bbox : `lsst.geom.Box2I` 

440 Template Bounding box of the detector geometry onto which to 

441 resample the coaddExposures. 

442 - skyMap : `lsst.skymap.SkyMap` 

443 Input definition of geometry/bbox and projection/wcs for 

444 template exposures. 

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

446 Template WCS onto which to resample the coaddExposures. 

447 

448 Returns 

449 ------- 

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

451 A struct with attributes: 

452 

453 ``coaddExposures`` 

454 List of Coadd exposures that overlap the detector (`list` 

455 [`lsst.afw.image.Exposure`]). 

456 ``dataIds`` 

457 List of data IDs of the coadd exposures that overlap the 

458 detector (`list` [`lsst.daf.butler.DataCoordinate`]). 

459 

460 Raises 

461 ------ 

462 NoWorkFound 

463 Raised if no patches overlap the input detector bbox. 

464 """ 

465 # Check that the patches actually overlap the detector 

466 # Exposure's validPolygon would be more accurate 

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

468 overlappingArea = 0 

469 coaddExposureList = [] 

470 dataIds = [] 

471 for coaddRef in inputs['coaddExposures']: 

472 dataId = coaddRef.dataId 

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

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

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

476 inputsWcs = inputs['wcs'] 

477 if inputsWcs is not None: 

478 patchPolygon = afwGeom.Polygon(inputsWcs.skyToPixel(patchCorners)) 

479 if patchPolygon.intersection(detectorPolygon): 

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

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

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

483 coaddExposureList.append(coaddRef.get()) 

484 dataIds.append(dataId) 

485 else: 

486 self.log.info("Exposure has no WCS, so cannot create associated template.") 

487 

488 if not overlappingArea: 

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

490 

491 return pipeBase.Struct(coaddExposures=coaddExposureList, 

492 dataIds=dataIds) 

493 

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

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

496 

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

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

499 template image into a meta-CoaddPsf. 

500 

501 Parameters 

502 ---------- 

503 coaddExposures : `list` [`lsst.afw.image.Exposure`] 

504 Coadds to be mosaicked. 

505 bbox : `lsst.geom.Box2I` 

506 Template Bounding box of the detector geometry onto which to 

507 resample the ``coaddExposures``. 

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

509 Template WCS onto which to resample the ``coaddExposures``. 

510 dataIds : `list` [`lsst.daf.butler.DataCoordinate`] 

511 Record of the tract and patch of each coaddExposure. 

512 **kwargs 

513 Any additional keyword parameters. 

514 

515 Returns 

516 ------- 

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

518 A struct with attributes: 

519 

520 ``template`` 

521 A template coadd exposure assembled out of patches 

522 (`lsst.afw.image.ExposureF`). 

523 """ 

524 # Table for CoaddPSF 

525 tractsSchema = afwTable.ExposureTable.makeMinimalSchema() 

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

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

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

529 tractsCatalog = afwTable.ExposureCatalog(tractsSchema) 

530 

531 finalWcs = wcs 

532 bbox.grow(self.config.templateBorderSize) 

533 finalBBox = bbox 

534 

535 nPatchesFound = 0 

536 maskedImageList = [] 

537 weightList = [] 

538 

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

540 

541 # warp to detector WCS 

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

543 

544 # Check if warped image is viable 

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

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

547 continue 

548 

549 exp = afwImage.ExposureF(finalBBox, finalWcs) 

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

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

552 

553 maskedImageList.append(exp.maskedImage) 

554 weightList.append(1) 

555 record = tractsCatalog.addNew() 

556 record.setPsf(coaddExposure.getPsf()) 

557 record.setWcs(coaddExposure.getWcs()) 

558 record.setPhotoCalib(coaddExposure.getPhotoCalib()) 

559 record.setBBox(coaddExposure.getBBox()) 

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

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

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

563 record.set(weightKey, 1.) 

564 nPatchesFound += 1 

565 

566 if nPatchesFound == 0: 

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

568 

569 # Combine images from individual patches together 

570 statsFlags = afwMath.stringToStatisticsProperty('MEAN') 

571 statsCtrl = afwMath.StatisticsControl() 

572 statsCtrl.setNanSafe(True) 

573 statsCtrl.setWeighted(True) 

574 statsCtrl.setCalcErrorMosaicMode(True) 

575 

576 templateExposure = afwImage.ExposureF(finalBBox, finalWcs) 

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

578 xy0 = templateExposure.getXY0() 

579 # Do not mask any values 

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

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

582 templateExposure.maskedImage.setXY0(xy0) 

583 

584 # CoaddPsf centroid not only must overlap image, but must overlap the 

585 # part of image with data. Use centroid of region with data. 

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

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

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

589 

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

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

592 if coaddPsf is None: 

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

594 

595 templateExposure.setPsf(coaddPsf) 

596 templateExposure.setFilter(coaddExposure.getFilter()) 

597 templateExposure.setPhotoCalib(coaddExposure.getPhotoCalib()) 

598 return pipeBase.Struct(template=templateExposure) 

599 

600 

601class GetDcrTemplateConnections(GetTemplateConnections, 

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

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

604 "warpTypeSuffix": "", 

605 "fakesType": ""}): 

606 visitInfo = pipeBase.connectionTypes.Input( 

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

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

609 storageClass="VisitInfo", 

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

611 ) 

612 dcrCoadds = pipeBase.connectionTypes.Input( 

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

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

615 storageClass="ExposureF", 

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

617 multiple=True, 

618 deferLoad=True 

619 ) 

620 

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

622 super().__init__(config=config) 

623 self.inputs.remove("coaddExposures") 

624 

625 

626class GetDcrTemplateConfig(GetTemplateConfig, 

627 pipelineConnections=GetDcrTemplateConnections): 

628 numSubfilters = pexConfig.Field( 

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

630 dtype=int, 

631 default=3, 

632 ) 

633 effectiveWavelength = pexConfig.Field( 

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

635 optional=False, 

636 dtype=float, 

637 ) 

638 bandwidth = pexConfig.Field( 

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

640 optional=False, 

641 dtype=float, 

642 ) 

643 

644 def validate(self): 

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

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

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

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

649 

650 

651class GetDcrTemplateTask(GetTemplateTask): 

652 ConfigClass = GetDcrTemplateConfig 

653 _DefaultName = "getDcrTemplate" 

654 

655 def getOverlappingExposures(self, inputs): 

656 """Return lists of coadds and their corresponding dataIds that overlap 

657 the detector. 

658 

659 The spatial index in the registry has generous padding and often 

660 supplies patches near, but not directly overlapping the detector. 

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

662 

663 Parameters 

664 ---------- 

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

666 - coaddExposureRefs : `list` 

667 [`lsst.daf.butler.DeferredDatasetHandle` of 

668 `lsst.afw.image.Exposure`] 

669 Data references to exposures that might overlap the detector. 

670 - bbox : `lsst.geom.Box2I` 

671 Template Bounding box of the detector geometry onto which to 

672 resample the coaddExposures. 

673 - skyMap : `lsst.skymap.SkyMap` 

674 Input definition of geometry/bbox and projection/wcs for 

675 template exposures. 

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

677 Template WCS onto which to resample the coaddExposures. 

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

679 Metadata for the science image. 

680 

681 Returns 

682 ------- 

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

684 A struct with attibutes: 

685 

686 ``coaddExposures`` 

687 Coadd exposures that overlap the detector (`list` 

688 [`lsst.afw.image.Exposure`]). 

689 ``dataIds`` 

690 Data IDs of the coadd exposures that overlap the detector 

691 (`list` [`lsst.daf.butler.DataCoordinate`]). 

692 

693 Raises 

694 ------ 

695 NoWorkFound 

696 Raised if no patches overlatp the input detector bbox. 

697 """ 

698 # Check that the patches actually overlap the detector 

699 # Exposure's validPolygon would be more accurate 

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

701 overlappingArea = 0 

702 coaddExposureRefList = [] 

703 dataIds = [] 

704 patchList = dict() 

705 for coaddRef in inputs["dcrCoadds"]: 

706 dataId = coaddRef.dataId 

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

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

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

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

711 if patchPolygon.intersection(detectorPolygon): 

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

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

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

715 coaddExposureRefList.append(coaddRef) 

716 if dataId['tract'] in patchList: 

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

718 else: 

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

720 dataIds.append(dataId) 

721 

722 if not overlappingArea: 

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

724 

725 self.checkPatchList(patchList) 

726 

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

728 return pipeBase.Struct(coaddExposures=coaddExposures, 

729 dataIds=dataIds) 

730 

731 def checkPatchList(self, patchList): 

732 """Check that all of the DcrModel subfilters are present for each 

733 patch. 

734 

735 Parameters 

736 ---------- 

737 patchList : `dict` 

738 Dict of the patches containing valid data for each tract. 

739 

740 Raises 

741 ------ 

742 RuntimeError 

743 If the number of exposures found for a patch does not match the 

744 number of subfilters. 

745 """ 

746 for tract in patchList: 

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

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

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

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

751 

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

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

754 

755 Parameters 

756 ---------- 

757 patchList : `dict` 

758 Dict of the patches containing valid data for each tract. 

759 coaddRefs : `list` [`lsst.daf.butler.DeferredDatasetHandle`] 

760 Data references to `~lsst.afw.image.Exposure` representing 

761 DcrModels that overlap the detector. 

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

763 Metadata for the science image. 

764 

765 Returns 

766 ------- 

767 coaddExposureList : `list` [`lsst.afw.image.Exposure`] 

768 Coadd exposures that overlap the detector. 

769 """ 

770 coaddExposureList = [] 

771 for tract in patchList: 

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

773 coaddRefList = [coaddRef for coaddRef in coaddRefs 

774 if _selectDataRef(coaddRef, tract, patch)] 

775 

776 dcrModel = DcrModel.fromQuantum(coaddRefList, 

777 self.config.effectiveWavelength, 

778 self.config.bandwidth, 

779 self.config.numSubfilters) 

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

781 return coaddExposureList 

782 

783 

784def _selectDataRef(coaddRef, tract, patch): 

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

786 return condition 

787 

788 

789class GetMultiTractCoaddTemplateConfig(GetTemplateConfig): 

790 pass 

791 

792 

793class GetMultiTractCoaddTemplateTask(GetTemplateTask): 

794 ConfigClass = GetMultiTractCoaddTemplateConfig 

795 _DefaultName = "getMultiTractCoaddTemplate" 

796 

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

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

799 self.log.warning("GetMultiTractCoaddTemplateTask is deprecated. Use GetTemplateTask instead.")