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

315 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-11 03:56 -0700

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

66 

67 ConfigClass = GetCoaddAsTemplateConfig 

68 _DefaultName = "GetCoaddAsTemplateTask" 

69 

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

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

72 that overlaps the science exposure. 

73 

74 Parameters 

75 ---------- 

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

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

78 coadd. 

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

80 Butler like object that supports getting data by DatasetRef. 

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

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

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

84 Iterable of references to the available template coadd patches. 

85 

86 Returns 

87 ------- 

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

89 A struct with attibutes: 

90 

91 ``exposure`` 

92 Template coadd exposure assembled out of patches 

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

94 ``sources`` 

95 Always `None` for this subtask. 

96 

97 """ 

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

99 skyMap = butlerQC.get(skyMapRef) 

100 coaddExposureRefs = butlerQC.get(coaddExposureRefs) 

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

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

103 tractInfo = skyMap[tracts[0]] 

104 else: 

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

106 "Use GetTemplateTask instead.") 

107 

108 detectorWcs = exposure.getWcs() 

109 if detectorWcs is None: 

110 templateExposure = None 

111 pixGood = 0 

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

113 else: 

114 detectorBBox = exposure.getBBox() 

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

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

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

118 

119 availableCoaddRefs = dict() 

120 overlappingArea = 0 

121 for coaddRef in coaddExposureRefs: 

122 dataId = coaddRef.dataId 

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

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

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

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

127 if patchPolygon.intersection(detectorPolygon): 

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

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

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

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

132 if dataId['patch'] in availableCoaddRefs: 

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

134 else: 

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

136 else: 

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

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

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

140 

141 if overlappingArea == 0: 

142 templateExposure = None 

143 pixGood = 0 

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

145 else: 

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

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

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

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

150 # Counting NaN pixels is insufficient because pixels without 

151 # 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 

161 science exposure. 

162 

163 Parameters 

164 ---------- 

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

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

167 coadd. 

168 

169 skyMap : `lsst.skymap.BaseSkyMap` 

170 SkyMap object that corresponds to the template coadd. 

171 

172 Returns 

173 ------- 

174 result : `tuple` of 

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

176 The selected tract. 

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

178 List of all overlap patches of the selected tract. 

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

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

181 `lsst.geom.Box2D.getCorners`. 

182 """ 

183 expWcs = exposure.getWcs() 

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

185 expBoxD.grow(self.config.templateBorderSize) 

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

187 tractInfo = skyMap.findTract(ctrSkyPos) 

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

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

190 patchList = tractInfo.findPatchList(skyCorners) 

191 

192 if not patchList: 

193 raise RuntimeError("No suitable tract found") 

194 

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

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

197 

198 return (tractInfo, patchList, skyCorners) 

199 

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

201 sensorRef=None, visitInfo=None): 

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

203 overlapping patch regions. 

204 

205 Parameters 

206 ---------- 

207 skyMap : `lsst.skymap.BaseSkyMap` 

208 SkyMap object that corresponds to the template coadd. 

209 tractInfo : `lsst.skymap.TractInfo` 

210 The selected tract. 

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

212 Patches to consider for making the template exposure. 

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

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

215 availableCoaddRefs : `dict` [`int`] 

216 Dictionary of spatially relevant retrieved coadd patches, 

217 indexed by their sequential patch number. Values are 

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

219 sensorRef : `None` 

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

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

222 VisitInfo to make dcr model. 

223 

224 Returns 

225 ------- 

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

227 The created template exposure. 

228 """ 

229 if sensorRef is not None: 

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

231 " Please move to Gen3 middleware.") 

232 coaddWcs = tractInfo.getWcs() 

233 

234 # compute coadd bbox 

235 coaddBBox = geom.Box2D() 

236 for skyPos in skyCorners: 

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

238 coaddBBox = geom.Box2I(coaddBBox) 

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

240 

241 coaddExposure = afwImage.ExposureF(coaddBBox, coaddWcs) 

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

243 nPatchesFound = 0 

244 coaddFilterLabel = None 

245 coaddPsf = None 

246 coaddPhotoCalib = None 

247 for patchInfo in patchList: 

248 patchNumber = tractInfo.getSequentialPatchIndex(patchInfo) 

249 patchSubBBox = patchInfo.getOuterBBox() 

250 patchSubBBox.clip(coaddBBox) 

251 if patchNumber not in availableCoaddRefs: 

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

253 continue 

254 if patchSubBBox.isEmpty(): 

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

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

257 else: 

258 tract = availableCoaddRefs[patchNumber]['tract'] 

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

260 continue 

261 

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

263 patchInnerBBox = patchInfo.getInnerBBox() 

264 patchInnerBBox.clip(coaddBBox) 

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

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

267 availableCoaddRefs[patchNumber]) 

268 continue 

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

270 availableCoaddRefs[patchNumber]) 

271 

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

273 self.config.effectiveWavelength, 

274 self.config.bandwidth) 

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

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

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

278 # pixels in the overlap region from previous patches. 

279 # Shrink the BBox to remove the contaminated pixels, 

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

281 dcrBBox = geom.Box2I(patchSubBBox) 

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

283 dcrBBox.include(patchInnerBBox) 

284 coaddPatch = dcrModel.buildMatchedExposure(bbox=dcrBBox, 

285 visitInfo=visitInfo) 

286 else: 

287 coaddPatch = availableCoaddRefs[patchNumber].get() 

288 

289 nPatchesFound += 1 

290 

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

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

293 overlapBox = coaddPatch.getBBox() 

294 overlapBox.clip(coaddBBox) 

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

296 

297 if coaddFilterLabel is None: 

298 coaddFilterLabel = coaddPatch.getFilter() 

299 

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

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

302 coaddPsf = coaddPatch.getPsf() 

303 

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

305 # retrieved> 

306 if coaddPhotoCalib is None: 

307 coaddPhotoCalib = coaddPatch.getPhotoCalib() 

308 

309 if coaddPhotoCalib is None: 

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

311 if nPatchesFound == 0: 

312 raise RuntimeError("No patches found!") 

313 if coaddPsf is None: 

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

315 

316 coaddExposure.setPhotoCalib(coaddPhotoCalib) 

317 coaddExposure.setPsf(coaddPsf) 

318 coaddExposure.setFilter(coaddFilterLabel) 

319 return coaddExposure 

320 

321 def getCoaddDatasetName(self): 

322 """Return coadd name for given task config 

323 

324 Returns 

325 ------- 

326 CoaddDatasetName : `string` 

327 

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

329 """ 

330 warpType = self.config.warpType 

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

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

333 

334 

335class GetTemplateConnections(pipeBase.PipelineTaskConnections, 

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

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

338 "warpTypeSuffix": "", 

339 "fakesType": ""}): 

340 bbox = pipeBase.connectionTypes.Input( 

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

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

343 storageClass="Box2I", 

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

345 ) 

346 wcs = pipeBase.connectionTypes.Input( 

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

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

349 storageClass="Wcs", 

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

351 ) 

352 skyMap = pipeBase.connectionTypes.Input( 

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

354 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME, 

355 dimensions=("skymap", ), 

356 storageClass="SkyMap", 

357 ) 

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

359 # Needed for DRP HSC 

360 coaddExposures = pipeBase.connectionTypes.Input( 

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

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

363 storageClass="ExposureF", 

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

365 multiple=True, 

366 deferLoad=True 

367 ) 

368 template = pipeBase.connectionTypes.Output( 

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

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

371 storageClass="ExposureF", 

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

373 ) 

374 

375 

376class GetTemplateConfig(pipeBase.PipelineTaskConfig, 

377 pipelineConnections=GetTemplateConnections): 

378 templateBorderSize = pexConfig.Field( 

379 dtype=int, 

380 default=20, 

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

382 ) 

383 warp = pexConfig.ConfigField( 

384 dtype=afwMath.Warper.ConfigClass, 

385 doc="warper configuration", 

386 ) 

387 coaddPsf = pexConfig.ConfigField( 

388 doc="Configuration for CoaddPsf", 

389 dtype=CoaddPsfConfig, 

390 ) 

391 

392 def setDefaults(self): 

393 self.warp.warpingKernelName = 'lanczos5' 

394 self.coaddPsf.warpingKernelName = 'lanczos5' 

395 

396 

397class GetTemplateTask(pipeBase.PipelineTask): 

398 ConfigClass = GetTemplateConfig 

399 _DefaultName = "getTemplate" 

400 

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

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

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

404 

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

406 # Read in all inputs. 

407 inputs = butlerQC.get(inputRefs) 

408 results = self.getOverlappingExposures(inputs) 

409 inputs["coaddExposures"] = results.coaddExposures 

410 inputs["dataIds"] = results.dataIds 

411 inputs["physical_filter"] = butlerQC.quantum.dataId["physical_filter"] 

412 outputs = self.run(**inputs) 

413 butlerQC.put(outputs, outputRefs) 

414 

415 def getOverlappingExposures(self, inputs): 

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

417 the detector. 

418 

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

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

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

422 

423 Parameters 

424 ---------- 

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

426 - coaddExposureRefs : `list` 

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

428 `lsst.afw.image.Exposure`] 

429 Data references to exposures that might overlap the detector. 

430 - bbox : `lsst.geom.Box2I` 

431 Template Bounding box of the detector geometry onto which to 

432 resample the coaddExposures. 

433 - skyMap : `lsst.skymap.SkyMap` 

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

435 template exposures. 

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

437 Template WCS onto which to resample the coaddExposures. 

438 

439 Returns 

440 ------- 

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

442 A struct with attributes: 

443 

444 ``coaddExposures`` 

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

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

447 ``dataIds`` 

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

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

450 

451 Raises 

452 ------ 

453 NoWorkFound 

454 Raised if no patches overlap the input detector bbox. 

455 """ 

456 # Check that the patches actually overlap the detector 

457 # Exposure's validPolygon would be more accurate 

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

459 overlappingArea = 0 

460 coaddExposureList = [] 

461 dataIds = [] 

462 for coaddRef in inputs['coaddExposures']: 

463 dataId = coaddRef.dataId 

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

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

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

467 inputsWcs = inputs['wcs'] 

468 if inputsWcs is not None: 

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

470 if patchPolygon.intersection(detectorPolygon): 

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

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

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

474 coaddExposureList.append(coaddRef.get()) 

475 dataIds.append(dataId) 

476 else: 

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

478 

479 if not overlappingArea: 

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

481 

482 return pipeBase.Struct(coaddExposures=coaddExposureList, 

483 dataIds=dataIds) 

484 

485 def run(self, coaddExposures, bbox, wcs, dataIds, physical_filter=None, **kwargs): 

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

487 

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

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

490 template image into a meta-CoaddPsf. 

491 

492 Parameters 

493 ---------- 

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

495 Coadds to be mosaicked. 

496 bbox : `lsst.geom.Box2I` 

497 Template Bounding box of the detector geometry onto which to 

498 resample the ``coaddExposures``. 

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

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

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

502 Record of the tract and patch of each coaddExposure. 

503 physical_filter : `str`, optional 

504 The physical filter of the science image. 

505 **kwargs 

506 Any additional keyword parameters. 

507 

508 Returns 

509 ------- 

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

511 A struct with attributes: 

512 

513 ``template`` 

514 A template coadd exposure assembled out of patches 

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

516 

517 Raises 

518 ------ 

519 NoWorkFound 

520 If no coadds are found with sufficient un-masked pixels. 

521 RuntimeError 

522 If the PSF of the template can't be calculated. 

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 # Coadds do not have a physical filter, so fetch it from the butler to prevent downstream warnings. 

597 if physical_filter is None: 

598 filterLabel = coaddExposure.getFilter() 

599 else: 

600 filterLabel = afwImage.FilterLabel(dataId['band'], physical_filter) 

601 templateExposure.setFilter(filterLabel) 

602 templateExposure.setPhotoCalib(coaddExposure.getPhotoCalib()) 

603 return pipeBase.Struct(template=templateExposure) 

604 

605 

606class GetDcrTemplateConnections(GetTemplateConnections, 

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

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

609 "warpTypeSuffix": "", 

610 "fakesType": ""}): 

611 visitInfo = pipeBase.connectionTypes.Input( 

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

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

614 storageClass="VisitInfo", 

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

616 ) 

617 dcrCoadds = pipeBase.connectionTypes.Input( 

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

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

620 storageClass="ExposureF", 

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

622 multiple=True, 

623 deferLoad=True 

624 ) 

625 

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

627 super().__init__(config=config) 

628 self.inputs.remove("coaddExposures") 

629 

630 

631class GetDcrTemplateConfig(GetTemplateConfig, 

632 pipelineConnections=GetDcrTemplateConnections): 

633 numSubfilters = pexConfig.Field( 

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

635 dtype=int, 

636 default=3, 

637 ) 

638 effectiveWavelength = pexConfig.Field( 

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

640 optional=False, 

641 dtype=float, 

642 ) 

643 bandwidth = pexConfig.Field( 

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

645 optional=False, 

646 dtype=float, 

647 ) 

648 

649 def validate(self): 

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

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

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

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

654 

655 

656class GetDcrTemplateTask(GetTemplateTask): 

657 ConfigClass = GetDcrTemplateConfig 

658 _DefaultName = "getDcrTemplate" 

659 

660 def getOverlappingExposures(self, inputs): 

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

662 the detector. 

663 

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

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

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

667 

668 Parameters 

669 ---------- 

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

671 - coaddExposureRefs : `list` 

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

673 `lsst.afw.image.Exposure`] 

674 Data references to exposures that might overlap the detector. 

675 - bbox : `lsst.geom.Box2I` 

676 Template Bounding box of the detector geometry onto which to 

677 resample the coaddExposures. 

678 - skyMap : `lsst.skymap.SkyMap` 

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

680 template exposures. 

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

682 Template WCS onto which to resample the coaddExposures. 

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

684 Metadata for the science image. 

685 

686 Returns 

687 ------- 

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

689 A struct with attibutes: 

690 

691 ``coaddExposures`` 

692 Coadd exposures that overlap the detector (`list` 

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

694 ``dataIds`` 

695 Data IDs of the coadd exposures that overlap the detector 

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

697 

698 Raises 

699 ------ 

700 NoWorkFound 

701 Raised if no patches overlatp the input detector bbox. 

702 """ 

703 # Check that the patches actually overlap the detector 

704 # Exposure's validPolygon would be more accurate 

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

706 overlappingArea = 0 

707 coaddExposureRefList = [] 

708 dataIds = [] 

709 patchList = dict() 

710 for coaddRef in inputs["dcrCoadds"]: 

711 dataId = coaddRef.dataId 

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

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

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

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

716 if patchPolygon.intersection(detectorPolygon): 

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

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

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

720 coaddExposureRefList.append(coaddRef) 

721 if dataId['tract'] in patchList: 

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

723 else: 

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

725 dataIds.append(dataId) 

726 

727 if not overlappingArea: 

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

729 

730 self.checkPatchList(patchList) 

731 

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

733 return pipeBase.Struct(coaddExposures=coaddExposures, 

734 dataIds=dataIds) 

735 

736 def checkPatchList(self, patchList): 

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

738 patch. 

739 

740 Parameters 

741 ---------- 

742 patchList : `dict` 

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

744 

745 Raises 

746 ------ 

747 RuntimeError 

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

749 number of subfilters. 

750 """ 

751 for tract in patchList: 

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

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

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

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

756 

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

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

759 

760 Parameters 

761 ---------- 

762 patchList : `dict` 

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

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

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

766 DcrModels that overlap the detector. 

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

768 Metadata for the science image. 

769 

770 Returns 

771 ------- 

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

773 Coadd exposures that overlap the detector. 

774 """ 

775 coaddExposureList = [] 

776 for tract in patchList: 

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

778 coaddRefList = [coaddRef for coaddRef in coaddRefs 

779 if _selectDataRef(coaddRef, tract, patch)] 

780 

781 dcrModel = DcrModel.fromQuantum(coaddRefList, 

782 self.config.effectiveWavelength, 

783 self.config.bandwidth, 

784 self.config.numSubfilters) 

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

786 return coaddExposureList 

787 

788 

789def _selectDataRef(coaddRef, tract, patch): 

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

791 return condition 

792 

793 

794class GetMultiTractCoaddTemplateConfig(GetTemplateConfig): 

795 pass 

796 

797 

798class GetMultiTractCoaddTemplateTask(GetTemplateTask): 

799 ConfigClass = GetMultiTractCoaddTemplateConfig 

800 _DefaultName = "getMultiTractCoaddTemplate" 

801 

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

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

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