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

311 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-04-28 04:48 -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 outputs = self.run(**inputs) 

412 butlerQC.put(outputs, outputRefs) 

413 

414 def getOverlappingExposures(self, inputs): 

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

416 the detector. 

417 

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

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

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

421 

422 Parameters 

423 ---------- 

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

425 - coaddExposureRefs : `list` 

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

427 `lsst.afw.image.Exposure`] 

428 Data references to exposures that might overlap the detector. 

429 - bbox : `lsst.geom.Box2I` 

430 Template Bounding box of the detector geometry onto which to 

431 resample the coaddExposures. 

432 - skyMap : `lsst.skymap.SkyMap` 

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

434 template exposures. 

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

436 Template WCS onto which to resample the coaddExposures. 

437 

438 Returns 

439 ------- 

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

441 A struct with attributes: 

442 

443 ``coaddExposures`` 

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

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

446 ``dataIds`` 

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

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

449 

450 Raises 

451 ------ 

452 NoWorkFound 

453 Raised if no patches overlap the input detector bbox. 

454 """ 

455 # Check that the patches actually overlap the detector 

456 # Exposure's validPolygon would be more accurate 

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

458 overlappingArea = 0 

459 coaddExposureList = [] 

460 dataIds = [] 

461 for coaddRef in inputs['coaddExposures']: 

462 dataId = coaddRef.dataId 

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

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

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

466 inputsWcs = inputs['wcs'] 

467 if inputsWcs is not None: 

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

469 if patchPolygon.intersection(detectorPolygon): 

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

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

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

473 coaddExposureList.append(coaddRef.get()) 

474 dataIds.append(dataId) 

475 else: 

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

477 

478 if not overlappingArea: 

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

480 

481 return pipeBase.Struct(coaddExposures=coaddExposureList, 

482 dataIds=dataIds) 

483 

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

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

486 

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

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

489 template image into a meta-CoaddPsf. 

490 

491 Parameters 

492 ---------- 

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

494 Coadds to be mosaicked. 

495 bbox : `lsst.geom.Box2I` 

496 Template Bounding box of the detector geometry onto which to 

497 resample the ``coaddExposures``. 

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

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

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

501 Record of the tract and patch of each coaddExposure. 

502 **kwargs 

503 Any additional keyword parameters. 

504 

505 Returns 

506 ------- 

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

508 A struct with attributes: 

509 

510 ``template`` 

511 A template coadd exposure assembled out of patches 

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

513 """ 

514 # Table for CoaddPSF 

515 tractsSchema = afwTable.ExposureTable.makeMinimalSchema() 

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

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

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

519 tractsCatalog = afwTable.ExposureCatalog(tractsSchema) 

520 

521 finalWcs = wcs 

522 bbox.grow(self.config.templateBorderSize) 

523 finalBBox = bbox 

524 

525 nPatchesFound = 0 

526 maskedImageList = [] 

527 weightList = [] 

528 

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

530 

531 # warp to detector WCS 

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

533 

534 # Check if warped image is viable 

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

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

537 continue 

538 

539 exp = afwImage.ExposureF(finalBBox, finalWcs) 

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

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

542 

543 maskedImageList.append(exp.maskedImage) 

544 weightList.append(1) 

545 record = tractsCatalog.addNew() 

546 record.setPsf(coaddExposure.getPsf()) 

547 record.setWcs(coaddExposure.getWcs()) 

548 record.setPhotoCalib(coaddExposure.getPhotoCalib()) 

549 record.setBBox(coaddExposure.getBBox()) 

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

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

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

553 record.set(weightKey, 1.) 

554 nPatchesFound += 1 

555 

556 if nPatchesFound == 0: 

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

558 

559 # Combine images from individual patches together 

560 statsFlags = afwMath.stringToStatisticsProperty('MEAN') 

561 statsCtrl = afwMath.StatisticsControl() 

562 statsCtrl.setNanSafe(True) 

563 statsCtrl.setWeighted(True) 

564 statsCtrl.setCalcErrorMosaicMode(True) 

565 

566 templateExposure = afwImage.ExposureF(finalBBox, finalWcs) 

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

568 xy0 = templateExposure.getXY0() 

569 # Do not mask any values 

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

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

572 templateExposure.maskedImage.setXY0(xy0) 

573 

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

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

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

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

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

579 

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

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

582 if coaddPsf is None: 

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

584 

585 templateExposure.setPsf(coaddPsf) 

586 templateExposure.setFilter(coaddExposure.getFilter()) 

587 templateExposure.setPhotoCalib(coaddExposure.getPhotoCalib()) 

588 return pipeBase.Struct(template=templateExposure) 

589 

590 

591class GetDcrTemplateConnections(GetTemplateConnections, 

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

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

594 "warpTypeSuffix": "", 

595 "fakesType": ""}): 

596 visitInfo = pipeBase.connectionTypes.Input( 

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

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

599 storageClass="VisitInfo", 

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

601 ) 

602 dcrCoadds = pipeBase.connectionTypes.Input( 

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

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

605 storageClass="ExposureF", 

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

607 multiple=True, 

608 deferLoad=True 

609 ) 

610 

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

612 super().__init__(config=config) 

613 self.inputs.remove("coaddExposures") 

614 

615 

616class GetDcrTemplateConfig(GetTemplateConfig, 

617 pipelineConnections=GetDcrTemplateConnections): 

618 numSubfilters = pexConfig.Field( 

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

620 dtype=int, 

621 default=3, 

622 ) 

623 effectiveWavelength = pexConfig.Field( 

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

625 optional=False, 

626 dtype=float, 

627 ) 

628 bandwidth = pexConfig.Field( 

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

630 optional=False, 

631 dtype=float, 

632 ) 

633 

634 def validate(self): 

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

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

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

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

639 

640 

641class GetDcrTemplateTask(GetTemplateTask): 

642 ConfigClass = GetDcrTemplateConfig 

643 _DefaultName = "getDcrTemplate" 

644 

645 def getOverlappingExposures(self, inputs): 

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

647 the detector. 

648 

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

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

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

652 

653 Parameters 

654 ---------- 

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

656 - coaddExposureRefs : `list` 

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

658 `lsst.afw.image.Exposure`] 

659 Data references to exposures that might overlap the detector. 

660 - bbox : `lsst.geom.Box2I` 

661 Template Bounding box of the detector geometry onto which to 

662 resample the coaddExposures. 

663 - skyMap : `lsst.skymap.SkyMap` 

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

665 template exposures. 

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

667 Template WCS onto which to resample the coaddExposures. 

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

669 Metadata for the science image. 

670 

671 Returns 

672 ------- 

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

674 A struct with attibutes: 

675 

676 ``coaddExposures`` 

677 Coadd exposures that overlap the detector (`list` 

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

679 ``dataIds`` 

680 Data IDs of the coadd exposures that overlap the detector 

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

682 

683 Raises 

684 ------ 

685 NoWorkFound 

686 Raised if no patches overlatp the input detector bbox. 

687 """ 

688 # Check that the patches actually overlap the detector 

689 # Exposure's validPolygon would be more accurate 

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

691 overlappingArea = 0 

692 coaddExposureRefList = [] 

693 dataIds = [] 

694 patchList = dict() 

695 for coaddRef in inputs["dcrCoadds"]: 

696 dataId = coaddRef.dataId 

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

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

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

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

701 if patchPolygon.intersection(detectorPolygon): 

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

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

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

705 coaddExposureRefList.append(coaddRef) 

706 if dataId['tract'] in patchList: 

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

708 else: 

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

710 dataIds.append(dataId) 

711 

712 if not overlappingArea: 

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

714 

715 self.checkPatchList(patchList) 

716 

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

718 return pipeBase.Struct(coaddExposures=coaddExposures, 

719 dataIds=dataIds) 

720 

721 def checkPatchList(self, patchList): 

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

723 patch. 

724 

725 Parameters 

726 ---------- 

727 patchList : `dict` 

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

729 

730 Raises 

731 ------ 

732 RuntimeError 

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

734 number of subfilters. 

735 """ 

736 for tract in patchList: 

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

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

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

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

741 

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

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

744 

745 Parameters 

746 ---------- 

747 patchList : `dict` 

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

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

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

751 DcrModels that overlap the detector. 

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

753 Metadata for the science image. 

754 

755 Returns 

756 ------- 

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

758 Coadd exposures that overlap the detector. 

759 """ 

760 coaddExposureList = [] 

761 for tract in patchList: 

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

763 coaddRefList = [coaddRef for coaddRef in coaddRefs 

764 if _selectDataRef(coaddRef, tract, patch)] 

765 

766 dcrModel = DcrModel.fromQuantum(coaddRefList, 

767 self.config.effectiveWavelength, 

768 self.config.bandwidth, 

769 self.config.numSubfilters) 

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

771 return coaddExposureList 

772 

773 

774def _selectDataRef(coaddRef, tract, patch): 

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

776 return condition 

777 

778 

779class GetMultiTractCoaddTemplateConfig(GetTemplateConfig): 

780 pass 

781 

782 

783class GetMultiTractCoaddTemplateTask(GetTemplateTask): 

784 ConfigClass = GetMultiTractCoaddTemplateConfig 

785 _DefaultName = "getMultiTractCoaddTemplate" 

786 

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

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

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